From 97d6f394dea5b7f5694ae82437efe7c08bb30d72 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 12 Feb 2026 23:04:18 +1300 Subject: [PATCH 001/197] Bump version to 2026.2.0b1 --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index efc2f464e3..16516a387f 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 = 2026.2.0-dev +PROJECT_NUMBER = 2026.2.0b1 # 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 00d8013cc6..3b5cccfb25 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2026.2.0-dev" +__version__ = "2026.2.0b1" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 8a08c688f628a0a2302db29dd52646168ea6fe99 Mon Sep 17 00:00:00 2001 From: schrob <83939986+schdro@users.noreply.github.com> Date: Thu, 12 Feb 2026 13:25:51 +0100 Subject: [PATCH 002/197] [mipi_spi] Add Waveshare 1.83 v2 panel (#13680) --- .../components/mipi_spi/models/waveshare.py | 83 ++++++++++++++++++- tests/components/mipi_spi/common.yaml | 24 +++++- 2 files changed, 103 insertions(+), 4 deletions(-) diff --git a/esphome/components/mipi_spi/models/waveshare.py b/esphome/components/mipi_spi/models/waveshare.py index e4e090da2e..cc86101f5e 100644 --- a/esphome/components/mipi_spi/models/waveshare.py +++ b/esphome/components/mipi_spi/models/waveshare.py @@ -1,4 +1,17 @@ -from esphome.components.mipi import DriverChip +from esphome.components.mipi import ( + ETMOD, + FRMCTR2, + GMCTRN1, + GMCTRP1, + IFCTR, + MODE_RGB, + PWCTR1, + PWCTR3, + PWCTR4, + PWCTR5, + PWSET, + DriverChip, +) import esphome.config_validation as cv from .amoled import CO5300 @@ -129,6 +142,16 @@ DriverChip( ), ), ) +ST7789P = DriverChip( + "ST7789P", + # Max supported dimensions + width=240, + height=320, + # SPI: RGB layout + color_order=MODE_RGB, + invert_colors=True, + draw_rounding=1, +) ILI9488_A.extend( "PICO-RESTOUCH-LCD-3.5", @@ -162,3 +185,61 @@ AXS15231.extend( cs_pin=9, reset_pin=21, ) + +# Waveshare 1.83-v2 +# +# Do not use on 1.83-v1: Vendor warning on different chip! +ST7789P.extend( + "WAVESHARE-1.83-V2", + # Panel size smaller than ST7789 max allowed + width=240, + height=284, + # Vendor specific init derived from vendor sample code + # "LCD_1.83_Code_Rev2/ESP32/LCD_1in83/LCD_Driver.cpp" + # Compatible MIT license, see esphome/LICENSE file. + initsequence=( + (FRMCTR2, 0x0C, 0x0C, 0x00, 0x33, 0x33), + (ETMOD, 0x35), + (0xBB, 0x19), + (PWCTR1, 0x2C), + (PWCTR3, 0x01), + (PWCTR4, 0x12), + (PWCTR5, 0x20), + (IFCTR, 0x0F), + (PWSET, 0xA4, 0xA1), + ( + GMCTRP1, + 0xD0, + 0x04, + 0x0D, + 0x11, + 0x13, + 0x2B, + 0x3F, + 0x54, + 0x4C, + 0x18, + 0x0D, + 0x0B, + 0x1F, + 0x23, + ), + ( + GMCTRN1, + 0xD0, + 0x04, + 0x0C, + 0x11, + 0x13, + 0x2C, + 0x3F, + 0x44, + 0x51, + 0x2F, + 0x1F, + 0x1F, + 0x20, + 0x23, + ), + ), +) diff --git a/tests/components/mipi_spi/common.yaml b/tests/components/mipi_spi/common.yaml index 692a9f436e..a867b726ed 100644 --- a/tests/components/mipi_spi/common.yaml +++ b/tests/components/mipi_spi/common.yaml @@ -3,9 +3,15 @@ display: spi_16: true pixel_mode: 18bit model: ili9488 - dc_pin: ${dc_pin} - cs_pin: ${cs_pin} - reset_pin: ${reset_pin} + dc_pin: + allow_other_uses: true + number: ${dc_pin} + cs_pin: + allow_other_uses: true + number: ${cs_pin} + reset_pin: + allow_other_uses: true + number: ${reset_pin} data_rate: 20MHz invert_colors: true show_test_card: true @@ -24,3 +30,15 @@ display: height: 200 enable_pin: ${enable_pin} bus_mode: single + + - platform: mipi_spi + model: WAVESHARE-1.83-V2 + dc_pin: + allow_other_uses: true + number: ${dc_pin} + cs_pin: + allow_other_uses: true + number: ${cs_pin} + reset_pin: + allow_other_uses: true + number: ${reset_pin} From 7b251dcc310a7751f85c22de33d493288c0f700a Mon Sep 17 00:00:00 2001 From: Guillermo Ruffino Date: Thu, 12 Feb 2026 13:23:59 -0300 Subject: [PATCH 003/197] [schema-gen] fix Windows: ensure UTF-8 encoding when reading component files (#13952) --- script/build_language_schema.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/build_language_schema.py b/script/build_language_schema.py index c9501cb193..bea540dc63 100755 --- a/script/build_language_schema.py +++ b/script/build_language_schema.py @@ -369,7 +369,7 @@ def get_logger_tags(): "api.service", ] for file in CORE_COMPONENTS_PATH.rglob("*.cpp"): - data = file.read_text() + data = file.read_text(encoding="utf-8") match = pattern.search(data) if match: tags.append(match.group(1)) From 9aa98ed6c65d7f2f025bdc9c5eb3ebe78e9bb71d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 12 Feb 2026 10:26:10 -0600 Subject: [PATCH 004/197] [uart] Remove redundant mutex, fix flush race, conditional event queue (#13955) --- .../uart/uart_component_esp_idf.cpp | 49 ++++++++++--------- .../components/uart/uart_component_esp_idf.h | 17 +++++-- 2 files changed, 41 insertions(+), 25 deletions(-) diff --git a/esphome/components/uart/uart_component_esp_idf.cpp b/esphome/components/uart/uart_component_esp_idf.cpp index 19b9a4077f..6c242220a6 100644 --- a/esphome/components/uart/uart_component_esp_idf.cpp +++ b/esphome/components/uart/uart_component_esp_idf.cpp @@ -90,7 +90,6 @@ void IDFUARTComponent::setup() { return; } this->uart_num_ = static_cast(next_uart_num++); - this->lock_ = xSemaphoreCreateMutex(); #if (SOC_UART_LP_NUM >= 1) size_t fifo_len = ((this->uart_num_ < SOC_UART_HP_NUM) ? SOC_UART_FIFO_LEN : SOC_LP_UART_FIFO_LEN); @@ -102,11 +101,7 @@ void IDFUARTComponent::setup() { this->rx_buffer_size_ = fifo_len * 2; } - xSemaphoreTake(this->lock_, portMAX_DELAY); - this->load_settings(false); - - xSemaphoreGive(this->lock_); } void IDFUARTComponent::load_settings(bool dump_config) { @@ -126,13 +121,20 @@ void IDFUARTComponent::load_settings(bool dump_config) { return; } } +#ifdef USE_UART_WAKE_LOOP_ON_RX + constexpr int event_queue_size = 20; + QueueHandle_t *event_queue_ptr = &this->uart_event_queue_; +#else + constexpr int event_queue_size = 0; + QueueHandle_t *event_queue_ptr = nullptr; +#endif err = uart_driver_install(this->uart_num_, // UART number this->rx_buffer_size_, // RX ring buffer size - 0, // TX ring buffer size. If zero, driver will not use a TX buffer and TX function will - // block task until all data has been sent out - 20, // event queue size/depth - &this->uart_event_queue_, // event queue - 0 // Flags used to allocate the interrupt + 0, // TX ring buffer size. If zero, driver will not use a TX buffer and TX function will + // block task until all data has been sent out + event_queue_size, // event queue size/depth + event_queue_ptr, // event queue + 0 // Flags used to allocate the interrupt ); if (err != ESP_OK) { ESP_LOGW(TAG, "uart_driver_install failed: %s", esp_err_to_name(err)); @@ -282,9 +284,7 @@ void IDFUARTComponent::set_rx_timeout(size_t rx_timeout) { } void IDFUARTComponent::write_array(const uint8_t *data, size_t len) { - xSemaphoreTake(this->lock_, portMAX_DELAY); int32_t write_len = uart_write_bytes(this->uart_num_, data, len); - xSemaphoreGive(this->lock_); if (write_len != (int32_t) len) { ESP_LOGW(TAG, "uart_write_bytes failed: %d != %zu", write_len, len); this->mark_failed(); @@ -299,7 +299,6 @@ void IDFUARTComponent::write_array(const uint8_t *data, size_t len) { bool IDFUARTComponent::peek_byte(uint8_t *data) { if (!this->check_read_timeout_()) return false; - xSemaphoreTake(this->lock_, portMAX_DELAY); if (this->has_peek_) { *data = this->peek_byte_; } else { @@ -311,7 +310,6 @@ bool IDFUARTComponent::peek_byte(uint8_t *data) { this->peek_byte_ = *data; } } - xSemaphoreGive(this->lock_); return true; } @@ -320,7 +318,6 @@ bool IDFUARTComponent::read_array(uint8_t *data, size_t len) { int32_t read_len = 0; if (!this->check_read_timeout_(len)) return false; - xSemaphoreTake(this->lock_, portMAX_DELAY); if (this->has_peek_) { length_to_read--; *data = this->peek_byte_; @@ -329,7 +326,6 @@ bool IDFUARTComponent::read_array(uint8_t *data, size_t len) { } if (length_to_read > 0) read_len = uart_read_bytes(this->uart_num_, data, length_to_read, 20 / portTICK_PERIOD_MS); - xSemaphoreGive(this->lock_); #ifdef USE_UART_DEBUGGER for (size_t i = 0; i < len; i++) { this->debug_callback_.call(UART_DIRECTION_RX, data[i]); @@ -342,9 +338,7 @@ size_t IDFUARTComponent::available() { size_t available = 0; esp_err_t err; - xSemaphoreTake(this->lock_, portMAX_DELAY); err = uart_get_buffered_data_len(this->uart_num_, &available); - xSemaphoreGive(this->lock_); if (err != ESP_OK) { ESP_LOGW(TAG, "uart_get_buffered_data_len failed: %s", esp_err_to_name(err)); @@ -358,9 +352,7 @@ size_t IDFUARTComponent::available() { void IDFUARTComponent::flush() { ESP_LOGVV(TAG, " Flushing"); - xSemaphoreTake(this->lock_, portMAX_DELAY); uart_wait_tx_done(this->uart_num_, portMAX_DELAY); - xSemaphoreGive(this->lock_); } void IDFUARTComponent::check_logger_conflict() {} @@ -384,6 +376,13 @@ void IDFUARTComponent::start_rx_event_task_() { ESP_LOGV(TAG, "RX event task started"); } +// FreeRTOS task that relays UART ISR events to the main loop. +// This task exists because wake_loop_threadsafe() is not ISR-safe (it uses a +// UDP loopback socket), so we need a task as an ISR-to-main-loop trampoline. +// IMPORTANT: This task must NOT call any UART wrapper methods (read_array, +// write_array, peek_byte, etc.) or touch has_peek_/peek_byte_ — all reading +// is done by the main loop. This task only reads from the event queue and +// calls App.wake_loop_threadsafe(). void IDFUARTComponent::rx_event_task_func(void *param) { auto *self = static_cast(param); uart_event_t event; @@ -405,8 +404,14 @@ void IDFUARTComponent::rx_event_task_func(void *param) { case UART_FIFO_OVF: case UART_BUFFER_FULL: - ESP_LOGW(TAG, "FIFO overflow or ring buffer full - clearing"); - uart_flush_input(self->uart_num_); + // Don't call uart_flush_input() here — this task does not own the read side. + // ESP-IDF examples flush on overflow because the same task handles both events + // and reads, so flush and read are serialized. Here, reads happen on the main + // loop, so flushing from this task races with read_array() and can destroy data + // mid-read. The driver self-heals without an explicit flush: uart_read_bytes() + // calls uart_check_buf_full() after each chunk, which moves stashed FIFO bytes + // into the ring buffer and re-enables RX interrupts once space is freed. + ESP_LOGW(TAG, "FIFO overflow or ring buffer full"); #if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) App.wake_loop_threadsafe(); #endif diff --git a/esphome/components/uart/uart_component_esp_idf.h b/esphome/components/uart/uart_component_esp_idf.h index 1ecb02d7ab..1517eab509 100644 --- a/esphome/components/uart/uart_component_esp_idf.h +++ b/esphome/components/uart/uart_component_esp_idf.h @@ -8,6 +8,13 @@ namespace esphome::uart { +/// ESP-IDF UART driver wrapper. +/// +/// Thread safety: All public methods must only be called from the main loop. +/// The ESP-IDF UART driver API does not guarantee thread safety, and ESPHome's +/// peek byte state (has_peek_/peek_byte_) is not synchronized. The rx_event_task +/// (when enabled) must not call any of these methods — it communicates with the +/// main loop exclusively via App.wake_loop_threadsafe(). class IDFUARTComponent : public UARTComponent, public Component { public: void setup() override; @@ -26,7 +33,9 @@ class IDFUARTComponent : public UARTComponent, public Component { void flush() override; uint8_t get_hw_serial_number() { return this->uart_num_; } +#ifdef USE_UART_WAKE_LOOP_ON_RX QueueHandle_t *get_uart_event_queue() { return &this->uart_event_queue_; } +#endif /** * Load the UART with the current settings. @@ -46,18 +55,20 @@ class IDFUARTComponent : public UARTComponent, public Component { protected: void check_logger_conflict() override; uart_port_t uart_num_; - QueueHandle_t uart_event_queue_; uart_config_t get_config_(); - SemaphoreHandle_t lock_; bool has_peek_{false}; uint8_t peek_byte_; #ifdef USE_UART_WAKE_LOOP_ON_RX - // RX notification support + // RX notification support — runs on a separate FreeRTOS task. + // IMPORTANT: rx_event_task_func must NOT call any UART wrapper methods (read_array, + // write_array, etc.) or touch has_peek_/peek_byte_. It must only read from the + // event queue and call App.wake_loop_threadsafe(). void start_rx_event_task_(); static void rx_event_task_func(void *param); + QueueHandle_t uart_event_queue_; TaskHandle_t rx_event_task_handle_{nullptr}; #endif // USE_UART_WAKE_LOOP_ON_RX }; From 725e774fe79aac107288af979bf8ff7da7479feb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 12 Feb 2026 10:26:36 -0600 Subject: [PATCH 005/197] [web_server] Guard icon JSON field with USE_ENTITY_ICON (#13948) --- esphome/components/web_server/web_server.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index dfd602be6b..7da8b49c6d 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -557,7 +557,9 @@ static void set_json_id(JsonObject &root, EntityBase *obj, const char *prefix, J root[ESPHOME_F("device")] = device_name; } #endif +#ifdef USE_ENTITY_ICON root[ESPHOME_F("icon")] = obj->get_icon_ref(); +#endif root[ESPHOME_F("entity_category")] = obj->get_entity_category(); bool is_disabled = obj->is_disabled_by_default(); if (is_disabled) From 60fef5e656396593f2a2867241ebf7c576549917 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 12 Feb 2026 10:26:54 -0600 Subject: [PATCH 006/197] [analyze_memory] Fix mDNS packet buffer miscategorized as wifi_config (#13949) Co-authored-by: Claude Opus 4.6 --- esphome/analyze_memory/const.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/esphome/analyze_memory/const.py b/esphome/analyze_memory/const.py index 66866615a6..3bdf555ae3 100644 --- a/esphome/analyze_memory/const.py +++ b/esphome/analyze_memory/const.py @@ -256,7 +256,7 @@ SYMBOL_PATTERNS = { "ipv6_stack": ["nd6_", "ip6_", "mld6_", "icmp6_", "icmp6_input"], # Order matters! More specific categories must come before general ones. # mdns must come before bluetooth to avoid "_mdns_disable_pcb" matching "ble_" pattern - "mdns_lib": ["mdns"], + "mdns_lib": ["mdns", "packet$"], # memory_mgmt must come before wifi_stack to catch mmu_hal_* symbols "memory_mgmt": [ "mem_", @@ -794,7 +794,6 @@ SYMBOL_PATTERNS = { "s_dp", "s_ni", "s_reg_dump", - "packet$", "d_mult_table", "K", "fcstab", From 0e1433329da006a6f58dfc1d5e086d93d743bf07 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 12 Feb 2026 11:04:23 -0600 Subject: [PATCH 007/197] [api] Extract cold code from APIServer::loop() hot path (#13902) --- esphome/components/api/api_server.cpp | 130 ++++++++++++++------------ esphome/components/api/api_server.h | 5 + 2 files changed, 74 insertions(+), 61 deletions(-) diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 53b41a5c14..5503cf4db8 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -117,37 +117,7 @@ void APIServer::setup() { void APIServer::loop() { // Accept new clients only if the socket exists and has incoming connections if (this->socket_ && this->socket_->ready()) { - while (true) { - struct sockaddr_storage source_addr; - socklen_t addr_len = sizeof(source_addr); - - auto sock = this->socket_->accept_loop_monitored((struct sockaddr *) &source_addr, &addr_len); - if (!sock) - break; - - char peername[socket::SOCKADDR_STR_LEN]; - sock->getpeername_to(peername); - - // Check if we're at the connection limit - if (this->clients_.size() >= this->max_connections_) { - ESP_LOGW(TAG, "Max connections (%d), rejecting %s", this->max_connections_, peername); - // Immediately close - socket destructor will handle cleanup - sock.reset(); - continue; - } - - ESP_LOGD(TAG, "Accept %s", peername); - - auto *conn = new APIConnection(std::move(sock), this); - this->clients_.emplace_back(conn); - conn->start(); - - // First client connected - clear warning and update timestamp - if (this->clients_.size() == 1 && this->reboot_timeout_ != 0) { - this->status_clear_warning(); - this->last_connected_ = App.get_loop_component_start_time(); - } - } + this->accept_new_connections_(); } if (this->clients_.empty()) { @@ -178,46 +148,84 @@ void APIServer::loop() { while (client_index < this->clients_.size()) { auto &client = this->clients_[client_index]; - if (!client->flags_.remove) { + if (client->flags_.remove) { + // Rare case: handle disconnection (don't increment - swapped element needs processing) + this->remove_client_(client_index); + } else { // Common case: process active client client->loop(); client_index++; + } + } +} + +void APIServer::remove_client_(size_t client_index) { + auto &client = this->clients_[client_index]; + +#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES + this->unregister_active_action_calls_for_connection(client.get()); +#endif + ESP_LOGV(TAG, "Remove connection %s", client->get_name()); + +#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER + // Save client info before closing socket and removal for the trigger + char peername_buf[socket::SOCKADDR_STR_LEN]; + std::string client_name(client->get_name()); + std::string client_peername(client->get_peername_to(peername_buf)); +#endif + + // Close socket now (was deferred from on_fatal_error to allow getpeername) + client->helper_->close(); + + // Swap with the last element and pop (avoids expensive vector shifts) + if (client_index < this->clients_.size() - 1) { + std::swap(this->clients_[client_index], this->clients_.back()); + } + this->clients_.pop_back(); + + // Last client disconnected - set warning and start tracking for reboot timeout + if (this->clients_.empty() && this->reboot_timeout_ != 0) { + this->status_set_warning(); + this->last_connected_ = App.get_loop_component_start_time(); + } + +#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER + // Fire trigger after client is removed so api.connected reflects the true state + this->client_disconnected_trigger_.trigger(client_name, client_peername); +#endif +} + +void APIServer::accept_new_connections_() { + while (true) { + struct sockaddr_storage source_addr; + socklen_t addr_len = sizeof(source_addr); + + auto sock = this->socket_->accept_loop_monitored((struct sockaddr *) &source_addr, &addr_len); + if (!sock) + break; + + char peername[socket::SOCKADDR_STR_LEN]; + sock->getpeername_to(peername); + + // Check if we're at the connection limit + if (this->clients_.size() >= this->max_connections_) { + ESP_LOGW(TAG, "Max connections (%d), rejecting %s", this->max_connections_, peername); + // Immediately close - socket destructor will handle cleanup + sock.reset(); continue; } - // Rare case: handle disconnection -#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES - this->unregister_active_action_calls_for_connection(client.get()); -#endif - ESP_LOGV(TAG, "Remove connection %s", client->get_name()); + ESP_LOGD(TAG, "Accept %s", peername); -#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER - // Save client info before closing socket and removal for the trigger - char peername_buf[socket::SOCKADDR_STR_LEN]; - std::string client_name(client->get_name()); - std::string client_peername(client->get_peername_to(peername_buf)); -#endif + auto *conn = new APIConnection(std::move(sock), this); + this->clients_.emplace_back(conn); + conn->start(); - // Close socket now (was deferred from on_fatal_error to allow getpeername) - client->helper_->close(); - - // Swap with the last element and pop (avoids expensive vector shifts) - if (client_index < this->clients_.size() - 1) { - std::swap(this->clients_[client_index], this->clients_.back()); - } - this->clients_.pop_back(); - - // Last client disconnected - set warning and start tracking for reboot timeout - if (this->clients_.empty() && this->reboot_timeout_ != 0) { - this->status_set_warning(); + // First client connected - clear warning and update timestamp + if (this->clients_.size() == 1 && this->reboot_timeout_ != 0) { + this->status_clear_warning(); this->last_connected_ = App.get_loop_component_start_time(); } - -#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER - // Fire trigger after client is removed so api.connected reflects the true state - this->client_disconnected_trigger_.trigger(client_name, client_peername); -#endif - // Don't increment client_index since we need to process the swapped element } } diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index 6ab3cdc576..28f60343e0 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -234,6 +234,11 @@ class APIServer : public Component, #endif protected: + // Accept incoming socket connections. Only called when socket has pending connections. + void __attribute__((noinline)) accept_new_connections_(); + // Remove a disconnected client by index. Swaps with last element and pops. + void __attribute__((noinline)) remove_client_(size_t client_index); + #ifdef USE_API_NOISE bool update_noise_psk_(const SavedNoisePsk &new_psk, const LogString *save_log_msg, const LogString *fail_log_msg, const psk_t &active_psk, bool make_active); From cde8b6671932d3b75b0a323ef947f24cd9f2872f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 12 Feb 2026 11:04:41 -0600 Subject: [PATCH 008/197] [web_server] Switch from getParam to arg API to eliminate heap allocations (#13942) --- .../captive_portal/captive_portal.cpp | 8 +- esphome/components/web_server/web_server.cpp | 94 +++++++++---------- esphome/components/web_server/web_server.h | 53 ++++------- esphome/components/web_server_idf/utils.cpp | 41 ++++---- esphome/components/web_server_idf/utils.h | 5 +- .../web_server_idf/web_server_idf.cpp | 52 ++++++++-- .../web_server_idf/web_server_idf.h | 11 +-- 7 files changed, 134 insertions(+), 130 deletions(-) diff --git a/esphome/components/captive_portal/captive_portal.cpp b/esphome/components/captive_portal/captive_portal.cpp index 8d88a10b27..5af6ab29a2 100644 --- a/esphome/components/captive_portal/captive_portal.cpp +++ b/esphome/components/captive_portal/captive_portal.cpp @@ -47,8 +47,8 @@ void CaptivePortal::handle_config(AsyncWebServerRequest *request) { request->send(stream); } void CaptivePortal::handle_wifisave(AsyncWebServerRequest *request) { - std::string ssid = request->arg("ssid").c_str(); // NOLINT(readability-redundant-string-cstr) - std::string psk = request->arg("psk").c_str(); // NOLINT(readability-redundant-string-cstr) + const auto &ssid = request->arg("ssid"); + const auto &psk = request->arg("psk"); ESP_LOGI(TAG, "Requested WiFi Settings Change:\n" " SSID='%s'\n" @@ -56,10 +56,10 @@ void CaptivePortal::handle_wifisave(AsyncWebServerRequest *request) { ssid.c_str(), psk.c_str()); #ifdef USE_ESP8266 // ESP8266 is single-threaded, call directly - wifi::global_wifi_component->save_wifi_sta(ssid, psk); + wifi::global_wifi_component->save_wifi_sta(ssid.c_str(), psk.c_str()); #else // Defer save to main loop thread to avoid NVS operations from HTTP thread - this->defer([ssid, psk]() { wifi::global_wifi_component->save_wifi_sta(ssid, psk); }); + this->defer([ssid, psk]() { wifi::global_wifi_component->save_wifi_sta(ssid.c_str(), psk.c_str()); }); #endif request->redirect(ESPHOME_F("/?save")); } diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 7da8b49c6d..c7a0639382 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -585,8 +585,7 @@ static void set_json_icon_state_value(JsonObject &root, EntityBase *obj, const c // Helper to get request detail parameter static JsonDetail get_request_detail(AsyncWebServerRequest *request) { - auto *param = request->getParam(ESPHOME_F("detail")); - return (param && param->value() == "all") ? DETAIL_ALL : DETAIL_STATE; + return request->arg(ESPHOME_F("detail")) == "all" ? DETAIL_ALL : DETAIL_STATE; } #ifdef USE_SENSOR @@ -863,10 +862,10 @@ void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatc } auto call = is_on ? obj->turn_on() : obj->turn_off(); - parse_int_param_(request, ESPHOME_F("speed_level"), call, &decltype(call)::set_speed); + parse_num_param_(request, ESPHOME_F("speed_level"), call, &decltype(call)::set_speed); - if (request->hasParam(ESPHOME_F("oscillation"))) { - auto speed = request->getParam(ESPHOME_F("oscillation"))->value(); + if (request->hasArg(ESPHOME_F("oscillation"))) { + auto speed = request->arg(ESPHOME_F("oscillation")); auto val = parse_on_off(speed.c_str()); switch (val) { case PARSE_ON: @@ -1042,14 +1041,14 @@ void WebServer::handle_cover_request(AsyncWebServerRequest *request, const UrlMa } auto traits = obj->get_traits(); - if ((request->hasParam(ESPHOME_F("position")) && !traits.get_supports_position()) || - (request->hasParam(ESPHOME_F("tilt")) && !traits.get_supports_tilt())) { + if ((request->hasArg(ESPHOME_F("position")) && !traits.get_supports_position()) || + (request->hasArg(ESPHOME_F("tilt")) && !traits.get_supports_tilt())) { request->send(409); return; } - parse_float_param_(request, ESPHOME_F("position"), call, &decltype(call)::set_position); - parse_float_param_(request, ESPHOME_F("tilt"), call, &decltype(call)::set_tilt); + parse_num_param_(request, ESPHOME_F("position"), call, &decltype(call)::set_position); + parse_num_param_(request, ESPHOME_F("tilt"), call, &decltype(call)::set_tilt); DEFER_ACTION(call, call.perform()); request->send(200); @@ -1108,7 +1107,7 @@ void WebServer::handle_number_request(AsyncWebServerRequest *request, const UrlM } auto call = obj->make_call(); - parse_float_param_(request, ESPHOME_F("value"), call, &decltype(call)::set_value); + parse_num_param_(request, ESPHOME_F("value"), call, &decltype(call)::set_value); DEFER_ACTION(call, call.perform()); request->send(200); @@ -1176,12 +1175,13 @@ void WebServer::handle_date_request(AsyncWebServerRequest *request, const UrlMat auto call = obj->make_call(); - if (!request->hasParam(ESPHOME_F("value"))) { + const auto &value = request->arg(ESPHOME_F("value")); + // Arduino String has isEmpty() not empty(), use length() for cross-platform compatibility + if (value.length() == 0) { // NOLINT(readability-container-size-empty) request->send(409); return; } - - parse_string_param_(request, ESPHOME_F("value"), call, &decltype(call)::set_date); + call.set_date(value.c_str(), value.length()); DEFER_ACTION(call, call.perform()); request->send(200); @@ -1236,12 +1236,13 @@ void WebServer::handle_time_request(AsyncWebServerRequest *request, const UrlMat auto call = obj->make_call(); - if (!request->hasParam(ESPHOME_F("value"))) { + const auto &value = request->arg(ESPHOME_F("value")); + // Arduino String has isEmpty() not empty(), use length() for cross-platform compatibility + if (value.length() == 0) { // NOLINT(readability-container-size-empty) request->send(409); return; } - - parse_string_param_(request, ESPHOME_F("value"), call, &decltype(call)::set_time); + call.set_time(value.c_str(), value.length()); DEFER_ACTION(call, call.perform()); request->send(200); @@ -1295,12 +1296,13 @@ void WebServer::handle_datetime_request(AsyncWebServerRequest *request, const Ur auto call = obj->make_call(); - if (!request->hasParam(ESPHOME_F("value"))) { + const auto &value = request->arg(ESPHOME_F("value")); + // Arduino String has isEmpty() not empty(), use length() for cross-platform compatibility + if (value.length() == 0) { // NOLINT(readability-container-size-empty) request->send(409); return; } - - parse_string_param_(request, ESPHOME_F("value"), call, &decltype(call)::set_datetime); + call.set_datetime(value.c_str(), value.length()); DEFER_ACTION(call, call.perform()); request->send(200); @@ -1479,10 +1481,14 @@ void WebServer::handle_climate_request(AsyncWebServerRequest *request, const Url parse_string_param_(request, ESPHOME_F("swing_mode"), call, &decltype(call)::set_swing_mode); // Parse temperature parameters - parse_float_param_(request, ESPHOME_F("target_temperature_high"), call, - &decltype(call)::set_target_temperature_high); - parse_float_param_(request, ESPHOME_F("target_temperature_low"), call, &decltype(call)::set_target_temperature_low); - parse_float_param_(request, ESPHOME_F("target_temperature"), call, &decltype(call)::set_target_temperature); + // static_cast needed to disambiguate overloaded setters (float vs optional) + using ClimateCall = decltype(call); + parse_num_param_(request, ESPHOME_F("target_temperature_high"), call, + static_cast(&ClimateCall::set_target_temperature_high)); + parse_num_param_(request, ESPHOME_F("target_temperature_low"), call, + static_cast(&ClimateCall::set_target_temperature_low)); + parse_num_param_(request, ESPHOME_F("target_temperature"), call, + static_cast(&ClimateCall::set_target_temperature)); DEFER_ACTION(call, call.perform()); request->send(200); @@ -1723,12 +1729,12 @@ void WebServer::handle_valve_request(AsyncWebServerRequest *request, const UrlMa } auto traits = obj->get_traits(); - if (request->hasParam(ESPHOME_F("position")) && !traits.get_supports_position()) { + if (request->hasArg(ESPHOME_F("position")) && !traits.get_supports_position()) { request->send(409); return; } - parse_float_param_(request, ESPHOME_F("position"), call, &decltype(call)::set_position); + parse_num_param_(request, ESPHOME_F("position"), call, &decltype(call)::set_position); DEFER_ACTION(call, call.perform()); request->send(200); @@ -1872,12 +1878,12 @@ void WebServer::handle_water_heater_request(AsyncWebServerRequest *request, cons parse_string_param_(request, ESPHOME_F("mode"), base_call, &water_heater::WaterHeaterCall::set_mode); // Parse temperature parameters - parse_float_param_(request, ESPHOME_F("target_temperature"), base_call, - &water_heater::WaterHeaterCall::set_target_temperature); - parse_float_param_(request, ESPHOME_F("target_temperature_low"), base_call, - &water_heater::WaterHeaterCall::set_target_temperature_low); - parse_float_param_(request, ESPHOME_F("target_temperature_high"), base_call, - &water_heater::WaterHeaterCall::set_target_temperature_high); + parse_num_param_(request, ESPHOME_F("target_temperature"), base_call, + &water_heater::WaterHeaterCall::set_target_temperature); + parse_num_param_(request, ESPHOME_F("target_temperature_low"), base_call, + &water_heater::WaterHeaterCall::set_target_temperature_low); + parse_num_param_(request, ESPHOME_F("target_temperature_high"), base_call, + &water_heater::WaterHeaterCall::set_target_temperature_high); // Parse away mode parameter parse_bool_param_(request, ESPHOME_F("away"), base_call, &water_heater::WaterHeaterCall::set_away); @@ -1981,16 +1987,16 @@ void WebServer::handle_infrared_request(AsyncWebServerRequest *request, const Ur auto call = obj->make_call(); // Parse carrier frequency (optional) - if (request->hasParam(ESPHOME_F("carrier_frequency"))) { - auto value = parse_number(request->getParam(ESPHOME_F("carrier_frequency"))->value().c_str()); + { + auto value = parse_number(request->arg(ESPHOME_F("carrier_frequency")).c_str()); if (value.has_value()) { call.set_carrier_frequency(*value); } } // Parse repeat count (optional, defaults to 1) - if (request->hasParam(ESPHOME_F("repeat_count"))) { - auto value = parse_number(request->getParam(ESPHOME_F("repeat_count"))->value().c_str()); + { + auto value = parse_number(request->arg(ESPHOME_F("repeat_count")).c_str()); if (value.has_value()) { call.set_repeat_count(*value); } @@ -1998,18 +2004,12 @@ void WebServer::handle_infrared_request(AsyncWebServerRequest *request, const Ur // Parse base64url-encoded raw timings (required) // Base64url is URL-safe: uses A-Za-z0-9-_ (no special characters needing escaping) - if (!request->hasParam(ESPHOME_F("data"))) { - request->send(400, ESPHOME_F("text/plain"), ESPHOME_F("Missing 'data' parameter")); - return; - } + const auto &data_arg = request->arg(ESPHOME_F("data")); - // .c_str() is required for Arduino framework where value() returns Arduino String instead of std::string - std::string encoded = - request->getParam(ESPHOME_F("data"))->value().c_str(); // NOLINT(readability-redundant-string-cstr) - - // Validate base64url is not empty - if (encoded.empty()) { - request->send(400, ESPHOME_F("text/plain"), ESPHOME_F("Empty 'data' parameter")); + // Validate base64url is not empty (also catches missing parameter since arg() returns empty string) + // Arduino String has isEmpty() not empty(), use length() for cross-platform compatibility + if (data_arg.length() == 0) { // NOLINT(readability-container-size-empty) + request->send(400, ESPHOME_F("text/plain"), ESPHOME_F("Missing or empty 'data' parameter")); return; } @@ -2017,7 +2017,7 @@ void WebServer::handle_infrared_request(AsyncWebServerRequest *request, const Ur // it outlives the call - set_raw_timings_base64url stores a pointer, so the string // must remain valid until perform() completes. // ESP8266 also needs this because ESPAsyncWebServer callbacks run in "sys" context. - this->defer([call, encoded = std::move(encoded)]() mutable { + this->defer([call, encoded = std::string(data_arg.c_str(), data_arg.length())]() mutable { call.set_raw_timings_base64url(encoded); call.perform(); }); diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index ce09ebf7a9..026da763ea 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -513,11 +513,9 @@ class WebServer : public Controller, template void parse_light_param_(AsyncWebServerRequest *request, ParamNameType param_name, T &call, Ret (T::*setter)(float), float scale = 1.0f) { - if (request->hasParam(param_name)) { - auto value = parse_number(request->getParam(param_name)->value().c_str()); - if (value.has_value()) { - (call.*setter)(*value / scale); - } + auto value = parse_number(request->arg(param_name).c_str()); + if (value.has_value()) { + (call.*setter)(*value / scale); } } @@ -525,34 +523,19 @@ class WebServer : public Controller, template void parse_light_param_uint_(AsyncWebServerRequest *request, ParamNameType param_name, T &call, Ret (T::*setter)(uint32_t), uint32_t scale = 1) { - if (request->hasParam(param_name)) { - auto value = parse_number(request->getParam(param_name)->value().c_str()); - if (value.has_value()) { - (call.*setter)(*value * scale); - } + auto value = parse_number(request->arg(param_name).c_str()); + if (value.has_value()) { + (call.*setter)(*value * scale); } } #endif - // Generic helper to parse and apply a float parameter - template - void parse_float_param_(AsyncWebServerRequest *request, ParamNameType param_name, T &call, Ret (T::*setter)(float)) { - if (request->hasParam(param_name)) { - auto value = parse_number(request->getParam(param_name)->value().c_str()); - if (value.has_value()) { - (call.*setter)(*value); - } - } - } - - // Generic helper to parse and apply an int parameter - template - void parse_int_param_(AsyncWebServerRequest *request, ParamNameType param_name, T &call, Ret (T::*setter)(int)) { - if (request->hasParam(param_name)) { - auto value = parse_number(request->getParam(param_name)->value().c_str()); - if (value.has_value()) { - (call.*setter)(*value); - } + // Generic helper to parse and apply a numeric parameter + template + void parse_num_param_(AsyncWebServerRequest *request, ParamNameType param_name, T &call, Ret (T::*setter)(NumT)) { + auto value = parse_number(request->arg(param_name).c_str()); + if (value.has_value()) { + (call.*setter)(*value); } } @@ -560,10 +543,9 @@ class WebServer : public Controller, template void parse_string_param_(AsyncWebServerRequest *request, ParamNameType param_name, T &call, Ret (T::*setter)(const std::string &)) { - if (request->hasParam(param_name)) { - // .c_str() is required for Arduino framework where value() returns Arduino String instead of std::string - std::string value = request->getParam(param_name)->value().c_str(); // NOLINT(readability-redundant-string-cstr) - (call.*setter)(value); + if (request->hasArg(param_name)) { + const auto &value = request->arg(param_name); + (call.*setter)(std::string(value.c_str(), value.length())); } } @@ -573,8 +555,9 @@ class WebServer : public Controller, // Invalid values are ignored (setter not called) template void parse_bool_param_(AsyncWebServerRequest *request, ParamNameType param_name, T &call, Ret (T::*setter)(bool)) { - if (request->hasParam(param_name)) { - auto param_value = request->getParam(param_name)->value(); + const auto ¶m_value = request->arg(param_name); + // Arduino String has isEmpty() not empty(), use length() for cross-platform compatibility + if (param_value.length() > 0) { // NOLINT(readability-container-size-empty) // First check on/off (default), then true/false (custom) auto val = parse_on_off(param_value.c_str()); if (val == PARSE_NONE) { diff --git a/esphome/components/web_server_idf/utils.cpp b/esphome/components/web_server_idf/utils.cpp index 81ae626277..c58ca2a24f 100644 --- a/esphome/components/web_server_idf/utils.cpp +++ b/esphome/components/web_server_idf/utils.cpp @@ -1,17 +1,13 @@ #ifdef USE_ESP32 -#include #include #include #include "esphome/core/helpers.h" -#include "esphome/core/log.h" #include "http_parser.h" #include "utils.h" namespace esphome::web_server_idf { -static const char *const TAG = "web_server_idf_utils"; - size_t url_decode(char *str) { char *start = str; char *ptr = str, buf; @@ -54,32 +50,15 @@ optional request_get_header(httpd_req_t *req, const char *name) { return {str}; } -optional request_get_url_query(httpd_req_t *req) { - auto len = httpd_req_get_url_query_len(req); - if (len == 0) { - return {}; - } - - std::string str; - str.resize(len); - - auto res = httpd_req_get_url_query_str(req, &str[0], len + 1); - if (res != ESP_OK) { - ESP_LOGW(TAG, "Can't get query for request: %s", esp_err_to_name(res)); - return {}; - } - - return {str}; -} - optional query_key_value(const char *query_url, size_t query_len, const char *key) { if (query_url == nullptr || query_len == 0) { return {}; } - // Use stack buffer for typical query strings, heap fallback for large ones - SmallBufferWithHeapFallback<256, char> val(query_len); - + // Value can't exceed query_len. Use small stack buffer for typical values, + // heap fallback for long ones (e.g. base64 IR data) to limit stack usage + // since callers may also have stack buffers for the query string. + SmallBufferWithHeapFallback<128, char> val(query_len); if (httpd_query_key_value(query_url, key, val.get(), query_len) != ESP_OK) { return {}; } @@ -88,6 +67,18 @@ optional query_key_value(const char *query_url, size_t query_len, c return {val.get()}; } +bool query_has_key(const char *query_url, size_t query_len, const char *key) { + if (query_url == nullptr || query_len == 0) { + return false; + } + // Minimal buffer — we only care if the key exists, not the value + char buf[1]; + // httpd_query_key_value returns ESP_OK if found, ESP_ERR_HTTPD_RESULT_TRUNC if found + // but value truncated (expected with 1-byte buffer), or other errors for invalid input + auto err = httpd_query_key_value(query_url, key, buf, sizeof(buf)); + return err == ESP_OK || err == ESP_ERR_HTTPD_RESULT_TRUNC; +} + // Helper function for case-insensitive string region comparison bool str_ncmp_ci(const char *s1, const char *s2, size_t n) { for (size_t i = 0; i < n; i++) { diff --git a/esphome/components/web_server_idf/utils.h b/esphome/components/web_server_idf/utils.h index 87635c0458..027a2f7b6c 100644 --- a/esphome/components/web_server_idf/utils.h +++ b/esphome/components/web_server_idf/utils.h @@ -13,11 +13,8 @@ size_t url_decode(char *str); bool request_has_header(httpd_req_t *req, const char *name); optional request_get_header(httpd_req_t *req, const char *name); -optional request_get_url_query(httpd_req_t *req); optional query_key_value(const char *query_url, size_t query_len, const char *key); -inline optional query_key_value(const std::string &query_url, const std::string &key) { - return query_key_value(query_url.c_str(), query_url.size(), key.c_str()); -} +bool query_has_key(const char *query_url, size_t query_len, const char *key); // Helper function for case-insensitive character comparison inline bool char_equals_ci(char a, char b) { return ::tolower(a) == ::tolower(b); } diff --git a/esphome/components/web_server_idf/web_server_idf.cpp b/esphome/components/web_server_idf/web_server_idf.cpp index f1f89beb49..1798159e7f 100644 --- a/esphome/components/web_server_idf/web_server_idf.cpp +++ b/esphome/components/web_server_idf/web_server_idf.cpp @@ -393,13 +393,7 @@ AsyncWebParameter *AsyncWebServerRequest::getParam(const char *name) { } // Look up value from query strings - optional val = query_key_value(this->post_query_.c_str(), this->post_query_.size(), name); - if (!val.has_value()) { - auto url_query = request_get_url_query(*this); - if (url_query.has_value()) { - val = query_key_value(url_query.value().c_str(), url_query.value().size(), name); - } - } + auto val = this->find_query_value_(name); // Don't cache misses to avoid wasting memory when handlers check for // optional parameters that don't exist in the request @@ -412,6 +406,50 @@ AsyncWebParameter *AsyncWebServerRequest::getParam(const char *name) { return param; } +/// Search post_query then URL query with a callback. +/// Returns first truthy result, or value-initialized default. +/// URL query is accessed directly from req->uri (same pattern as url_to()). +template +static auto search_query_sources(httpd_req_t *req, const std::string &post_query, const char *name, Func func) + -> decltype(func(nullptr, size_t{0}, name)) { + if (!post_query.empty()) { + auto result = func(post_query.c_str(), post_query.size(), name); + if (result) { + return result; + } + } + // Use httpd API for query length, then access string directly from URI. + // http_parser identifies components by offset/length without modifying the URI string. + // This is the same pattern used by url_to(). + auto len = httpd_req_get_url_query_len(req); + if (len == 0) { + return {}; + } + const char *query = strchr(req->uri, '?'); + if (query == nullptr) { + return {}; + } + query++; // skip '?' + return func(query, len, name); +} + +optional AsyncWebServerRequest::find_query_value_(const char *name) const { + return search_query_sources(this->req_, this->post_query_, name, + [](const char *q, size_t len, const char *k) { return query_key_value(q, len, k); }); +} + +bool AsyncWebServerRequest::hasArg(const char *name) { + return search_query_sources(this->req_, this->post_query_, name, query_has_key); +} + +std::string AsyncWebServerRequest::arg(const char *name) { + auto val = this->find_query_value_(name); + if (val.has_value()) { + return std::move(val.value()); + } + return {}; +} + void AsyncWebServerResponse::addHeader(const char *name, const char *value) { httpd_resp_set_hdr(*this->req_, name, value); } diff --git a/esphome/components/web_server_idf/web_server_idf.h b/esphome/components/web_server_idf/web_server_idf.h index 6a409de74e..12df0303de 100644 --- a/esphome/components/web_server_idf/web_server_idf.h +++ b/esphome/components/web_server_idf/web_server_idf.h @@ -170,14 +170,8 @@ class AsyncWebServerRequest { AsyncWebParameter *getParam(const std::string &name) { return this->getParam(name.c_str()); } // NOLINTNEXTLINE(readability-identifier-naming) - bool hasArg(const char *name) { return this->hasParam(name); } - std::string arg(const char *name) { - auto *param = this->getParam(name); - if (param) { - return param->value(); - } - return {}; - } + bool hasArg(const char *name); + std::string arg(const char *name); std::string arg(const std::string &name) { return this->arg(name.c_str()); } operator httpd_req_t *() const { return this->req_; } @@ -192,6 +186,7 @@ class AsyncWebServerRequest { // is faster than tree/hash overhead. AsyncWebParameter stores both name and value to avoid // duplicate storage. Only successful lookups are cached to prevent cache pollution when // handlers check for optional parameters that don't exist. + optional find_query_value_(const char *name) const; std::vector params_; std::string post_query_; AsyncWebServerRequest(httpd_req_t *req) : req_(req) {} From 0dcff82bb479b131a4dbb33d00940c7a91120dc2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 12 Feb 2026 11:14:36 -0600 Subject: [PATCH 009/197] [wifi] Deprecate wifi_ssid() in favor of wifi_ssid_to() (#13958) --- esphome/components/wifi/wifi_component.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index ac28a1bc81..53ff0d9cad 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -502,6 +502,8 @@ class WiFiComponent : public Component { } network::IPAddresses wifi_sta_ip_addresses(); + // Remove before 2026.9.0 + ESPDEPRECATED("Use wifi_ssid_to() instead. Removed in 2026.9.0", "2026.3.0") std::string wifi_ssid(); /// Write SSID to buffer without heap allocation. /// Returns pointer to buffer, or empty string if not connected. From e3a457e40268949364f776fd5ec3ff5fd833a443 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Ma=C5=88as?= Date: Thu, 12 Feb 2026 18:20:54 +0100 Subject: [PATCH 010/197] [pulse_meter] Fix early edge detection (#12360) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- .../pulse_meter/pulse_meter_sensor.cpp | 75 ++++++++++--------- .../pulse_meter/pulse_meter_sensor.h | 11 ++- 2 files changed, 44 insertions(+), 42 deletions(-) diff --git a/esphome/components/pulse_meter/pulse_meter_sensor.cpp b/esphome/components/pulse_meter/pulse_meter_sensor.cpp index 007deb66e5..433e1f0b7e 100644 --- a/esphome/components/pulse_meter/pulse_meter_sensor.cpp +++ b/esphome/components/pulse_meter/pulse_meter_sensor.cpp @@ -38,8 +38,7 @@ void PulseMeterSensor::setup() { } void PulseMeterSensor::loop() { - // Reset the count in get before we pass it back to the ISR as set - this->get_->count_ = 0; + State state; { // Lock the interrupt so the interrupt code doesn't interfere with itself @@ -58,31 +57,35 @@ void PulseMeterSensor::loop() { } this->last_pin_val_ = current; - // Swap out set and get to get the latest state from the ISR - std::swap(this->set_, this->get_); + // Get the latest state from the ISR and reset the count in the ISR + state.last_detected_edge_us_ = this->state_.last_detected_edge_us_; + state.last_rising_edge_us_ = this->state_.last_rising_edge_us_; + state.count_ = this->state_.count_; + this->state_.count_ = 0; } const uint32_t now = micros(); // If an edge was peeked, repay the debt - if (this->peeked_edge_ && this->get_->count_ > 0) { + if (this->peeked_edge_ && state.count_ > 0) { this->peeked_edge_ = false; - this->get_->count_--; // NOLINT(clang-diagnostic-deprecated-volatile) + state.count_--; } - // If there is an unprocessed edge, and filter_us_ has passed since, count this edge early - if (this->get_->last_rising_edge_us_ != this->get_->last_detected_edge_us_ && - now - this->get_->last_rising_edge_us_ >= this->filter_us_) { + // If there is an unprocessed edge, and filter_us_ has passed since, count this edge early. + // Wait for the debt to be repaid before counting another unprocessed edge early. + if (!this->peeked_edge_ && state.last_rising_edge_us_ != state.last_detected_edge_us_ && + now - state.last_rising_edge_us_ >= this->filter_us_) { this->peeked_edge_ = true; - this->get_->last_detected_edge_us_ = this->get_->last_rising_edge_us_; - this->get_->count_++; // NOLINT(clang-diagnostic-deprecated-volatile) + state.last_detected_edge_us_ = state.last_rising_edge_us_; + state.count_++; } // Check if we detected a pulse this loop - if (this->get_->count_ > 0) { + if (state.count_ > 0) { // Keep a running total of pulses if a total sensor is configured if (this->total_sensor_ != nullptr) { - this->total_pulses_ += this->get_->count_; + this->total_pulses_ += state.count_; const uint32_t total = this->total_pulses_; this->total_sensor_->publish_state(total); } @@ -94,15 +97,15 @@ void PulseMeterSensor::loop() { this->meter_state_ = MeterState::RUNNING; } break; case MeterState::RUNNING: { - uint32_t delta_us = this->get_->last_detected_edge_us_ - this->last_processed_edge_us_; - float pulse_width_us = delta_us / float(this->get_->count_); - ESP_LOGV(TAG, "New pulse, delta: %" PRIu32 " µs, count: %" PRIu32 ", width: %.5f µs", delta_us, - this->get_->count_, pulse_width_us); + uint32_t delta_us = state.last_detected_edge_us_ - this->last_processed_edge_us_; + float pulse_width_us = delta_us / float(state.count_); + ESP_LOGV(TAG, "New pulse, delta: %" PRIu32 " µs, count: %" PRIu32 ", width: %.5f µs", delta_us, state.count_, + pulse_width_us); this->publish_state((60.0f * 1000000.0f) / pulse_width_us); } break; } - this->last_processed_edge_us_ = this->get_->last_detected_edge_us_; + this->last_processed_edge_us_ = state.last_detected_edge_us_; } // No detected edges this loop else { @@ -141,14 +144,14 @@ void IRAM_ATTR PulseMeterSensor::edge_intr(PulseMeterSensor *sensor) { // This is an interrupt handler - we can't call any virtual method from this method // Get the current time before we do anything else so the measurements are consistent const uint32_t now = micros(); - auto &state = sensor->edge_state_; - auto &set = *sensor->set_; + auto &edge_state = sensor->edge_state_; + auto &state = sensor->state_; - if ((now - state.last_sent_edge_us_) >= sensor->filter_us_) { - state.last_sent_edge_us_ = now; - set.last_detected_edge_us_ = now; - set.last_rising_edge_us_ = now; - set.count_++; // NOLINT(clang-diagnostic-deprecated-volatile) + if ((now - edge_state.last_sent_edge_us_) >= sensor->filter_us_) { + edge_state.last_sent_edge_us_ = now; + state.last_detected_edge_us_ = now; + state.last_rising_edge_us_ = now; + state.count_++; // NOLINT(clang-diagnostic-deprecated-volatile) } // This ISR is bound to rising edges, so the pin is high @@ -160,26 +163,26 @@ void IRAM_ATTR PulseMeterSensor::pulse_intr(PulseMeterSensor *sensor) { // Get the current time before we do anything else so the measurements are consistent const uint32_t now = micros(); const bool pin_val = sensor->isr_pin_.digital_read(); - auto &state = sensor->pulse_state_; - auto &set = *sensor->set_; + auto &pulse_state = sensor->pulse_state_; + auto &state = sensor->state_; // Filter length has passed since the last interrupt - const bool length = now - state.last_intr_ >= sensor->filter_us_; + const bool length = now - pulse_state.last_intr_ >= sensor->filter_us_; - if (length && state.latched_ && !sensor->last_pin_val_) { // Long enough low edge - state.latched_ = false; - } else if (length && !state.latched_ && sensor->last_pin_val_) { // Long enough high edge - state.latched_ = true; - set.last_detected_edge_us_ = state.last_intr_; - set.count_++; // NOLINT(clang-diagnostic-deprecated-volatile) + if (length && pulse_state.latched_ && !sensor->last_pin_val_) { // Long enough low edge + pulse_state.latched_ = false; + } else if (length && !pulse_state.latched_ && sensor->last_pin_val_) { // Long enough high edge + pulse_state.latched_ = true; + state.last_detected_edge_us_ = pulse_state.last_intr_; + state.count_++; // NOLINT(clang-diagnostic-deprecated-volatile) } // Due to order of operations this includes // length && latched && rising (just reset from a long low edge) // !latched && (rising || high) (noise on the line resetting the potential rising edge) - set.last_rising_edge_us_ = !state.latched_ && pin_val ? now : set.last_detected_edge_us_; + state.last_rising_edge_us_ = !pulse_state.latched_ && pin_val ? now : state.last_detected_edge_us_; - state.last_intr_ = now; + pulse_state.last_intr_ = now; sensor->last_pin_val_ = pin_val; } diff --git a/esphome/components/pulse_meter/pulse_meter_sensor.h b/esphome/components/pulse_meter/pulse_meter_sensor.h index 5800c4ec42..e46f1e615f 100644 --- a/esphome/components/pulse_meter/pulse_meter_sensor.h +++ b/esphome/components/pulse_meter/pulse_meter_sensor.h @@ -46,17 +46,16 @@ class PulseMeterSensor : public sensor::Sensor, public Component { uint32_t total_pulses_ = 0; uint32_t last_processed_edge_us_ = 0; - // This struct (and the two pointers) are used to pass data between the ISR and loop. - // These two pointers are exchanged each loop. - // Use these to send data from the ISR to the loop not the other way around (except for resetting the values). + // This struct and variable are used to pass data between the ISR and loop. + // The data from state_ is read and then count_ in state_ is reset in each loop. + // This must be done while guarded by an InterruptLock. Use this variable to send data + // from the ISR to the loop not the other way around (except for resetting count_). struct State { uint32_t last_detected_edge_us_ = 0; uint32_t last_rising_edge_us_ = 0; uint32_t count_ = 0; }; - State state_[2]; - volatile State *set_ = state_; - volatile State *get_ = state_ + 1; + volatile State state_{}; // Only use the following variables in the ISR or while guarded by an InterruptLock ISRInternalGPIOPin isr_pin_; From 7fd535179e4cc793047ebcf802b0fe174b3987cb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 12 Feb 2026 11:47:44 -0600 Subject: [PATCH 011/197] [helpers] Add heap warnings to format_hex_pretty, deprecate ethernet/web_server std::string APIs (#13959) --- .../components/ethernet/ethernet_component.h | 2 ++ .../components/web_server_idf/web_server_idf.h | 3 ++- esphome/core/helpers.h | 18 ++++++++++++++++++ 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/esphome/components/ethernet/ethernet_component.h b/esphome/components/ethernet/ethernet_component.h index 5a2869c5a7..b4859c308d 100644 --- a/esphome/components/ethernet/ethernet_component.h +++ b/esphome/components/ethernet/ethernet_component.h @@ -110,6 +110,8 @@ class EthernetComponent : public Component { const char *get_use_address() const; void set_use_address(const char *use_address); void get_eth_mac_address_raw(uint8_t *mac); + // Remove before 2026.9.0 + ESPDEPRECATED("Use get_eth_mac_address_pretty_into_buffer() instead. Removed in 2026.9.0", "2026.3.0") 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(); diff --git a/esphome/components/web_server_idf/web_server_idf.h b/esphome/components/web_server_idf/web_server_idf.h index 12df0303de..74601ffda8 100644 --- a/esphome/components/web_server_idf/web_server_idf.h +++ b/esphome/components/web_server_idf/web_server_idf.h @@ -116,7 +116,8 @@ class AsyncWebServerRequest { /// Write URL (without query string) to buffer, returns StringRef pointing to buffer. /// URL is decoded (e.g., %20 -> space). StringRef url_to(std::span buffer) const; - /// Get URL as std::string. Prefer url_to() to avoid heap allocation. + // Remove before 2026.9.0 + ESPDEPRECATED("Use url_to() instead. Removed in 2026.9.0", "2026.3.0") std::string url() const { char buffer[URL_BUF_SIZE]; return std::string(this->url_to(buffer)); diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index f7de34b6d5..34c7452484 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -1083,6 +1083,9 @@ template std::string format_hex(const std::array &dat * Each byte is displayed as a two-digit uppercase hex value, separated by the specified separator. * Optionally includes the total byte count in parentheses at the end. * + * @warning Allocates heap memory. Use format_hex_pretty_to() with a stack buffer instead. + * Causes heap fragmentation on long-running devices. + * * @param data Pointer to the byte array to format. * @param length Number of bytes in the array. * @param separator Character to use between hex bytes (default: '.'). @@ -1108,6 +1111,9 @@ std::string format_hex_pretty(const uint8_t *data, size_t length, char separator * * Similar to the byte array version, but formats 16-bit words as 4-digit hex values. * + * @warning Allocates heap memory. Use format_hex_pretty_to() with a stack buffer instead. + * Causes heap fragmentation on long-running devices. + * * @param data Pointer to the 16-bit word array to format. * @param length Number of 16-bit words in the array. * @param separator Character to use between hex words (default: '.'). @@ -1131,6 +1137,9 @@ std::string format_hex_pretty(const uint16_t *data, size_t length, char separato * Convenience overload for std::vector. Formats each byte as a two-digit * uppercase hex value with customizable separator. * + * @warning Allocates heap memory. Use format_hex_pretty_to() with a stack buffer instead. + * Causes heap fragmentation on long-running devices. + * * @param data Vector of bytes to format. * @param separator Character to use between hex bytes (default: '.'). * @param show_length Whether to append the byte count in parentheses (default: true). @@ -1154,6 +1163,9 @@ std::string format_hex_pretty(const std::vector &data, char separator = * Convenience overload for std::vector. Each 16-bit word is formatted * as a 4-digit uppercase hex value in big-endian order. * + * @warning Allocates heap memory. Use format_hex_pretty_to() with a stack buffer instead. + * Causes heap fragmentation on long-running devices. + * * @param data Vector of 16-bit words to format. * @param separator Character to use between hex words (default: '.'). * @param show_length Whether to append the word count in parentheses (default: true). @@ -1176,6 +1188,9 @@ std::string format_hex_pretty(const std::vector &data, char separator * Treats each character in the string as a byte and formats it in hex. * Useful for debugging binary data stored in std::string containers. * + * @warning Allocates heap memory. Use format_hex_pretty_to() with a stack buffer instead. + * Causes heap fragmentation on long-running devices. + * * @param data String whose bytes should be formatted as hex. * @param separator Character to use between hex bytes (default: '.'). * @param show_length Whether to append the byte count in parentheses (default: true). @@ -1198,6 +1213,9 @@ std::string format_hex_pretty(const std::string &data, char separator = '.', boo * Converts the integer to big-endian byte order and formats each byte as hex. * The most significant byte appears first in the output string. * + * @warning Allocates heap memory. Use format_hex_pretty_to() with a stack buffer instead. + * Causes heap fragmentation on long-running devices. + * * @tparam T Unsigned integer type (uint8_t, uint16_t, uint32_t, uint64_t, etc.). * @param val The unsigned integer value to format. * @param separator Character to use between hex bytes (default: '.'). From bbc88d92ea0c30efd12d11a27edfe50ea618e299 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 12 Feb 2026 14:31:43 -0600 Subject: [PATCH 012/197] Bump docker/build-push-action from 6.19.1 to 6.19.2 in /.github/actions/build-image (#13965) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/actions/build-image/action.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/actions/build-image/action.yaml b/.github/actions/build-image/action.yaml index 494304eced..38e93c4f17 100644 --- a/.github/actions/build-image/action.yaml +++ b/.github/actions/build-image/action.yaml @@ -47,7 +47,7 @@ runs: - name: Build and push to ghcr by digest id: build-ghcr - uses: docker/build-push-action@601a80b39c9405e50806ae38af30926f9d957c47 # v6.19.1 + uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 env: DOCKER_BUILD_SUMMARY: false DOCKER_BUILD_RECORD_UPLOAD: false @@ -73,7 +73,7 @@ runs: - name: Build and push to dockerhub by digest id: build-dockerhub - uses: docker/build-push-action@601a80b39c9405e50806ae38af30926f9d957c47 # v6.19.1 + uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 env: DOCKER_BUILD_SUMMARY: false DOCKER_BUILD_RECORD_UPLOAD: false From db7870ef5fb3f7fce20f897f7c6279f82695e8c9 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 12 Feb 2026 16:04:39 -0500 Subject: [PATCH 013/197] [alarm_control_panel] Fix flaky integration test race condition (#13964) Co-authored-by: Claude Opus 4.6 --- ...t_alarm_control_panel_state_transitions.py | 25 +++++++++++++++---- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/tests/integration/test_alarm_control_panel_state_transitions.py b/tests/integration/test_alarm_control_panel_state_transitions.py index 09348f5bea..0b07710961 100644 --- a/tests/integration/test_alarm_control_panel_state_transitions.py +++ b/tests/integration/test_alarm_control_panel_state_transitions.py @@ -270,6 +270,14 @@ async def test_alarm_control_panel_state_transitions( # The chime_sensor has chime: true, so opening it while disarmed # should trigger on_chime callback + # Set up future for the on_ready from opening the chime sensor + # (alarm becomes "not ready" when chime sensor opens). + # We must wait for this BEFORE creating the close future, otherwise + # the open event's log can arrive late and resolve the close future, + # causing the test to proceed before the chime close is processed. + ready_after_chime_open: asyncio.Future[bool] = loop.create_future() + ready_futures.append(ready_after_chime_open) + # We're currently DISARMED - open the chime sensor client.switch_command(chime_switch_info.key, True) @@ -279,11 +287,18 @@ async def test_alarm_control_panel_state_transitions( except TimeoutError: pytest.fail(f"on_chime callback not fired. Log lines: {log_lines[-20:]}") - # Close the chime sensor and wait for alarm to become ready again - # We need to wait for this transition before testing door sensor, - # otherwise there's a race where the door sensor state change could - # arrive before the chime sensor state change, leaving the alarm in - # a continuous "not ready" state with no on_ready callback fired. + # Wait for the on_ready from the chime sensor opening + try: + await asyncio.wait_for(ready_after_chime_open, timeout=2.0) + except TimeoutError: + pytest.fail( + f"on_ready callback not fired when chime sensor opened. " + f"Log lines: {log_lines[-20:]}" + ) + + # Now create the future for the close event and close the sensor. + # Since we waited for the open event above, the close event's + # on_ready log cannot be confused with the open event's. ready_after_chime_close: asyncio.Future[bool] = loop.create_future() ready_futures.append(ready_after_chime_close) From 136d17366f5ab2a08912ce2f6a5459eea358ec24 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 12 Feb 2026 16:12:17 -0500 Subject: [PATCH 014/197] [docker] Suppress git detached HEAD advice (#13962) Co-authored-by: Claude Opus 4.6 --- docker/Dockerfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 8ebdd1e49b..540d28be7f 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -9,7 +9,8 @@ FROM ghcr.io/esphome/docker-base:${BUILD_OS}-ha-addon-${BUILD_BASE_VERSION} AS b ARG BUILD_TYPE FROM base-source-${BUILD_TYPE} AS base -RUN git config --system --add safe.directory "*" +RUN git config --system --add safe.directory "*" \ + && git config --system advice.detachedHead false # Install build tools for Python packages that require compilation # (e.g., ruamel.yaml.clibz used by ESP-IDF's idf-component-manager) From 36aba385af3f502462f1a38fb6c334e580ad7155 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 12 Feb 2026 18:20:21 -0600 Subject: [PATCH 015/197] [web_server] Flatten deq_push_back_with_dedup_ to inline vector realloc (#13968) --- esphome/components/web_server/web_server.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index c7a0639382..3acd2d2119 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -198,7 +198,8 @@ EntityMatchResult UrlMatch::match_entity(EntityBase *entity) const { #if !defined(USE_ESP32) && defined(USE_ARDUINO) // helper for allowing only unique entries in the queue -void DeferredUpdateEventSource::deq_push_back_with_dedup_(void *source, message_generator_t *message_generator) { +void __attribute__((flatten)) +DeferredUpdateEventSource::deq_push_back_with_dedup_(void *source, message_generator_t *message_generator) { DeferredEvent item(source, message_generator); // Use range-based for loop instead of std::find_if to reduce template instantiation overhead and binary size From 7dff631dcb133e69a26d841a1f0645cac812202a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 12 Feb 2026 18:20:39 -0600 Subject: [PATCH 016/197] [core] Flatten single-callsite vector realloc functions (#13970) --- esphome/components/api/api_connection.cpp | 6 ++++-- esphome/components/api/api_connection.h | 2 ++ esphome/components/api/api_server.cpp | 2 +- esphome/components/wifi/wifi_component.cpp | 13 +++++++++++++ esphome/components/wifi/wifi_component.h | 13 +------------ 5 files changed, 21 insertions(+), 15 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 4bc3c9b307..4d564af9e2 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1864,6 +1864,8 @@ void APIConnection::on_fatal_error() { this->flags_.remove = true; } +void __attribute__((flatten)) APIConnection::DeferredBatch::push_item(const BatchItem &item) { items.push_back(item); } + void APIConnection::DeferredBatch::add_item(EntityBase *entity, uint8_t message_type, uint8_t estimated_size, uint8_t aux_data_index) { // Check if we already have a message of this type for this entity @@ -1880,7 +1882,7 @@ void APIConnection::DeferredBatch::add_item(EntityBase *entity, uint8_t message_ } } // No existing item found (or event), add new one - items.push_back({entity, message_type, estimated_size, aux_data_index}); + this->push_item({entity, message_type, estimated_size, aux_data_index}); } void APIConnection::DeferredBatch::add_item_front(EntityBase *entity, uint8_t message_type, uint8_t estimated_size) { @@ -1888,7 +1890,7 @@ void APIConnection::DeferredBatch::add_item_front(EntityBase *entity, uint8_t me // This avoids expensive vector::insert which shifts all elements // Note: We only ever have one high-priority message at a time (ping OR disconnect) // If we're disconnecting, pings are blocked, so this simple swap is sufficient - items.push_back({entity, message_type, estimated_size, AUX_DATA_UNUSED}); + this->push_item({entity, message_type, estimated_size, AUX_DATA_UNUSED}); if (items.size() > 1) { // Swap the new high-priority item to the front std::swap(items.front(), items.back()); diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index d3d09a01c8..e34bed8ada 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -541,6 +541,8 @@ class APIConnection final : public APIServerConnectionBase { uint8_t aux_data_index = AUX_DATA_UNUSED); // Add item to the front of the batch (for high priority messages like ping) void add_item_front(EntityBase *entity, uint8_t message_type, uint8_t estimated_size); + // Single push_back site to avoid duplicate _M_realloc_insert instantiation + void push_item(const BatchItem &item); // Clear all items void clear() { diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 5503cf4db8..211bda66de 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -195,7 +195,7 @@ void APIServer::remove_client_(size_t client_index) { #endif } -void APIServer::accept_new_connections_() { +void __attribute__((flatten)) APIServer::accept_new_connections_() { while (true) { struct sockaddr_storage source_addr; socklen_t addr_len = sizeof(source_addr); diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 61d05d7635..a2efac8d26 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -487,6 +487,19 @@ bool WiFiComponent::matches_configured_network_(const char *ssid, const uint8_t return false; } +void __attribute__((flatten)) WiFiComponent::set_sta_priority(bssid_t bssid, int8_t priority) { + for (auto &it : this->sta_priorities_) { + if (it.bssid == bssid) { + it.priority = priority; + return; + } + } + this->sta_priorities_.push_back(WiFiSTAPriority{ + .bssid = bssid, + .priority = priority, + }); +} + void WiFiComponent::log_discarded_scan_result_(const char *ssid, const uint8_t *bssid, int8_t rssi, uint8_t channel) { #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE // Skip logging during roaming scans to avoid log buffer overflow diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 53ff0d9cad..4a038f602c 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -488,18 +488,7 @@ class WiFiComponent : public Component { } return 0; } - void set_sta_priority(const bssid_t bssid, int8_t priority) { - for (auto &it : this->sta_priorities_) { - if (it.bssid == bssid) { - it.priority = priority; - return; - } - } - this->sta_priorities_.push_back(WiFiSTAPriority{ - .bssid = bssid, - .priority = priority, - }); - } + void set_sta_priority(bssid_t bssid, int8_t priority); network::IPAddresses wifi_sta_ip_addresses(); // Remove before 2026.9.0 From e0c03b2dfa39021bcff1805376ce53dde1e8bbdf Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 12 Feb 2026 18:20:58 -0600 Subject: [PATCH 017/197] [api] Fix ESP8266 noise API handshake deadlock and prompt socket cleanup (#13972) --- esphome/components/api/api_frame_helper_noise.cpp | 10 ++++++---- esphome/components/api/api_server.cpp | 8 ++++++-- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/esphome/components/api/api_frame_helper_noise.cpp b/esphome/components/api/api_frame_helper_noise.cpp index c1641b398a..1ae848dead 100644 --- a/esphome/components/api/api_frame_helper_noise.cpp +++ b/esphome/components/api/api_frame_helper_noise.cpp @@ -138,10 +138,12 @@ APIError APINoiseFrameHelper::handle_noise_error_(int err, const LogString *func /// Run through handshake messages (if in that phase) APIError APINoiseFrameHelper::loop() { - // During handshake phase, process as many actions as possible until we can't progress - // socket_->ready() stays true until next main loop, but state_action() will return - // WOULD_BLOCK when no more data is available to read - while (state_ != State::DATA && this->socket_->ready()) { + // Cache ready() outside the loop. On ESP8266 LWIP raw TCP, ready() returns false once + // the rx buffer is consumed. Re-checking each iteration would block handshake writes + // that must follow reads, deadlocking the handshake. state_action() will return + // WOULD_BLOCK when no more data is available to read. + bool socket_ready = this->socket_->ready(); + while (state_ != State::DATA && socket_ready) { APIError err = state_action_(); if (err == APIError::WOULD_BLOCK) { break; diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 211bda66de..f25a9bc0e2 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -148,12 +148,16 @@ void APIServer::loop() { while (client_index < this->clients_.size()) { auto &client = this->clients_[client_index]; + // Common case: process active client + if (!client->flags_.remove) { + client->loop(); + } + // Handle disconnection promptly - close socket to free LWIP PCB + // resources and prevent retransmit crashes on ESP8266. if (client->flags_.remove) { // Rare case: handle disconnection (don't increment - swapped element needs processing) this->remove_client_(client_index); } else { - // Common case: process active client - client->loop(); client_index++; } } From b04e427f01974947856793b0a53425cccfe180bc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 13 Feb 2026 06:39:00 -0600 Subject: [PATCH 018/197] [usb_host] Extract cold path from loop(), replace std::string with buffer API (#13957) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/usb_host/usb_host.h | 1 + .../components/usb_host/usb_host_client.cpp | 123 ++++++++++-------- 2 files changed, 67 insertions(+), 57 deletions(-) diff --git a/esphome/components/usb_host/usb_host.h b/esphome/components/usb_host/usb_host.h index d11a148a0f..d1ec356613 100644 --- a/esphome/components/usb_host/usb_host.h +++ b/esphome/components/usb_host/usb_host.h @@ -148,6 +148,7 @@ class USBClient : public Component { EventPool event_pool; protected: + void handle_open_state_(); TransferRequest *get_trq_(); // Lock-free allocation using atomic bitmask (multi-consumer safe) virtual void disconnect(); virtual void on_connected() {} diff --git a/esphome/components/usb_host/usb_host_client.cpp b/esphome/components/usb_host/usb_host_client.cpp index 09da6e3b73..0612d7a841 100644 --- a/esphome/components/usb_host/usb_host_client.cpp +++ b/esphome/components/usb_host/usb_host_client.cpp @@ -9,6 +9,7 @@ #include #include #include +#include namespace esphome { namespace usb_host { @@ -142,18 +143,23 @@ static void usb_client_print_config_descriptor(const usb_config_desc_t *cfg_desc } while (next_desc != NULL); } #endif -static std::string get_descriptor_string(const usb_str_desc_t *desc) { - char buffer[256]; - if (desc == nullptr) +// USB string descriptors: bLength (uint8_t, max 255) includes the 2-byte header (bLength and bDescriptorType). +// Character count = (bLength - 2) / 2, max 126 chars + null terminator. +static constexpr size_t DESC_STRING_BUF_SIZE = 128; + +static const char *get_descriptor_string(const usb_str_desc_t *desc, std::span buffer) { + if (desc == nullptr || desc->bLength < 2) return "(unspecified)"; - char *p = buffer; - for (int i = 0; i != desc->bLength / 2; i++) { + int char_count = (desc->bLength - 2) / 2; + char *p = buffer.data(); + char *end = p + buffer.size() - 1; + for (int i = 0; i != char_count && p < end; i++) { auto c = desc->wData[i]; if (c < 0x100) *p++ = static_cast(c); } *p = '\0'; - return {buffer}; + return buffer.data(); } // CALLBACK CONTEXT: USB task (called from usb_host_client_handle_events in USB task) @@ -259,60 +265,63 @@ void USBClient::loop() { ESP_LOGW(TAG, "Dropped %u USB events due to queue overflow", dropped); } - switch (this->state_) { - case USB_CLIENT_OPEN: { - int err; - ESP_LOGD(TAG, "Open device %d", this->device_addr_); - err = usb_host_device_open(this->handle_, this->device_addr_, &this->device_handle_); - if (err != ESP_OK) { - ESP_LOGW(TAG, "Device open failed: %s", esp_err_to_name(err)); - this->state_ = USB_CLIENT_INIT; - break; - } - ESP_LOGD(TAG, "Get descriptor device %d", this->device_addr_); - const usb_device_desc_t *desc; - err = usb_host_get_device_descriptor(this->device_handle_, &desc); - if (err != ESP_OK) { - ESP_LOGW(TAG, "Device get_desc failed: %s", esp_err_to_name(err)); - this->disconnect(); - } else { - ESP_LOGD(TAG, "Device descriptor: vid %X pid %X", desc->idVendor, desc->idProduct); - if (desc->idVendor == this->vid_ && desc->idProduct == this->pid_ || this->vid_ == 0 && this->pid_ == 0) { - usb_device_info_t dev_info; - err = usb_host_device_info(this->device_handle_, &dev_info); - if (err != ESP_OK) { - ESP_LOGW(TAG, "Device info failed: %s", esp_err_to_name(err)); - this->disconnect(); - break; - } - this->state_ = USB_CLIENT_CONNECTED; - ESP_LOGD(TAG, "Device connected: Manuf: %s; Prod: %s; Serial: %s", - get_descriptor_string(dev_info.str_desc_manufacturer).c_str(), - get_descriptor_string(dev_info.str_desc_product).c_str(), - get_descriptor_string(dev_info.str_desc_serial_num).c_str()); + if (this->state_ == USB_CLIENT_OPEN) { + this->handle_open_state_(); + } +} + +void USBClient::handle_open_state_() { + int err; + ESP_LOGD(TAG, "Open device %d", this->device_addr_); + err = usb_host_device_open(this->handle_, this->device_addr_, &this->device_handle_); + if (err != ESP_OK) { + ESP_LOGW(TAG, "Device open failed: %s", esp_err_to_name(err)); + this->state_ = USB_CLIENT_INIT; + return; + } + ESP_LOGD(TAG, "Get descriptor device %d", this->device_addr_); + const usb_device_desc_t *desc; + err = usb_host_get_device_descriptor(this->device_handle_, &desc); + if (err != ESP_OK) { + ESP_LOGW(TAG, "Device get_desc failed: %s", esp_err_to_name(err)); + this->disconnect(); + return; + } + ESP_LOGD(TAG, "Device descriptor: vid %X pid %X", desc->idVendor, desc->idProduct); + if (desc->idVendor != this->vid_ || desc->idProduct != this->pid_) { + if (this->vid_ != 0 || this->pid_ != 0) { + ESP_LOGD(TAG, "Not our device, closing"); + this->disconnect(); + return; + } + } + usb_device_info_t dev_info; + err = usb_host_device_info(this->device_handle_, &dev_info); + if (err != ESP_OK) { + ESP_LOGW(TAG, "Device info failed: %s", esp_err_to_name(err)); + this->disconnect(); + return; + } + this->state_ = USB_CLIENT_CONNECTED; + char buf_manuf[DESC_STRING_BUF_SIZE]; + char buf_product[DESC_STRING_BUF_SIZE]; + char buf_serial[DESC_STRING_BUF_SIZE]; + ESP_LOGD(TAG, "Device connected: Manuf: %s; Prod: %s; Serial: %s", + get_descriptor_string(dev_info.str_desc_manufacturer, buf_manuf), + get_descriptor_string(dev_info.str_desc_product, buf_product), + get_descriptor_string(dev_info.str_desc_serial_num, buf_serial)); #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE - const usb_device_desc_t *device_desc; - err = usb_host_get_device_descriptor(this->device_handle_, &device_desc); - if (err == ESP_OK) - usb_client_print_device_descriptor(device_desc); - const usb_config_desc_t *config_desc; - err = usb_host_get_active_config_descriptor(this->device_handle_, &config_desc); - if (err == ESP_OK) - usb_client_print_config_descriptor(config_desc, nullptr); + const usb_device_desc_t *device_desc; + err = usb_host_get_device_descriptor(this->device_handle_, &device_desc); + if (err == ESP_OK) + usb_client_print_device_descriptor(device_desc); + const usb_config_desc_t *config_desc; + err = usb_host_get_active_config_descriptor(this->device_handle_, &config_desc); + if (err == ESP_OK) + usb_client_print_config_descriptor(config_desc, nullptr); #endif - this->on_connected(); - } else { - ESP_LOGD(TAG, "Not our device, closing"); - this->disconnect(); - } - } - break; - } - - default: - break; - } + this->on_connected(); } void USBClient::on_opened(uint8_t addr) { From 903971de12c1b291ca8cd367ce5cdf9af8b304ae Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Fri, 13 Feb 2026 10:25:43 -0600 Subject: [PATCH 019/197] [runtime_image, online_image] Create runtime_image component to decode images (#10212) --- CODEOWNERS | 1 + esphome/components/online_image/__init__.py | 119 +----- ...{image_decoder.cpp => download_buffer.cpp} | 38 +- .../components/online_image/download_buffer.h | 44 ++ .../components/online_image/online_image.cpp | 396 ++++++------------ .../components/online_image/online_image.h | 135 +----- esphome/components/runtime_image/__init__.py | 191 +++++++++ .../bmp_decoder.cpp} | 28 +- .../bmp_decoder.h} | 22 +- .../runtime_image/image_decoder.cpp | 28 ++ .../image_decoder.h | 72 ++-- .../jpeg_decoder.cpp} | 49 ++- .../jpeg_decoder.h} | 17 +- .../png_decoder.cpp} | 25 +- .../png_decoder.h} | 19 +- .../runtime_image/runtime_image.cpp | 300 +++++++++++++ .../components/runtime_image/runtime_image.h | 214 ++++++++++ esphome/core/defines.h | 6 +- 18 files changed, 1075 insertions(+), 629 deletions(-) rename esphome/components/online_image/{image_decoder.cpp => download_buffer.cpp} (52%) create mode 100644 esphome/components/online_image/download_buffer.h create mode 100644 esphome/components/runtime_image/__init__.py rename esphome/components/{online_image/bmp_image.cpp => runtime_image/bmp_decoder.cpp} (82%) rename esphome/components/{online_image/bmp_image.h => runtime_image/bmp_decoder.h} (52%) create mode 100644 esphome/components/runtime_image/image_decoder.cpp rename esphome/components/{online_image => runtime_image}/image_decoder.h (60%) rename esphome/components/{online_image/jpeg_image.cpp => runtime_image/jpeg_decoder.cpp} (69%) rename esphome/components/{online_image/jpeg_image.h => runtime_image/jpeg_decoder.h} (54%) rename esphome/components/{online_image/png_image.cpp => runtime_image/png_decoder.cpp} (82%) rename esphome/components/{online_image/png_image.h => runtime_image/png_decoder.h} (65%) create mode 100644 esphome/components/runtime_image/runtime_image.cpp create mode 100644 esphome/components/runtime_image/runtime_image.h diff --git a/CODEOWNERS b/CODEOWNERS index 25e6dc1b29..2aa0656343 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -411,6 +411,7 @@ esphome/components/rp2040_pwm/* @jesserockz esphome/components/rpi_dpi_rgb/* @clydebarrow esphome/components/rtl87xx/* @kuba2k2 esphome/components/rtttl/* @glmnet +esphome/components/runtime_image/* @clydebarrow @guillempages @kahrendt esphome/components/runtime_stats/* @bdraco esphome/components/rx8130/* @beormund esphome/components/safe_mode/* @jsuanet @kbx81 @paulmonigatti diff --git a/esphome/components/online_image/__init__.py b/esphome/components/online_image/__init__.py index 7a6d25bc7d..057244e03d 100644 --- a/esphome/components/online_image/__init__.py +++ b/esphome/components/online_image/__init__.py @@ -2,97 +2,34 @@ import logging from esphome import automation import esphome.codegen as cg -from esphome.components.const import CONF_BYTE_ORDER, CONF_REQUEST_HEADERS +from esphome.components import runtime_image +from esphome.components.const import CONF_REQUEST_HEADERS from esphome.components.http_request import CONF_HTTP_REQUEST_ID, HttpRequestComponent -from esphome.components.image import ( - CONF_INVERT_ALPHA, - CONF_TRANSPARENCY, - IMAGE_SCHEMA, - Image_, - get_image_type_enum, - get_transparency_enum, - validate_settings, -) import esphome.config_validation as cv from esphome.const import ( CONF_BUFFER_SIZE, - CONF_DITHER, - CONF_FILE, - CONF_FORMAT, CONF_ID, CONF_ON_ERROR, - CONF_RESIZE, CONF_TRIGGER_ID, - CONF_TYPE, CONF_URL, ) from esphome.core import Lambda -AUTO_LOAD = ["image"] +AUTO_LOAD = ["image", "runtime_image"] DEPENDENCIES = ["display", "http_request"] CODEOWNERS = ["@guillempages", "@clydebarrow"] MULTI_CONF = True CONF_ON_DOWNLOAD_FINISHED = "on_download_finished" -CONF_PLACEHOLDER = "placeholder" CONF_UPDATE = "update" _LOGGER = logging.getLogger(__name__) online_image_ns = cg.esphome_ns.namespace("online_image") -ImageFormat = online_image_ns.enum("ImageFormat") - - -class Format: - def __init__(self, image_type): - self.image_type = image_type - - @property - def enum(self): - return getattr(ImageFormat, self.image_type) - - def actions(self): - pass - - -class BMPFormat(Format): - def __init__(self): - super().__init__("BMP") - - def actions(self): - cg.add_define("USE_ONLINE_IMAGE_BMP_SUPPORT") - - -class JPEGFormat(Format): - def __init__(self): - super().__init__("JPEG") - - def actions(self): - cg.add_define("USE_ONLINE_IMAGE_JPEG_SUPPORT") - cg.add_library("JPEGDEC", None, "https://github.com/bitbank2/JPEGDEC#ca1e0f2") - - -class PNGFormat(Format): - def __init__(self): - super().__init__("PNG") - - def actions(self): - cg.add_define("USE_ONLINE_IMAGE_PNG_SUPPORT") - cg.add_library("pngle", "1.1.0") - - -IMAGE_FORMATS = { - x.image_type: x - for x in ( - BMPFormat(), - JPEGFormat(), - PNGFormat(), - ) -} -IMAGE_FORMATS.update({"JPG": IMAGE_FORMATS["JPEG"]}) - -OnlineImage = online_image_ns.class_("OnlineImage", cg.PollingComponent, Image_) +OnlineImage = online_image_ns.class_( + "OnlineImage", cg.PollingComponent, runtime_image.RuntimeImage +) # Actions SetUrlAction = online_image_ns.class_( @@ -111,29 +48,17 @@ DownloadErrorTrigger = online_image_ns.class_( ) -def remove_options(*options): - return { - cv.Optional(option): cv.invalid( - f"{option} is an invalid option for online_image" - ) - for option in options - } - - ONLINE_IMAGE_SCHEMA = ( - IMAGE_SCHEMA.extend(remove_options(CONF_FILE, CONF_INVERT_ALPHA, CONF_DITHER)) + runtime_image.runtime_image_schema(OnlineImage) .extend( { - cv.Required(CONF_ID): cv.declare_id(OnlineImage), - cv.GenerateID(CONF_HTTP_REQUEST_ID): cv.use_id(HttpRequestComponent), # Online Image specific options + cv.GenerateID(CONF_HTTP_REQUEST_ID): cv.use_id(HttpRequestComponent), cv.Required(CONF_URL): cv.url, + cv.Optional(CONF_BUFFER_SIZE, default=65536): cv.int_range(256, 65536), cv.Optional(CONF_REQUEST_HEADERS): cv.All( cv.Schema({cv.string: cv.templatable(cv.string)}) ), - cv.Required(CONF_FORMAT): cv.one_of(*IMAGE_FORMATS, upper=True), - cv.Optional(CONF_PLACEHOLDER): cv.use_id(Image_), - cv.Optional(CONF_BUFFER_SIZE, default=65536): cv.int_range(256, 65536), cv.Optional(CONF_ON_DOWNLOAD_FINISHED): automation.validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( @@ -162,7 +87,7 @@ CONFIG_SCHEMA = cv.Schema( rp2040_arduino=cv.Version(0, 0, 0), host=cv.Version(0, 0, 0), ), - validate_settings, + runtime_image.validate_runtime_image_settings, ) ) @@ -199,23 +124,21 @@ async def online_image_action_to_code(config, action_id, template_arg, args): async def to_code(config): - image_format = IMAGE_FORMATS[config[CONF_FORMAT]] - image_format.actions() + # Use the enhanced helper function to get all runtime image parameters + settings = await runtime_image.process_runtime_image_config(config) url = config[CONF_URL] - width, height = config.get(CONF_RESIZE, (0, 0)) - transparent = get_transparency_enum(config[CONF_TRANSPARENCY]) - var = cg.new_Pvariable( config[CONF_ID], url, - width, - height, - image_format.enum, - get_image_type_enum(config[CONF_TYPE]), - transparent, + settings.width, + settings.height, + settings.format_enum, + settings.image_type_enum, + settings.transparent, + settings.placeholder or cg.nullptr, config[CONF_BUFFER_SIZE], - config.get(CONF_BYTE_ORDER) != "LITTLE_ENDIAN", + settings.byte_order_big_endian, ) await cg.register_component(var, config) await cg.register_parented(var, config[CONF_HTTP_REQUEST_ID]) @@ -227,10 +150,6 @@ async def to_code(config): else: cg.add(var.add_request_header(key, value)) - if placeholder_id := config.get(CONF_PLACEHOLDER): - placeholder = await cg.get_variable(placeholder_id) - cg.add(var.set_placeholder(placeholder)) - for conf in config.get(CONF_ON_DOWNLOAD_FINISHED, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) await automation.build_automation(trigger, [(bool, "cached")], conf) diff --git a/esphome/components/online_image/image_decoder.cpp b/esphome/components/online_image/download_buffer.cpp similarity index 52% rename from esphome/components/online_image/image_decoder.cpp rename to esphome/components/online_image/download_buffer.cpp index 0ab7dadde3..999005df82 100644 --- a/esphome/components/online_image/image_decoder.cpp +++ b/esphome/components/online_image/download_buffer.cpp @@ -1,29 +1,10 @@ -#include "image_decoder.h" -#include "online_image.h" - +#include "download_buffer.h" #include "esphome/core/log.h" +#include -namespace esphome { -namespace online_image { +namespace esphome::online_image { -static const char *const TAG = "online_image.decoder"; - -bool ImageDecoder::set_size(int width, int height) { - bool success = this->image_->resize_(width, height) > 0; - this->x_scale_ = static_cast(this->image_->buffer_width_) / width; - this->y_scale_ = static_cast(this->image_->buffer_height_) / height; - return success; -} - -void ImageDecoder::draw(int x, int y, int w, int h, const Color &color) { - auto width = std::min(this->image_->buffer_width_, static_cast(std::ceil((x + w) * this->x_scale_))); - auto height = std::min(this->image_->buffer_height_, static_cast(std::ceil((y + h) * this->y_scale_))); - for (int i = x * this->x_scale_; i < width; i++) { - for (int j = y * this->y_scale_; j < height; j++) { - this->image_->draw_pixel_(i, j, color); - } - } -} +static const char *const TAG = "online_image.download_buffer"; DownloadBuffer::DownloadBuffer(size_t size) : size_(size) { this->buffer_ = this->allocator_.allocate(size); @@ -43,10 +24,12 @@ uint8_t *DownloadBuffer::data(size_t offset) { } size_t DownloadBuffer::read(size_t len) { - this->unread_ -= len; - if (this->unread_ > 0) { - memmove(this->data(), this->data(len), this->unread_); + if (len >= this->unread_) { + this->unread_ = 0; + return 0; } + this->unread_ -= len; + memmove(this->data(), this->data(len), this->unread_); return this->unread_; } @@ -69,5 +52,4 @@ size_t DownloadBuffer::resize(size_t size) { } } -} // namespace online_image -} // namespace esphome +} // namespace esphome::online_image diff --git a/esphome/components/online_image/download_buffer.h b/esphome/components/online_image/download_buffer.h new file mode 100644 index 0000000000..110a4b608a --- /dev/null +++ b/esphome/components/online_image/download_buffer.h @@ -0,0 +1,44 @@ +#pragma once + +#include "esphome/core/helpers.h" +#include +#include + +namespace esphome::online_image { + +/** + * @brief Buffer for managing downloaded data. + * + * This class provides a buffer for downloading data with tracking of + * unread bytes and dynamic resizing capabilities. + */ +class DownloadBuffer { + public: + DownloadBuffer(size_t size); + ~DownloadBuffer() { this->allocator_.deallocate(this->buffer_, this->size_); } + + uint8_t *data(size_t offset = 0); + uint8_t *append() { return this->data(this->unread_); } + + size_t unread() const { return this->unread_; } + size_t size() const { return this->size_; } + size_t free_capacity() const { return this->size_ - this->unread_; } + + size_t read(size_t len); + size_t write(size_t len) { + this->unread_ += len; + return this->unread_; + } + + void reset() { this->unread_ = 0; } + size_t resize(size_t size); + + protected: + RAMAllocator allocator_{}; + uint8_t *buffer_; + size_t size_; + /** Total number of downloaded bytes not yet read. */ + size_t unread_; +}; + +} // namespace esphome::online_image diff --git a/esphome/components/online_image/online_image.cpp b/esphome/components/online_image/online_image.cpp index 4e2ecc2c77..6f5b82116d 100644 --- a/esphome/components/online_image/online_image.cpp +++ b/esphome/components/online_image/online_image.cpp @@ -1,6 +1,6 @@ #include "online_image.h" - #include "esphome/core/log.h" +#include static const char *const TAG = "online_image"; static const char *const ETAG_HEADER_NAME = "etag"; @@ -8,142 +8,82 @@ static const char *const IF_NONE_MATCH_HEADER_NAME = "if-none-match"; static const char *const LAST_MODIFIED_HEADER_NAME = "last-modified"; static const char *const IF_MODIFIED_SINCE_HEADER_NAME = "if-modified-since"; -#include "image_decoder.h" +namespace esphome::online_image { -#ifdef USE_ONLINE_IMAGE_BMP_SUPPORT -#include "bmp_image.h" -#endif -#ifdef USE_ONLINE_IMAGE_JPEG_SUPPORT -#include "jpeg_image.h" -#endif -#ifdef USE_ONLINE_IMAGE_PNG_SUPPORT -#include "png_image.h" -#endif - -namespace esphome { -namespace online_image { - -using image::ImageType; - -inline bool is_color_on(const Color &color) { - // This produces the most accurate monochrome conversion, but is slightly slower. - // return (0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b) > 127; - - // Approximation using fast integer computations; produces acceptable results - // Equivalent to 0.25 * R + 0.5 * G + 0.25 * B - return ((color.r >> 2) + (color.g >> 1) + (color.b >> 2)) & 0x80; -} - -OnlineImage::OnlineImage(const std::string &url, int width, int height, ImageFormat format, ImageType type, - image::Transparency transparency, uint32_t download_buffer_size, bool is_big_endian) - : Image(nullptr, 0, 0, type, transparency), - buffer_(nullptr), - download_buffer_(download_buffer_size), - download_buffer_initial_size_(download_buffer_size), - format_(format), - fixed_width_(width), - fixed_height_(height), - is_big_endian_(is_big_endian) { +OnlineImage::OnlineImage(const std::string &url, int width, int height, runtime_image::ImageFormat format, + image::ImageType type, image::Transparency transparency, image::Image *placeholder, + uint32_t buffer_size, bool is_big_endian) + : RuntimeImage(format, type, transparency, placeholder, is_big_endian, width, height), + download_buffer_(buffer_size), + download_buffer_initial_size_(buffer_size) { this->set_url(url); } -void OnlineImage::draw(int x, int y, display::Display *display, Color color_on, Color color_off) { - if (this->data_start_) { - Image::draw(x, y, display, color_on, color_off); - } else if (this->placeholder_) { - this->placeholder_->draw(x, y, display, color_on, color_off); +bool OnlineImage::validate_url_(const std::string &url) { + if (url.empty()) { + ESP_LOGE(TAG, "URL is empty"); + return false; } -} - -void OnlineImage::release() { - if (this->buffer_) { - ESP_LOGV(TAG, "Deallocating old buffer"); - this->allocator_.deallocate(this->buffer_, this->get_buffer_size_()); - this->data_start_ = nullptr; - this->buffer_ = nullptr; - this->width_ = 0; - this->height_ = 0; - this->buffer_width_ = 0; - this->buffer_height_ = 0; - this->last_modified_ = ""; - this->etag_ = ""; - this->end_connection_(); + if (url.length() > 2048) { + ESP_LOGE(TAG, "URL is too long"); + return false; } -} - -size_t OnlineImage::resize_(int width_in, int height_in) { - int width = this->fixed_width_; - int height = this->fixed_height_; - if (this->is_auto_resize_()) { - width = width_in; - height = height_in; - if (this->width_ != width && this->height_ != height) { - this->release(); - } + if (url.compare(0, 7, "http://") != 0 && url.compare(0, 8, "https://") != 0) { + ESP_LOGE(TAG, "URL must start with http:// or https://"); + return false; } - size_t new_size = this->get_buffer_size_(width, height); - if (this->buffer_) { - // Buffer already allocated => no need to resize - return new_size; - } - ESP_LOGD(TAG, "Allocating new buffer of %zu bytes", new_size); - this->buffer_ = this->allocator_.allocate(new_size); - if (this->buffer_ == nullptr) { - ESP_LOGE(TAG, "allocation of %zu bytes failed. Biggest block in heap: %zu Bytes", new_size, - this->allocator_.get_max_free_block_size()); - this->end_connection_(); - return 0; - } - this->buffer_width_ = width; - this->buffer_height_ = height; - this->width_ = width; - ESP_LOGV(TAG, "New size: (%d, %d)", width, height); - return new_size; + return true; } void OnlineImage::update() { - if (this->decoder_) { + if (this->is_decoding()) { ESP_LOGW(TAG, "Image already being updated."); return; } - ESP_LOGI(TAG, "Updating image %s", this->url_.c_str()); - std::list headers = {}; - - http_request::Header accept_header; - accept_header.name = "Accept"; - std::string accept_mime_type; - switch (this->format_) { -#ifdef USE_ONLINE_IMAGE_BMP_SUPPORT - case ImageFormat::BMP: - accept_mime_type = "image/bmp"; - break; -#endif // USE_ONLINE_IMAGE_BMP_SUPPORT -#ifdef USE_ONLINE_IMAGE_JPEG_SUPPORT - case ImageFormat::JPEG: - accept_mime_type = "image/jpeg"; - break; -#endif // USE_ONLINE_IMAGE_JPEG_SUPPORT -#ifdef USE_ONLINE_IMAGE_PNG_SUPPORT - case ImageFormat::PNG: - accept_mime_type = "image/png"; - break; -#endif // USE_ONLINE_IMAGE_PNG_SUPPORT - default: - accept_mime_type = "image/*"; + if (!this->validate_url_(this->url_)) { + ESP_LOGE(TAG, "Invalid URL: %s", this->url_.c_str()); + this->download_error_callback_.call(); + return; } - accept_header.value = accept_mime_type + ",*/*;q=0.8"; + ESP_LOGD(TAG, "Updating image from %s", this->url_.c_str()); + + std::list headers; + + // Add caching headers if we have them if (!this->etag_.empty()) { - headers.push_back(http_request::Header{IF_NONE_MATCH_HEADER_NAME, this->etag_}); + headers.push_back({IF_NONE_MATCH_HEADER_NAME, this->etag_}); } - if (!this->last_modified_.empty()) { - headers.push_back(http_request::Header{IF_MODIFIED_SINCE_HEADER_NAME, this->last_modified_}); + headers.push_back({IF_MODIFIED_SINCE_HEADER_NAME, this->last_modified_}); } - headers.push_back(accept_header); + // Add Accept header based on image format + const char *accept_mime_type; + switch (this->get_format()) { +#ifdef USE_RUNTIME_IMAGE_BMP + case runtime_image::BMP: + accept_mime_type = "image/bmp,*/*;q=0.8"; + break; +#endif +#ifdef USE_RUNTIME_IMAGE_JPEG + case runtime_image::JPEG: + accept_mime_type = "image/jpeg,*/*;q=0.8"; + break; +#endif +#ifdef USE_RUNTIME_IMAGE_PNG + case runtime_image::PNG: + accept_mime_type = "image/png,*/*;q=0.8"; + break; +#endif + default: + accept_mime_type = "image/*,*/*;q=0.8"; + break; + } + headers.push_back({"Accept", accept_mime_type}); + // User headers last so they can override any of the above for (auto &header : this->request_headers_) { headers.push_back(http_request::Header{header.first, header.second.value()}); } @@ -175,186 +115,117 @@ void OnlineImage::update() { ESP_LOGD(TAG, "Starting download"); size_t total_size = this->downloader_->content_length; -#ifdef USE_ONLINE_IMAGE_BMP_SUPPORT - if (this->format_ == ImageFormat::BMP) { - ESP_LOGD(TAG, "Allocating BMP decoder"); - this->decoder_ = make_unique(this); - this->enable_loop(); + // Initialize decoder with the known format + if (!this->begin_decode(total_size)) { + ESP_LOGE(TAG, "Failed to initialize decoder for format %d", this->get_format()); + this->end_connection_(); + this->download_error_callback_.call(); + return; } -#endif // USE_ONLINE_IMAGE_BMP_SUPPORT -#ifdef USE_ONLINE_IMAGE_JPEG_SUPPORT - if (this->format_ == ImageFormat::JPEG) { - ESP_LOGD(TAG, "Allocating JPEG decoder"); - this->decoder_ = esphome::make_unique(this); - this->enable_loop(); - } -#endif // USE_ONLINE_IMAGE_JPEG_SUPPORT -#ifdef USE_ONLINE_IMAGE_PNG_SUPPORT - if (this->format_ == ImageFormat::PNG) { - ESP_LOGD(TAG, "Allocating PNG decoder"); - this->decoder_ = make_unique(this); - this->enable_loop(); - } -#endif // USE_ONLINE_IMAGE_PNG_SUPPORT - if (!this->decoder_) { - ESP_LOGE(TAG, "Could not instantiate decoder. Image format unsupported: %d", this->format_); - this->end_connection_(); - this->download_error_callback_.call(); - return; - } - auto prepare_result = this->decoder_->prepare(total_size); - if (prepare_result < 0) { - this->end_connection_(); - this->download_error_callback_.call(); - return; + // JPEG requires the complete image in the download buffer before decoding + if (this->get_format() == runtime_image::JPEG && total_size > this->download_buffer_.size()) { + this->download_buffer_.resize(total_size); } + ESP_LOGI(TAG, "Downloading image (Size: %zu)", total_size); this->start_time_ = ::time(nullptr); + this->enable_loop(); } void OnlineImage::loop() { - if (!this->decoder_) { + if (!this->is_decoding()) { // Not decoding at the moment => nothing to do. this->disable_loop(); return; } - if (!this->downloader_ || this->decoder_->is_finished()) { - this->data_start_ = buffer_; - this->width_ = buffer_width_; - this->height_ = buffer_height_; - ESP_LOGD(TAG, "Image fully downloaded, read %zu bytes, width/height = %d/%d", this->downloader_->get_bytes_read(), - this->width_, this->height_); - ESP_LOGD(TAG, "Total time: %" PRIu32 "s", (uint32_t) (::time(nullptr) - this->start_time_)); + + if (!this->downloader_) { + ESP_LOGE(TAG, "Downloader not instantiated; cannot download"); + this->end_connection_(); + this->download_error_callback_.call(); + return; + } + + // Check if download is complete — use decoder's format-specific completion check + // to handle both known content-length and chunked transfer encoding + if (this->is_decode_finished() || (this->downloader_->content_length > 0 && + this->downloader_->get_bytes_read() >= this->downloader_->content_length && + this->download_buffer_.unread() == 0)) { + // Finalize decoding + this->end_decode(); + + ESP_LOGD(TAG, "Image fully downloaded, %zu bytes in %" PRIu32 "s", this->downloader_->get_bytes_read(), + (uint32_t) (::time(nullptr) - this->start_time_)); + + // Save caching headers this->etag_ = this->downloader_->get_response_header(ETAG_HEADER_NAME); this->last_modified_ = this->downloader_->get_response_header(LAST_MODIFIED_HEADER_NAME); + this->download_finished_callback_.call(false); this->end_connection_(); return; } - if (this->downloader_ == nullptr) { - ESP_LOGE(TAG, "Downloader not instantiated; cannot download"); - return; - } + + // Download and decode more data size_t available = this->download_buffer_.free_capacity(); - if (available) { - // Some decoders need to fully download the image before downloading. - // In case of huge images, don't wait blocking until the whole image has been downloaded, - // use smaller chunks + if (available > 0) { + // Download in chunks to avoid blocking available = std::min(available, this->download_buffer_initial_size_); auto len = this->downloader_->read(this->download_buffer_.append(), available); + if (len > 0) { this->download_buffer_.write(len); - auto fed = this->decoder_->decode(this->download_buffer_.data(), this->download_buffer_.unread()); - if (fed < 0) { - ESP_LOGE(TAG, "Error when decoding image."); + + // Feed data to decoder + auto consumed = this->feed_data(this->download_buffer_.data(), this->download_buffer_.unread()); + + if (consumed < 0) { + ESP_LOGE(TAG, "Error decoding image: %d", consumed); this->end_connection_(); this->download_error_callback_.call(); return; } - this->download_buffer_.read(fed); - } - } -} -void OnlineImage::map_chroma_key(Color &color) { - if (this->transparency_ == image::TRANSPARENCY_CHROMA_KEY) { - if (color.g == 1 && color.r == 0 && color.b == 0) { - color.g = 0; - } - if (color.w < 0x80) { - color.r = 0; - color.g = this->type_ == ImageType::IMAGE_TYPE_RGB565 ? 4 : 1; - color.b = 0; - } - } -} - -void OnlineImage::draw_pixel_(int x, int y, Color color) { - if (!this->buffer_) { - ESP_LOGE(TAG, "Buffer not allocated!"); - return; - } - if (x < 0 || y < 0 || x >= this->buffer_width_ || y >= this->buffer_height_) { - ESP_LOGE(TAG, "Tried to paint a pixel (%d,%d) outside the image!", x, y); - return; - } - uint32_t pos = this->get_position_(x, y); - switch (this->type_) { - case ImageType::IMAGE_TYPE_BINARY: { - const uint32_t width_8 = ((this->width_ + 7u) / 8u) * 8u; - pos = x + y * width_8; - auto bitno = 0x80 >> (pos % 8u); - pos /= 8u; - auto on = is_color_on(color); - if (this->has_transparency() && color.w < 0x80) - on = false; - if (on) { - this->buffer_[pos] |= bitno; - } else { - this->buffer_[pos] &= ~bitno; + if (consumed > 0) { + this->download_buffer_.read(consumed); } - break; + } else if (len < 0) { + ESP_LOGE(TAG, "Error downloading image: %d", len); + this->end_connection_(); + this->download_error_callback_.call(); + return; } - case ImageType::IMAGE_TYPE_GRAYSCALE: { - auto gray = static_cast(0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b); - if (this->transparency_ == image::TRANSPARENCY_CHROMA_KEY) { - if (gray == 1) { - gray = 0; - } - if (color.w < 0x80) { - gray = 1; - } - } else if (this->transparency_ == image::TRANSPARENCY_ALPHA_CHANNEL) { - if (color.w != 0xFF) - gray = color.w; - } - this->buffer_[pos] = gray; - break; - } - case ImageType::IMAGE_TYPE_RGB565: { - this->map_chroma_key(color); - uint16_t col565 = display::ColorUtil::color_to_565(color); - if (this->is_big_endian_) { - this->buffer_[pos + 0] = static_cast((col565 >> 8) & 0xFF); - this->buffer_[pos + 1] = static_cast(col565 & 0xFF); - } else { - this->buffer_[pos + 0] = static_cast(col565 & 0xFF); - this->buffer_[pos + 1] = static_cast((col565 >> 8) & 0xFF); - } - if (this->transparency_ == image::TRANSPARENCY_ALPHA_CHANNEL) { - this->buffer_[pos + 2] = color.w; - } - break; - } - case ImageType::IMAGE_TYPE_RGB: { - this->map_chroma_key(color); - this->buffer_[pos + 0] = color.r; - this->buffer_[pos + 1] = color.g; - this->buffer_[pos + 2] = color.b; - if (this->transparency_ == image::TRANSPARENCY_ALPHA_CHANNEL) { - this->buffer_[pos + 3] = color.w; - } - break; + } else { + // Buffer is full, need to decode some data first + auto consumed = this->feed_data(this->download_buffer_.data(), this->download_buffer_.unread()); + if (consumed > 0) { + this->download_buffer_.read(consumed); + } else if (consumed < 0) { + ESP_LOGE(TAG, "Decode error with full buffer: %d", consumed); + this->end_connection_(); + this->download_error_callback_.call(); + return; + } else { + // Decoder can't process more data, might need complete image + // This is normal for JPEG which needs complete data + ESP_LOGV(TAG, "Decoder waiting for more data"); } } } void OnlineImage::end_connection_() { + // Abort any in-progress decode to free decoder resources. + // Use RuntimeImage::release() directly to avoid recursion with OnlineImage::release(). + if (this->is_decoding()) { + RuntimeImage::release(); + } if (this->downloader_) { this->downloader_->end(); this->downloader_ = nullptr; } - this->decoder_.reset(); this->download_buffer_.reset(); -} - -bool OnlineImage::validate_url_(const std::string &url) { - if ((url.length() < 8) || !url.starts_with("http") || (url.find("://") == std::string::npos)) { - ESP_LOGE(TAG, "URL is invalid and/or must be prefixed with 'http://' or 'https://'"); - return false; - } - return true; + this->disable_loop(); } void OnlineImage::add_on_finished_callback(std::function &&callback) { @@ -365,5 +236,16 @@ void OnlineImage::add_on_error_callback(std::function &&callback) { this->download_error_callback_.add(std::move(callback)); } -} // namespace online_image -} // namespace esphome +void OnlineImage::release() { + // Clear cache headers + this->etag_ = ""; + this->last_modified_ = ""; + + // End any active connection + this->end_connection_(); + + // Call parent's release to free the image buffer + RuntimeImage::release(); +} + +} // namespace esphome::online_image diff --git a/esphome/components/online_image/online_image.h b/esphome/components/online_image/online_image.h index 12d409ca29..c7c80c7c66 100644 --- a/esphome/components/online_image/online_image.h +++ b/esphome/components/online_image/online_image.h @@ -1,15 +1,14 @@ #pragma once +#include "download_buffer.h" #include "esphome/components/http_request/http_request.h" -#include "esphome/components/image/image.h" +#include "esphome/components/runtime_image/runtime_image.h" +#include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/defines.h" #include "esphome/core/helpers.h" -#include "image_decoder.h" - -namespace esphome { -namespace online_image { +namespace esphome::online_image { using t_http_codes = enum { HTTP_CODE_OK = 200, @@ -17,27 +16,13 @@ using t_http_codes = enum { HTTP_CODE_NOT_FOUND = 404, }; -/** - * @brief Format that the image is encoded with. - */ -enum ImageFormat { - /** Automatically detect from MIME type. Not supported yet. */ - AUTO, - /** JPEG format. */ - JPEG, - /** PNG format. */ - PNG, - /** BMP format. */ - BMP, -}; - /** * @brief Download an image from a given URL, and decode it using the specified decoder. * The image will then be stored in a buffer, so that it can be re-displayed without the * need to re-download or re-decode. */ class OnlineImage : public PollingComponent, - public image::Image, + public runtime_image::RuntimeImage, public Parented { public: /** @@ -46,17 +31,19 @@ class OnlineImage : public PollingComponent, * @param url URL to download the image from. * @param width Desired width of the target image area. * @param height Desired height of the target image area. - * @param format Format that the image is encoded in (@see ImageFormat). + * @param format Format that the image is encoded in (@see runtime_image::ImageFormat). + * @param type The pixel format for the image. + * @param transparency The transparency type for the image. + * @param placeholder Optional placeholder image to show while loading. * @param buffer_size Size of the buffer used to download the image. + * @param is_big_endian Whether the image is stored in big-endian format. */ - OnlineImage(const std::string &url, int width, int height, ImageFormat format, image::ImageType type, - image::Transparency transparency, uint32_t buffer_size, bool is_big_endian); - - void draw(int x, int y, display::Display *display, Color color_on, Color color_off) override; + OnlineImage(const std::string &url, int width, int height, runtime_image::ImageFormat format, image::ImageType type, + image::Transparency transparency, image::Image *placeholder, uint32_t buffer_size, + bool is_big_endian = false); void update() override; void loop() override; - void map_chroma_key(Color &color); /** Set the URL to download the image from. */ void set_url(const std::string &url) { @@ -69,82 +56,26 @@ class OnlineImage : public PollingComponent, /** Add the request header */ template void add_request_header(const std::string &header, V value) { - this->request_headers_.push_back(std::pair >(header, value)); + this->request_headers_.push_back(std::pair>(header, value)); } - /** - * @brief Set the image that needs to be shown as long as the downloaded image - * is not available. - * - * @param placeholder Pointer to the (@link Image) to show as placeholder. - */ - void set_placeholder(image::Image *placeholder) { this->placeholder_ = placeholder; } - /** * Release the buffer storing the image. The image will need to be downloaded again * to be able to be displayed. */ void release(); - /** - * Resize the download buffer - * - * @param size The new size for the download buffer. - */ - size_t resize_download_buffer(size_t size) { return this->download_buffer_.resize(size); } - void add_on_finished_callback(std::function &&callback); void add_on_error_callback(std::function &&callback); protected: bool validate_url_(const std::string &url); - - RAMAllocator allocator_{}; - - uint32_t get_buffer_size_() const { return get_buffer_size_(this->buffer_width_, this->buffer_height_); } - int get_buffer_size_(int width, int height) const { return (this->get_bpp() * width + 7u) / 8u * height; } - - int get_position_(int x, int y) const { return (x + y * this->buffer_width_) * this->get_bpp() / 8; } - - ESPHOME_ALWAYS_INLINE bool is_auto_resize_() const { return this->fixed_width_ == 0 || this->fixed_height_ == 0; } - - /** - * @brief Resize the image buffer to the requested dimensions. - * - * The buffer will be allocated if not existing. - * If the dimensions have been fixed in the yaml config, the buffer will be created - * with those dimensions and not resized, even on request. - * Otherwise, the old buffer will be deallocated and a new buffer with the requested - * allocated - * - * @param width - * @param height - * @return 0 if no memory could be allocated, the size of the new buffer otherwise. - */ - size_t resize_(int width, int height); - - /** - * @brief Draw a pixel into the buffer. - * - * This is used by the decoder to fill the buffer that will later be displayed - * by the `draw` method. This will internally convert the supplied 32 bit RGBA - * color into the requested image storage format. - * - * @param x Horizontal pixel position. - * @param y Vertical pixel position. - * @param color 32 bit color to put into the pixel. - */ - void draw_pixel_(int x, int y, Color color); - void end_connection_(); CallbackManager download_finished_callback_{}; CallbackManager download_error_callback_{}; std::shared_ptr downloader_{nullptr}; - std::unique_ptr decoder_{nullptr}; - - uint8_t *buffer_; DownloadBuffer download_buffer_; /** * This is the *initial* size of the download buffer, not the current size. @@ -153,40 +84,10 @@ class OnlineImage : public PollingComponent, */ size_t download_buffer_initial_size_; - const ImageFormat format_; - image::Image *placeholder_{nullptr}; - std::string url_{""}; - std::vector > > request_headers_; + std::vector>> request_headers_; - /** width requested on configuration, or 0 if non specified. */ - const int fixed_width_; - /** height requested on configuration, or 0 if non specified. */ - const int fixed_height_; - /** - * Whether the image is stored in big-endian format. - * This is used to determine how to store 16 bit colors in the buffer. - */ - bool is_big_endian_; - /** - * Actual width of the current image. If fixed_width_ is specified, - * this will be equal to it; otherwise it will be set once the decoding - * starts and the original size is known. - * This needs to be separate from "BaseImage::get_width()" because the latter - * must return 0 until the image has been decoded (to avoid showing partially - * decoded images). - */ - int buffer_width_; - /** - * Actual height of the current image. If fixed_height_ is specified, - * this will be equal to it; otherwise it will be set once the decoding - * starts and the original size is known. - * This needs to be separate from "BaseImage::get_height()" because the latter - * must return 0 until the image has been decoded (to avoid showing partially - * decoded images). - */ - int buffer_height_; /** * The value of the ETag HTTP header provided in the last response. */ @@ -197,9 +98,6 @@ class OnlineImage : public PollingComponent, std::string last_modified_ = ""; time_t start_time_; - - friend bool ImageDecoder::set_size(int width, int height); - friend void ImageDecoder::draw(int x, int y, int w, int h, const Color &color); }; template class OnlineImageSetUrlAction : public Action { @@ -241,5 +139,4 @@ class DownloadErrorTrigger : public Trigger<> { } }; -} // namespace online_image -} // namespace esphome +} // namespace esphome::online_image diff --git a/esphome/components/runtime_image/__init__.py b/esphome/components/runtime_image/__init__.py new file mode 100644 index 0000000000..0773a53d91 --- /dev/null +++ b/esphome/components/runtime_image/__init__.py @@ -0,0 +1,191 @@ +from dataclasses import dataclass + +import esphome.codegen as cg +from esphome.components.const import CONF_BYTE_ORDER +from esphome.components.image import ( + IMAGE_TYPE, + Image_, + validate_settings, + validate_transparency, + validate_type, +) +import esphome.config_validation as cv +from esphome.const import CONF_FORMAT, CONF_ID, CONF_RESIZE, CONF_TYPE + +AUTO_LOAD = ["image"] +CODEOWNERS = ["@guillempages", "@clydebarrow", "@kahrendt"] + +CONF_PLACEHOLDER = "placeholder" +CONF_TRANSPARENCY = "transparency" + +runtime_image_ns = cg.esphome_ns.namespace("runtime_image") + +# Base decoder classes +ImageDecoder = runtime_image_ns.class_("ImageDecoder") +BmpDecoder = runtime_image_ns.class_("BmpDecoder", ImageDecoder) +JpegDecoder = runtime_image_ns.class_("JpegDecoder", ImageDecoder) +PngDecoder = runtime_image_ns.class_("PngDecoder", ImageDecoder) + +# Runtime image class +RuntimeImage = runtime_image_ns.class_( + "RuntimeImage", cg.esphome_ns.namespace("image").class_("Image") +) + +# Image format enum +ImageFormat = runtime_image_ns.enum("ImageFormat") +IMAGE_FORMAT_AUTO = ImageFormat.AUTO +IMAGE_FORMAT_JPEG = ImageFormat.JPEG +IMAGE_FORMAT_PNG = ImageFormat.PNG +IMAGE_FORMAT_BMP = ImageFormat.BMP + +# Export enum for decode errors +DecodeError = runtime_image_ns.enum("DecodeError") +DECODE_ERROR_INVALID_TYPE = DecodeError.DECODE_ERROR_INVALID_TYPE +DECODE_ERROR_UNSUPPORTED_FORMAT = DecodeError.DECODE_ERROR_UNSUPPORTED_FORMAT +DECODE_ERROR_OUT_OF_MEMORY = DecodeError.DECODE_ERROR_OUT_OF_MEMORY + + +class Format: + """Base class for image format definitions.""" + + def __init__(self, name: str, decoder_class: cg.MockObjClass) -> None: + self.name = name + self.decoder_class = decoder_class + + def actions(self) -> None: + """Add defines and libraries needed for this format.""" + + +class BMPFormat(Format): + """BMP format decoder configuration.""" + + def __init__(self): + super().__init__("BMP", BmpDecoder) + + def actions(self) -> None: + cg.add_define("USE_RUNTIME_IMAGE_BMP") + + +class JPEGFormat(Format): + """JPEG format decoder configuration.""" + + def __init__(self): + super().__init__("JPEG", JpegDecoder) + + def actions(self) -> None: + cg.add_define("USE_RUNTIME_IMAGE_JPEG") + cg.add_library("JPEGDEC", None, "https://github.com/bitbank2/JPEGDEC#ca1e0f2") + + +class PNGFormat(Format): + """PNG format decoder configuration.""" + + def __init__(self): + super().__init__("PNG", PngDecoder) + + def actions(self) -> None: + cg.add_define("USE_RUNTIME_IMAGE_PNG") + cg.add_library("pngle", "1.1.0") + + +# Registry of available formats +IMAGE_FORMATS = { + "BMP": BMPFormat(), + "JPEG": JPEGFormat(), + "PNG": PNGFormat(), + "JPG": JPEGFormat(), # Alias for JPEG +} + + +def get_format(format_name: str) -> Format | None: + """Get a format instance by name.""" + return IMAGE_FORMATS.get(format_name.upper()) + + +def enable_format(format_name: str) -> Format | None: + """Enable a specific image format by adding its defines and libraries.""" + format_obj = get_format(format_name) + if format_obj: + format_obj.actions() + return format_obj + return None + + +# Runtime image configuration schema base - to be extended by components +def runtime_image_schema(image_class: cg.MockObjClass = RuntimeImage) -> cv.Schema: + """Create a runtime image schema with the specified image class.""" + return cv.Schema( + { + cv.Required(CONF_ID): cv.declare_id(image_class), + cv.Required(CONF_FORMAT): cv.one_of(*IMAGE_FORMATS, upper=True), + cv.Optional(CONF_RESIZE): cv.dimensions, + cv.Required(CONF_TYPE): validate_type(IMAGE_TYPE), + cv.Optional(CONF_BYTE_ORDER): cv.one_of( + "BIG_ENDIAN", "LITTLE_ENDIAN", upper=True + ), + cv.Optional(CONF_TRANSPARENCY, default="OPAQUE"): validate_transparency(), + cv.Optional(CONF_PLACEHOLDER): cv.use_id(Image_), + } + ) + + +def validate_runtime_image_settings(config: dict) -> dict: + """Apply validate_settings from image component to runtime image config.""" + return validate_settings(config) + + +@dataclass +class RuntimeImageSettings: + """Processed runtime image configuration parameters.""" + + width: int + height: int + format_enum: cg.MockObj + image_type_enum: cg.MockObj + transparent: cg.MockObj + byte_order_big_endian: bool + placeholder: cg.MockObj | None + + +async def process_runtime_image_config(config: dict) -> RuntimeImageSettings: + """ + Helper function to process common runtime image configuration parameters. + Handles format enabling and returns all necessary enums and parameters. + """ + from esphome.components.image import get_image_type_enum, get_transparency_enum + + # Get resize dimensions with default (0, 0) + width, height = config.get(CONF_RESIZE, (0, 0)) + + # Handle format (required for runtime images) + format_name = config[CONF_FORMAT] + # Enable the format in the runtime_image component + enable_format(format_name) + # Map format names to enum values (handle JPG as alias for JPEG) + if format_name.upper() == "JPG": + format_name = "JPEG" + format_enum = getattr(ImageFormat, format_name.upper()) + + # Get image type enum + image_type_enum = get_image_type_enum(config[CONF_TYPE]) + + # Get transparency enum + transparent = get_transparency_enum(config.get(CONF_TRANSPARENCY, "OPAQUE")) + + # Get byte order (True for big endian, False for little endian) + byte_order_big_endian = config.get(CONF_BYTE_ORDER) != "LITTLE_ENDIAN" + + # Get placeholder if specified + placeholder = None + if placeholder_id := config.get(CONF_PLACEHOLDER): + placeholder = await cg.get_variable(placeholder_id) + + return RuntimeImageSettings( + width=width, + height=height, + format_enum=format_enum, + image_type_enum=image_type_enum, + transparent=transparent, + byte_order_big_endian=byte_order_big_endian, + placeholder=placeholder, + ) diff --git a/esphome/components/online_image/bmp_image.cpp b/esphome/components/runtime_image/bmp_decoder.cpp similarity index 82% rename from esphome/components/online_image/bmp_image.cpp rename to esphome/components/runtime_image/bmp_decoder.cpp index 676a2efca9..1a56484c60 100644 --- a/esphome/components/online_image/bmp_image.cpp +++ b/esphome/components/runtime_image/bmp_decoder.cpp @@ -1,15 +1,14 @@ -#include "bmp_image.h" +#include "bmp_decoder.h" -#ifdef USE_ONLINE_IMAGE_BMP_SUPPORT +#ifdef USE_RUNTIME_IMAGE_BMP #include "esphome/components/display/display.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace online_image { +namespace esphome::runtime_image { -static const char *const TAG = "online_image.bmp"; +static const char *const TAG = "image_decoder.bmp"; int HOT BmpDecoder::decode(uint8_t *buffer, size_t size) { size_t index = 0; @@ -30,7 +29,11 @@ int HOT BmpDecoder::decode(uint8_t *buffer, size_t size) { return DECODE_ERROR_INVALID_TYPE; } - this->download_size_ = encode_uint32(buffer[5], buffer[4], buffer[3], buffer[2]); + // BMP file contains its own size in the header + size_t file_size = encode_uint32(buffer[5], buffer[4], buffer[3], buffer[2]); + if (this->expected_size_ == 0) { + this->expected_size_ = file_size; // Use file header size if not provided + } this->data_offset_ = encode_uint32(buffer[13], buffer[12], buffer[11], buffer[10]); this->current_index_ = 14; @@ -90,8 +93,8 @@ int HOT BmpDecoder::decode(uint8_t *buffer, size_t size) { while (index < size) { uint8_t current_byte = buffer[index]; for (uint8_t i = 0; i < 8; i++) { - size_t x = (this->paint_index_ % this->width_) + i; - size_t y = (this->height_ - 1) - (this->paint_index_ / this->width_); + size_t x = (this->paint_index_ % static_cast(this->width_)) + i; + size_t y = static_cast(this->height_ - 1) - (this->paint_index_ / static_cast(this->width_)); Color c = (current_byte & (1 << (7 - i))) ? display::COLOR_ON : display::COLOR_OFF; this->draw(x, y, 1, 1, c); } @@ -110,8 +113,8 @@ int HOT BmpDecoder::decode(uint8_t *buffer, size_t size) { uint8_t b = buffer[index]; uint8_t g = buffer[index + 1]; uint8_t r = buffer[index + 2]; - size_t x = this->paint_index_ % this->width_; - size_t y = (this->height_ - 1) - (this->paint_index_ / this->width_); + size_t x = this->paint_index_ % static_cast(this->width_); + size_t y = static_cast(this->height_ - 1) - (this->paint_index_ / static_cast(this->width_)); Color c = Color(r, g, b); this->draw(x, y, 1, 1, c); this->paint_index_++; @@ -133,7 +136,6 @@ int HOT BmpDecoder::decode(uint8_t *buffer, size_t size) { return size; }; -} // namespace online_image -} // namespace esphome +} // namespace esphome::runtime_image -#endif // USE_ONLINE_IMAGE_BMP_SUPPORT +#endif // USE_RUNTIME_IMAGE_BMP diff --git a/esphome/components/online_image/bmp_image.h b/esphome/components/runtime_image/bmp_decoder.h similarity index 52% rename from esphome/components/online_image/bmp_image.h rename to esphome/components/runtime_image/bmp_decoder.h index 916ffea1ad..37db6b4940 100644 --- a/esphome/components/online_image/bmp_image.h +++ b/esphome/components/runtime_image/bmp_decoder.h @@ -1,27 +1,32 @@ #pragma once #include "esphome/core/defines.h" -#ifdef USE_ONLINE_IMAGE_BMP_SUPPORT +#ifdef USE_RUNTIME_IMAGE_BMP #include "image_decoder.h" +#include "runtime_image.h" -namespace esphome { -namespace online_image { +namespace esphome::runtime_image { /** - * @brief Image decoder specialization for PNG images. + * @brief Image decoder specialization for BMP images. */ class BmpDecoder : public ImageDecoder { public: /** * @brief Construct a new BMP Decoder object. * - * @param display The image to decode the stream into. + * @param image The RuntimeImage to decode the stream into. */ - BmpDecoder(OnlineImage *image) : ImageDecoder(image) {} + BmpDecoder(RuntimeImage *image) : ImageDecoder(image) {} int HOT decode(uint8_t *buffer, size_t size) override; + bool is_finished() const override { + // BMP is finished when we've decoded all pixel data + return this->paint_index_ >= static_cast(this->width_ * this->height_); + } + protected: size_t current_index_{0}; size_t paint_index_{0}; @@ -36,7 +41,6 @@ class BmpDecoder : public ImageDecoder { uint8_t padding_bytes_{0}; }; -} // namespace online_image -} // namespace esphome +} // namespace esphome::runtime_image -#endif // USE_ONLINE_IMAGE_BMP_SUPPORT +#endif // USE_RUNTIME_IMAGE_BMP diff --git a/esphome/components/runtime_image/image_decoder.cpp b/esphome/components/runtime_image/image_decoder.cpp new file mode 100644 index 0000000000..8d3320b5d1 --- /dev/null +++ b/esphome/components/runtime_image/image_decoder.cpp @@ -0,0 +1,28 @@ +#include "image_decoder.h" +#include "runtime_image.h" +#include "esphome/core/log.h" +#include +#include + +namespace esphome::runtime_image { + +static const char *const TAG = "image_decoder"; + +bool ImageDecoder::set_size(int width, int height) { + bool success = this->image_->resize(width, height) > 0; + this->x_scale_ = static_cast(this->image_->get_buffer_width()) / width; + this->y_scale_ = static_cast(this->image_->get_buffer_height()) / height; + return success; +} + +void ImageDecoder::draw(int x, int y, int w, int h, const Color &color) { + auto width = std::min(this->image_->get_buffer_width(), static_cast(std::ceil((x + w) * this->x_scale_))); + auto height = std::min(this->image_->get_buffer_height(), static_cast(std::ceil((y + h) * this->y_scale_))); + for (int i = x * this->x_scale_; i < width; i++) { + for (int j = y * this->y_scale_; j < height; j++) { + this->image_->draw_pixel(i, j, color); + } + } +} + +} // namespace esphome::runtime_image diff --git a/esphome/components/online_image/image_decoder.h b/esphome/components/runtime_image/image_decoder.h similarity index 60% rename from esphome/components/online_image/image_decoder.h rename to esphome/components/runtime_image/image_decoder.h index d11b8b46d3..926108a8a0 100644 --- a/esphome/components/online_image/image_decoder.h +++ b/esphome/components/runtime_image/image_decoder.h @@ -1,8 +1,7 @@ #pragma once #include "esphome/core/color.h" -namespace esphome { -namespace online_image { +namespace esphome::runtime_image { enum DecodeError : int { DECODE_ERROR_INVALID_TYPE = -1, @@ -10,7 +9,7 @@ enum DecodeError : int { DECODE_ERROR_OUT_OF_MEMORY = -3, }; -class OnlineImage; +class RuntimeImage; /** * @brief Class to abstract decoding different image formats. @@ -20,19 +19,19 @@ class ImageDecoder { /** * @brief Construct a new Image Decoder object * - * @param image The image to decode the stream into. + * @param image The RuntimeImage to decode the stream into. */ - ImageDecoder(OnlineImage *image) : image_(image) {} + ImageDecoder(RuntimeImage *image) : image_(image) {} virtual ~ImageDecoder() = default; /** * @brief Initialize the decoder. * - * @param download_size The total number of bytes that need to be downloaded for the image. + * @param expected_size Hint about the expected data size (0 if unknown). * @return int Returns 0 on success, a {@see DecodeError} value in case of an error. */ - virtual int prepare(size_t download_size) { - this->download_size_ = download_size; + virtual int prepare(size_t expected_size) { + this->expected_size_ = expected_size; return 0; } @@ -73,49 +72,26 @@ class ImageDecoder { */ void draw(int x, int y, int w, int h, const Color &color); - bool is_finished() const { return this->decoded_bytes_ == this->download_size_; } + /** + * @brief Check if the decoder has finished processing. + * + * This should be overridden by decoders that can detect completion + * based on format-specific markers rather than byte counts. + */ + virtual bool is_finished() const { + if (this->expected_size_ > 0) { + return this->decoded_bytes_ >= this->expected_size_; + } + // If size is unknown, derived classes should override this + return false; + } protected: - OnlineImage *image_; - // Initializing to 1, to ensure it is distinguishable from initial "decoded_bytes_". - // Will be overwritten anyway once the download size is known. - size_t download_size_ = 1; - size_t decoded_bytes_ = 0; + RuntimeImage *image_; + size_t expected_size_ = 0; // Expected data size (0 if unknown) + size_t decoded_bytes_ = 0; // Bytes processed so far double x_scale_ = 1.0; double y_scale_ = 1.0; }; -class DownloadBuffer { - public: - DownloadBuffer(size_t size); - - virtual ~DownloadBuffer() { this->allocator_.deallocate(this->buffer_, this->size_); } - - uint8_t *data(size_t offset = 0); - - uint8_t *append() { return this->data(this->unread_); } - - size_t unread() const { return this->unread_; } - size_t size() const { return this->size_; } - size_t free_capacity() const { return this->size_ - this->unread_; } - - size_t read(size_t len); - size_t write(size_t len) { - this->unread_ += len; - return this->unread_; - } - - void reset() { this->unread_ = 0; } - - size_t resize(size_t size); - - protected: - RAMAllocator allocator_{}; - uint8_t *buffer_; - size_t size_; - /** Total number of downloaded bytes not yet read. */ - size_t unread_; -}; - -} // namespace online_image -} // namespace esphome +} // namespace esphome::runtime_image diff --git a/esphome/components/online_image/jpeg_image.cpp b/esphome/components/runtime_image/jpeg_decoder.cpp similarity index 69% rename from esphome/components/online_image/jpeg_image.cpp rename to esphome/components/runtime_image/jpeg_decoder.cpp index 10586091d5..dcaa07cd58 100644 --- a/esphome/components/online_image/jpeg_image.cpp +++ b/esphome/components/runtime_image/jpeg_decoder.cpp @@ -1,16 +1,19 @@ -#include "jpeg_image.h" -#ifdef USE_ONLINE_IMAGE_JPEG_SUPPORT +#include "jpeg_decoder.h" +#ifdef USE_RUNTIME_IMAGE_JPEG #include "esphome/components/display/display_buffer.h" #include "esphome/core/application.h" +#include "esphome/core/defines.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" -#include "online_image.h" -static const char *const TAG = "online_image.jpeg"; +#ifdef USE_ESP_IDF +#include "esp_task_wdt.h" +#endif -namespace esphome { -namespace online_image { +static const char *const TAG = "image_decoder.jpeg"; + +namespace esphome::runtime_image { /** * @brief Callback method that will be called by the JPEGDEC engine when a chunk @@ -22,8 +25,14 @@ static int draw_callback(JPEGDRAW *jpeg) { ImageDecoder *decoder = (ImageDecoder *) jpeg->pUser; // Some very big images take too long to decode, so feed the watchdog on each callback - // to avoid crashing. - App.feed_wdt(); + // to avoid crashing if the executing task has a watchdog enabled. +#ifdef USE_ESP_IDF + if (esp_task_wdt_status(nullptr) == ESP_OK) { +#endif + App.feed_wdt(); +#ifdef USE_ESP_IDF + } +#endif size_t position = 0; size_t height = static_cast(jpeg->iHeight); size_t width = static_cast(jpeg->iWidth); @@ -43,22 +52,23 @@ static int draw_callback(JPEGDRAW *jpeg) { return 1; } -int JpegDecoder::prepare(size_t download_size) { - ImageDecoder::prepare(download_size); - auto size = this->image_->resize_download_buffer(download_size); - if (size < download_size) { - ESP_LOGE(TAG, "Download buffer resize failed!"); - return DECODE_ERROR_OUT_OF_MEMORY; - } +int JpegDecoder::prepare(size_t expected_size) { + ImageDecoder::prepare(expected_size); + // JPEG decoder needs complete data before decoding return 0; } int HOT JpegDecoder::decode(uint8_t *buffer, size_t size) { - if (size < this->download_size_) { - ESP_LOGV(TAG, "Download not complete. Size: %d/%d", size, this->download_size_); + // JPEG decoder requires complete data + // If we know the expected size, wait for it + if (this->expected_size_ > 0 && size < this->expected_size_) { + ESP_LOGV(TAG, "Download not complete. Size: %zu/%zu", size, this->expected_size_); return 0; } + // If size unknown, try to decode and see if it's valid + // The JPEGDEC library will fail gracefully if data is incomplete + if (!this->jpeg_.openRAM(buffer, size, draw_callback)) { ESP_LOGE(TAG, "Could not open image for decoding: %d", this->jpeg_.getLastError()); return DECODE_ERROR_INVALID_TYPE; @@ -88,7 +98,6 @@ int HOT JpegDecoder::decode(uint8_t *buffer, size_t size) { return size; } -} // namespace online_image -} // namespace esphome +} // namespace esphome::runtime_image -#endif // USE_ONLINE_IMAGE_JPEG_SUPPORT +#endif // USE_RUNTIME_IMAGE_JPEG diff --git a/esphome/components/online_image/jpeg_image.h b/esphome/components/runtime_image/jpeg_decoder.h similarity index 54% rename from esphome/components/online_image/jpeg_image.h rename to esphome/components/runtime_image/jpeg_decoder.h index fd488d6138..ed2401e263 100644 --- a/esphome/components/online_image/jpeg_image.h +++ b/esphome/components/runtime_image/jpeg_decoder.h @@ -1,12 +1,12 @@ #pragma once #include "image_decoder.h" +#include "runtime_image.h" #include "esphome/core/defines.h" -#ifdef USE_ONLINE_IMAGE_JPEG_SUPPORT +#ifdef USE_RUNTIME_IMAGE_JPEG #include -namespace esphome { -namespace online_image { +namespace esphome::runtime_image { /** * @brief Image decoder specialization for JPEG images. @@ -16,19 +16,18 @@ class JpegDecoder : public ImageDecoder { /** * @brief Construct a new JPEG Decoder object. * - * @param display The image to decode the stream into. + * @param image The RuntimeImage to decode the stream into. */ - JpegDecoder(OnlineImage *image) : ImageDecoder(image) {} + JpegDecoder(RuntimeImage *image) : ImageDecoder(image) {} ~JpegDecoder() override {} - int prepare(size_t download_size) override; + int prepare(size_t expected_size) override; int HOT decode(uint8_t *buffer, size_t size) override; protected: JPEGDEC jpeg_{}; }; -} // namespace online_image -} // namespace esphome +} // namespace esphome::runtime_image -#endif // USE_ONLINE_IMAGE_JPEG_SUPPORT +#endif // USE_RUNTIME_IMAGE_JPEG diff --git a/esphome/components/online_image/png_image.cpp b/esphome/components/runtime_image/png_decoder.cpp similarity index 82% rename from esphome/components/online_image/png_image.cpp rename to esphome/components/runtime_image/png_decoder.cpp index ce9d3bdc91..9fe4a9c4ff 100644 --- a/esphome/components/online_image/png_image.cpp +++ b/esphome/components/runtime_image/png_decoder.cpp @@ -1,15 +1,14 @@ -#include "png_image.h" -#ifdef USE_ONLINE_IMAGE_PNG_SUPPORT +#include "png_decoder.h" +#ifdef USE_RUNTIME_IMAGE_PNG #include "esphome/components/display/display_buffer.h" #include "esphome/core/application.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" -static const char *const TAG = "online_image.png"; +static const char *const TAG = "image_decoder.png"; -namespace esphome { -namespace online_image { +namespace esphome::runtime_image { /** * @brief Callback method that will be called by the PNGLE engine when the basic @@ -49,7 +48,7 @@ static void draw_callback(pngle_t *pngle, uint32_t x, uint32_t y, uint32_t w, ui } } -PngDecoder::PngDecoder(OnlineImage *image) : ImageDecoder(image) { +PngDecoder::PngDecoder(RuntimeImage *image) : ImageDecoder(image) { { pngle_t *pngle = this->allocator_.allocate(1, PNGLE_T_SIZE); if (!pngle) { @@ -69,8 +68,8 @@ PngDecoder::~PngDecoder() { } } -int PngDecoder::prepare(size_t download_size) { - ImageDecoder::prepare(download_size); +int PngDecoder::prepare(size_t expected_size) { + ImageDecoder::prepare(expected_size); if (!this->pngle_) { ESP_LOGE(TAG, "PNG decoder engine not initialized!"); return DECODE_ERROR_OUT_OF_MEMORY; @@ -86,8 +85,9 @@ int HOT PngDecoder::decode(uint8_t *buffer, size_t size) { ESP_LOGE(TAG, "PNG decoder engine not initialized!"); return DECODE_ERROR_OUT_OF_MEMORY; } - if (size < 256 && size < this->download_size_ - this->decoded_bytes_) { - ESP_LOGD(TAG, "Waiting for data"); + // PNG can be decoded progressively, but wait for a reasonable chunk + if (size < 256 && this->expected_size_ > 0 && size < this->expected_size_ - this->decoded_bytes_) { + ESP_LOGD(TAG, "Waiting for more data"); return 0; } auto fed = pngle_feed(this->pngle_, buffer, size); @@ -99,7 +99,6 @@ int HOT PngDecoder::decode(uint8_t *buffer, size_t size) { return fed; } -} // namespace online_image -} // namespace esphome +} // namespace esphome::runtime_image -#endif // USE_ONLINE_IMAGE_PNG_SUPPORT +#endif // USE_RUNTIME_IMAGE_PNG diff --git a/esphome/components/online_image/png_image.h b/esphome/components/runtime_image/png_decoder.h similarity index 65% rename from esphome/components/online_image/png_image.h rename to esphome/components/runtime_image/png_decoder.h index 40e85dde33..b5c1e70c2a 100644 --- a/esphome/components/online_image/png_image.h +++ b/esphome/components/runtime_image/png_decoder.h @@ -3,11 +3,11 @@ #include "esphome/core/defines.h" #include "esphome/core/helpers.h" #include "image_decoder.h" -#ifdef USE_ONLINE_IMAGE_PNG_SUPPORT +#include "runtime_image.h" +#ifdef USE_RUNTIME_IMAGE_PNG #include -namespace esphome { -namespace online_image { +namespace esphome::runtime_image { /** * @brief Image decoder specialization for PNG images. @@ -17,12 +17,12 @@ class PngDecoder : public ImageDecoder { /** * @brief Construct a new PNG Decoder object. * - * @param display The image to decode the stream into. + * @param image The RuntimeImage to decode the stream into. */ - PngDecoder(OnlineImage *image); + PngDecoder(RuntimeImage *image); ~PngDecoder() override; - int prepare(size_t download_size) override; + int prepare(size_t expected_size) override; int HOT decode(uint8_t *buffer, size_t size) override; void increment_pixels_decoded(uint32_t count) { this->pixels_decoded_ += count; } @@ -30,11 +30,10 @@ class PngDecoder : public ImageDecoder { protected: RAMAllocator allocator_; - pngle_t *pngle_; + pngle_t *pngle_{nullptr}; uint32_t pixels_decoded_{0}; }; -} // namespace online_image -} // namespace esphome +} // namespace esphome::runtime_image -#endif // USE_ONLINE_IMAGE_PNG_SUPPORT +#endif // USE_RUNTIME_IMAGE_PNG diff --git a/esphome/components/runtime_image/runtime_image.cpp b/esphome/components/runtime_image/runtime_image.cpp new file mode 100644 index 0000000000..1d70f38d6b --- /dev/null +++ b/esphome/components/runtime_image/runtime_image.cpp @@ -0,0 +1,300 @@ +#include "runtime_image.h" +#include "image_decoder.h" +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" +#include + +#ifdef USE_RUNTIME_IMAGE_BMP +#include "bmp_decoder.h" +#endif +#ifdef USE_RUNTIME_IMAGE_JPEG +#include "jpeg_decoder.h" +#endif +#ifdef USE_RUNTIME_IMAGE_PNG +#include "png_decoder.h" +#endif + +namespace esphome::runtime_image { + +static const char *const TAG = "runtime_image"; + +inline bool is_color_on(const Color &color) { + // This produces the most accurate monochrome conversion, but is slightly slower. + // return (0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b) > 127; + + // Approximation using fast integer computations; produces acceptable results + // Equivalent to 0.25 * R + 0.5 * G + 0.25 * B + return ((color.r >> 2) + (color.g >> 1) + (color.b >> 2)) & 0x80; +} + +RuntimeImage::RuntimeImage(ImageFormat format, image::ImageType type, image::Transparency transparency, + image::Image *placeholder, bool is_big_endian, int fixed_width, int fixed_height) + : Image(nullptr, 0, 0, type, transparency), + format_(format), + fixed_width_(fixed_width), + fixed_height_(fixed_height), + placeholder_(placeholder), + is_big_endian_(is_big_endian) {} + +RuntimeImage::~RuntimeImage() { this->release(); } + +int RuntimeImage::resize(int width, int height) { + // Use fixed dimensions if specified (0 means auto-resize) + int target_width = this->fixed_width_ ? this->fixed_width_ : width; + int target_height = this->fixed_height_ ? this->fixed_height_ : height; + + size_t result = this->resize_buffer_(target_width, target_height); + if (result > 0 && this->progressive_display_) { + // Update display dimensions for progressive display + this->width_ = this->buffer_width_; + this->height_ = this->buffer_height_; + this->data_start_ = this->buffer_; + } + return result; +} + +void RuntimeImage::draw_pixel(int x, int y, const Color &color) { + if (!this->buffer_) { + ESP_LOGE(TAG, "Buffer not allocated!"); + return; + } + if (x < 0 || y < 0 || x >= this->buffer_width_ || y >= this->buffer_height_) { + ESP_LOGE(TAG, "Tried to paint a pixel (%d,%d) outside the image!", x, y); + return; + } + + switch (this->type_) { + case image::IMAGE_TYPE_BINARY: { + const uint32_t width_8 = ((this->buffer_width_ + 7u) / 8u) * 8u; + uint32_t pos = x + y * width_8; + auto bitno = 0x80 >> (pos % 8u); + pos /= 8u; + auto on = is_color_on(color); + if (this->has_transparency() && color.w < 0x80) + on = false; + if (on) { + this->buffer_[pos] |= bitno; + } else { + this->buffer_[pos] &= ~bitno; + } + break; + } + case image::IMAGE_TYPE_GRAYSCALE: { + uint32_t pos = this->get_position_(x, y); + auto gray = static_cast(0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b); + if (this->transparency_ == image::TRANSPARENCY_CHROMA_KEY) { + if (gray == 1) { + gray = 0; + } + if (color.w < 0x80) { + gray = 1; + } + } else if (this->transparency_ == image::TRANSPARENCY_ALPHA_CHANNEL) { + if (color.w != 0xFF) + gray = color.w; + } + this->buffer_[pos] = gray; + break; + } + case image::IMAGE_TYPE_RGB565: { + uint32_t pos = this->get_position_(x, y); + Color mapped_color = color; + this->map_chroma_key(mapped_color); + uint16_t rgb565 = display::ColorUtil::color_to_565(mapped_color); + if (this->is_big_endian_) { + this->buffer_[pos + 0] = static_cast((rgb565 >> 8) & 0xFF); + this->buffer_[pos + 1] = static_cast(rgb565 & 0xFF); + } else { + this->buffer_[pos + 0] = static_cast(rgb565 & 0xFF); + this->buffer_[pos + 1] = static_cast((rgb565 >> 8) & 0xFF); + } + if (this->transparency_ == image::TRANSPARENCY_ALPHA_CHANNEL) { + this->buffer_[pos + 2] = color.w; + } + break; + } + case image::IMAGE_TYPE_RGB: { + uint32_t pos = this->get_position_(x, y); + Color mapped_color = color; + this->map_chroma_key(mapped_color); + this->buffer_[pos + 0] = mapped_color.r; + this->buffer_[pos + 1] = mapped_color.g; + this->buffer_[pos + 2] = mapped_color.b; + if (this->transparency_ == image::TRANSPARENCY_ALPHA_CHANNEL) { + this->buffer_[pos + 3] = color.w; + } + break; + } + } +} + +void RuntimeImage::map_chroma_key(Color &color) { + if (this->transparency_ == image::TRANSPARENCY_CHROMA_KEY) { + if (color.g == 1 && color.r == 0 && color.b == 0) { + color.g = 0; + } + if (color.w < 0x80) { + color.r = 0; + color.g = this->type_ == image::IMAGE_TYPE_RGB565 ? 4 : 1; + color.b = 0; + } + } +} + +void RuntimeImage::draw(int x, int y, display::Display *display, Color color_on, Color color_off) { + if (this->data_start_) { + // If we have a complete image, use the base class draw method + Image::draw(x, y, display, color_on, color_off); + } else if (this->placeholder_) { + // Show placeholder while the runtime image is not available + this->placeholder_->draw(x, y, display, color_on, color_off); + } + // If no image is loaded and no placeholder, nothing to draw +} + +bool RuntimeImage::begin_decode(size_t expected_size) { + if (this->decoder_) { + ESP_LOGW(TAG, "Decoding already in progress"); + return false; + } + + this->decoder_ = this->create_decoder_(); + if (!this->decoder_) { + ESP_LOGE(TAG, "Failed to create decoder for format %d", this->format_); + return false; + } + + this->total_size_ = expected_size; + this->decoded_bytes_ = 0; + + // Initialize decoder + int result = this->decoder_->prepare(expected_size); + if (result < 0) { + ESP_LOGE(TAG, "Failed to prepare decoder: %d", result); + this->decoder_ = nullptr; + return false; + } + + return true; +} + +int RuntimeImage::feed_data(uint8_t *data, size_t len) { + if (!this->decoder_) { + ESP_LOGE(TAG, "No decoder initialized"); + return -1; + } + + int consumed = this->decoder_->decode(data, len); + if (consumed > 0) { + this->decoded_bytes_ += consumed; + } + + return consumed; +} + +bool RuntimeImage::end_decode() { + if (!this->decoder_) { + return false; + } + + // Finalize the image for display + if (!this->progressive_display_) { + // Only now make the image visible + this->width_ = this->buffer_width_; + this->height_ = this->buffer_height_; + this->data_start_ = this->buffer_; + } + + // Clean up decoder + this->decoder_ = nullptr; + + ESP_LOGD(TAG, "Decoding complete: %dx%d, %zu bytes", this->width_, this->height_, this->decoded_bytes_); + return true; +} + +bool RuntimeImage::is_decode_finished() const { + if (!this->decoder_) { + return false; + } + return this->decoder_->is_finished(); +} + +void RuntimeImage::release() { + this->release_buffer_(); + // Reset decoder separately — release() can be called from within the decoder + // (via set_size -> resize -> resize_buffer_), so we must not destroy the decoder here. + // The decoder lifecycle is managed by begin_decode()/end_decode(). + this->decoder_ = nullptr; +} + +void RuntimeImage::release_buffer_() { + if (this->buffer_) { + ESP_LOGV(TAG, "Releasing buffer of size %zu", this->get_buffer_size_(this->buffer_width_, this->buffer_height_)); + this->allocator_.deallocate(this->buffer_, this->get_buffer_size_(this->buffer_width_, this->buffer_height_)); + this->buffer_ = nullptr; + this->data_start_ = nullptr; + this->width_ = 0; + this->height_ = 0; + this->buffer_width_ = 0; + this->buffer_height_ = 0; + } +} + +size_t RuntimeImage::resize_buffer_(int width, int height) { + size_t new_size = this->get_buffer_size_(width, height); + + if (this->buffer_ && this->buffer_width_ == width && this->buffer_height_ == height) { + // Buffer already allocated with correct size + return new_size; + } + + // Release old buffer if dimensions changed + if (this->buffer_) { + this->release_buffer_(); + } + + ESP_LOGD(TAG, "Allocating buffer: %dx%d, %zu bytes", width, height, new_size); + this->buffer_ = this->allocator_.allocate(new_size); + + if (!this->buffer_) { + ESP_LOGE(TAG, "Failed to allocate %zu bytes. Largest free block: %zu", new_size, + this->allocator_.get_max_free_block_size()); + return 0; + } + + // Clear buffer + memset(this->buffer_, 0, new_size); + + this->buffer_width_ = width; + this->buffer_height_ = height; + + return new_size; +} + +size_t RuntimeImage::get_buffer_size_(int width, int height) const { + return (this->get_bpp() * width + 7u) / 8u * height; +} + +int RuntimeImage::get_position_(int x, int y) const { return (x + y * this->buffer_width_) * this->get_bpp() / 8; } + +std::unique_ptr RuntimeImage::create_decoder_() { + switch (this->format_) { +#ifdef USE_RUNTIME_IMAGE_BMP + case BMP: + return make_unique(this); +#endif +#ifdef USE_RUNTIME_IMAGE_JPEG + case JPEG: + return make_unique(this); +#endif +#ifdef USE_RUNTIME_IMAGE_PNG + case PNG: + return make_unique(this); +#endif + default: + ESP_LOGE(TAG, "Unsupported image format: %d", this->format_); + return nullptr; + } +} + +} // namespace esphome::runtime_image diff --git a/esphome/components/runtime_image/runtime_image.h b/esphome/components/runtime_image/runtime_image.h new file mode 100644 index 0000000000..0a5279d86d --- /dev/null +++ b/esphome/components/runtime_image/runtime_image.h @@ -0,0 +1,214 @@ +#pragma once + +#include "esphome/components/image/image.h" +#include "esphome/core/helpers.h" + +namespace esphome::runtime_image { + +// Forward declaration +class ImageDecoder; + +/** + * @brief Image format types that can be decoded dynamically. + */ +enum ImageFormat { + /** Automatically detect from data. Not implemented yet. */ + AUTO, + /** JPEG format. */ + JPEG, + /** PNG format. */ + PNG, + /** BMP format. */ + BMP, +}; + +/** + * @brief A dynamic image that can be loaded and decoded at runtime. + * + * This class provides dynamic buffer allocation and management for images + * that are decoded at runtime, as opposed to static images compiled into + * the firmware. It serves as a base class for components that need to + * load images dynamically from various sources. + */ +class RuntimeImage : public image::Image { + public: + /** + * @brief Construct a new RuntimeImage object. + * + * @param format The image format to decode. + * @param type The pixel format for the image. + * @param transparency The transparency type for the image. + * @param placeholder Optional placeholder image to show while loading. + * @param is_big_endian Whether the image is stored in big-endian format. + * @param fixed_width Fixed width for the image (0 for auto-resize). + * @param fixed_height Fixed height for the image (0 for auto-resize). + */ + RuntimeImage(ImageFormat format, image::ImageType type, image::Transparency transparency, + image::Image *placeholder = nullptr, bool is_big_endian = false, int fixed_width = 0, + int fixed_height = 0); + + ~RuntimeImage(); + + // Decoder interface methods + /** + * @brief Resize the image buffer to the requested dimensions. + * + * The buffer will be allocated if not existing. + * If fixed dimensions have been specified in the constructor, the buffer will be created + * with those dimensions and not resized, even on request. + * Otherwise, the old buffer will be deallocated and a new buffer with the requested + * dimensions allocated. + * + * @param width Requested width (ignored if fixed_width_ is set) + * @param height Requested height (ignored if fixed_height_ is set) + * @return Size of the allocated buffer in bytes, or 0 if allocation failed. + */ + int resize(int width, int height); + void draw_pixel(int x, int y, const Color &color); + void map_chroma_key(Color &color); + int get_buffer_width() const { return this->buffer_width_; } + int get_buffer_height() const { return this->buffer_height_; } + + // Image drawing interface + void draw(int x, int y, display::Display *display, Color color_on, Color color_off) override; + + /** + * @brief Begin decoding an image. + * + * @param expected_size Optional hint about the expected data size. + * @return true if decoder was successfully initialized. + */ + bool begin_decode(size_t expected_size = 0); + + /** + * @brief Feed data to the decoder. + * + * @param data Pointer to the data buffer. + * @param len Length of data to process. + * @return Number of bytes consumed by the decoder. + */ + int feed_data(uint8_t *data, size_t len); + + /** + * @brief Complete the decoding process. + * + * @return true if decoding completed successfully. + */ + bool end_decode(); + + /** + * @brief Check if decoding is currently in progress. + */ + bool is_decoding() const { return this->decoder_ != nullptr; } + + /** + * @brief Check if the decoder has finished processing all data. + * + * This delegates to the decoder's format-specific completion check, + * which handles both known-size and chunked transfer cases. + */ + bool is_decode_finished() const; + + /** + * @brief Check if an image is currently loaded. + */ + bool is_loaded() const { return this->buffer_ != nullptr; } + + /** + * @brief Get the image format. + */ + ImageFormat get_format() const { return this->format_; } + + /** + * @brief Release the image buffer and free memory. + */ + void release(); + + /** + * @brief Set whether to allow progressive display during decode. + * + * When enabled, the image can be displayed even while still decoding. + * When disabled, the image is only displayed after decoding completes. + */ + void set_progressive_display(bool progressive) { this->progressive_display_ = progressive; } + + protected: + /** + * @brief Resize the image buffer to the requested dimensions. + * + * @param width New width in pixels. + * @param height New height in pixels. + * @return Size of the allocated buffer, or 0 on failure. + */ + size_t resize_buffer_(int width, int height); + + /** + * @brief Release only the image buffer without resetting the decoder. + * + * This is safe to call from within the decoder (e.g., during resize). + */ + void release_buffer_(); + + /** + * @brief Get the buffer size in bytes for given dimensions. + */ + size_t get_buffer_size_(int width, int height) const; + + /** + * @brief Get the position in the buffer for a pixel. + */ + int get_position_(int x, int y) const; + + /** + * @brief Create decoder instance for the image's format. + */ + std::unique_ptr create_decoder_(); + + // Memory management + RAMAllocator allocator_{}; + uint8_t *buffer_{nullptr}; + + // Decoder management + std::unique_ptr decoder_{nullptr}; + /** The image format this RuntimeImage is configured to decode. */ + const ImageFormat format_; + + /** + * Actual width of the current image. + * This needs to be separate from "Image::get_width()" because the latter + * must return 0 until the image has been decoded (to avoid showing partially + * decoded images). When progressive_display_ is enabled, Image dimensions + * are updated during decoding to allow rendering in progress. + */ + int buffer_width_{0}; + /** + * Actual height of the current image. + * This needs to be separate from "Image::get_height()" because the latter + * must return 0 until the image has been decoded (to avoid showing partially + * decoded images). When progressive_display_ is enabled, Image dimensions + * are updated during decoding to allow rendering in progress. + */ + int buffer_height_{0}; + + // Decoding state + size_t total_size_{0}; + size_t decoded_bytes_{0}; + + /** Fixed width requested on configuration, or 0 if not specified. */ + const int fixed_width_{0}; + /** Fixed height requested on configuration, or 0 if not specified. */ + const int fixed_height_{0}; + + /** Placeholder image to show when the runtime image is not available. */ + image::Image *placeholder_{nullptr}; + + // Configuration + bool progressive_display_{false}; + /** + * Whether the image is stored in big-endian format. + * This is used to determine how to store 16 bit colors in the buffer. + */ + bool is_big_endian_{false}; +}; + +} // namespace esphome::runtime_image diff --git a/esphome/core/defines.h b/esphome/core/defines.h index bfa33e4e59..0d6c1a42e8 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -148,9 +148,9 @@ #define USE_MQTT #define USE_MQTT_COVER_JSON #define USE_NETWORK -#define USE_ONLINE_IMAGE_BMP_SUPPORT -#define USE_ONLINE_IMAGE_PNG_SUPPORT -#define USE_ONLINE_IMAGE_JPEG_SUPPORT +#define USE_RUNTIME_IMAGE_BMP +#define USE_RUNTIME_IMAGE_PNG +#define USE_RUNTIME_IMAGE_JPEG #define USE_OTA #define USE_OTA_PASSWORD #define USE_OTA_STATE_LISTENER From f24e7709aca9e0cb7fc5951ea82141fec1b5dd9e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 13 Feb 2026 16:21:50 -0600 Subject: [PATCH 020/197] [core] Make LOG_ENTITY_ICON a no-op when icons are compiled out (#13973) --- esphome/core/entity_base.cpp | 2 ++ esphome/core/entity_base.h | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/esphome/core/entity_base.cpp b/esphome/core/entity_base.cpp index 811b856b5e..f6a7ec1dfd 100644 --- a/esphome/core/entity_base.cpp +++ b/esphome/core/entity_base.cpp @@ -152,11 +152,13 @@ void EntityBase_UnitOfMeasurement::set_unit_of_measurement(const char *unit_of_m this->unit_of_measurement_ = unit_of_measurement; } +#ifdef USE_ENTITY_ICON void log_entity_icon(const char *tag, const char *prefix, const EntityBase &obj) { if (!obj.get_icon_ref().empty()) { ESP_LOGCONFIG(tag, "%s Icon: '%s'", prefix, obj.get_icon_ref().c_str()); } } +#endif void log_entity_device_class(const char *tag, const char *prefix, const EntityBase_DeviceClass &obj) { if (!obj.get_device_class_ref().empty()) { diff --git a/esphome/core/entity_base.h b/esphome/core/entity_base.h index 86cb75495b..cbc07cc44c 100644 --- a/esphome/core/entity_base.h +++ b/esphome/core/entity_base.h @@ -231,8 +231,13 @@ class EntityBase_UnitOfMeasurement { // NOLINT(readability-identifier-naming) }; /// Log entity icon if set (for use in dump_config) +#ifdef USE_ENTITY_ICON #define LOG_ENTITY_ICON(tag, prefix, obj) log_entity_icon(tag, prefix, obj) void log_entity_icon(const char *tag, const char *prefix, const EntityBase &obj); +#else +#define LOG_ENTITY_ICON(tag, prefix, obj) ((void) 0) +inline void log_entity_icon(const char *, const char *, const EntityBase &) {} +#endif /// Log entity device class if set (for use in dump_config) #define LOG_ENTITY_DEVICE_CLASS(tag, prefix, obj) log_entity_device_class(tag, prefix, obj) void log_entity_device_class(const char *tag, const char *prefix, const EntityBase_DeviceClass &obj); From 79d9fbf64579bee42d14adc9a4e17f8ddac3ac0b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 13 Feb 2026 16:22:05 -0600 Subject: [PATCH 021/197] [nfc] Replace constant std::vector with static constexpr std::array (#13978) --- esphome/components/pn532/pn532.h | 4 +- .../components/pn532/pn532_mifare_classic.cpp | 68 +++++++++---------- .../pn532/pn532_mifare_ultralight.cpp | 16 ++--- esphome/components/pn7150/pn7150.h | 4 +- .../pn7150/pn7150_mifare_classic.cpp | 66 +++++++++--------- .../pn7150/pn7150_mifare_ultralight.cpp | 13 ++-- esphome/components/pn7160/pn7160.h | 4 +- .../pn7160/pn7160_mifare_classic.cpp | 66 +++++++++--------- .../pn7160/pn7160_mifare_ultralight.cpp | 13 ++-- 9 files changed, 130 insertions(+), 124 deletions(-) diff --git a/esphome/components/pn532/pn532.h b/esphome/components/pn532/pn532.h index 73a6c15164..f98c0f9322 100644 --- a/esphome/components/pn532/pn532.h +++ b/esphome/components/pn532/pn532.h @@ -76,7 +76,7 @@ class PN532 : public PollingComponent { std::unique_ptr read_mifare_classic_tag_(nfc::NfcTagUid &uid); bool read_mifare_classic_block_(uint8_t block_num, std::vector &data); - bool write_mifare_classic_block_(uint8_t block_num, std::vector &data); + bool write_mifare_classic_block_(uint8_t block_num, const uint8_t *data, size_t len); bool auth_mifare_classic_block_(nfc::NfcTagUid &uid, uint8_t block_num, uint8_t key_num, const uint8_t *key); bool format_mifare_classic_mifare_(nfc::NfcTagUid &uid); bool format_mifare_classic_ndef_(nfc::NfcTagUid &uid); @@ -88,7 +88,7 @@ class PN532 : public PollingComponent { uint16_t read_mifare_ultralight_capacity_(); bool find_mifare_ultralight_ndef_(const std::vector &page_3_to_6, uint8_t &message_length, uint8_t &message_start_index); - bool write_mifare_ultralight_page_(uint8_t page_num, std::vector &write_data); + bool write_mifare_ultralight_page_(uint8_t page_num, const uint8_t *write_data, size_t len); bool write_mifare_ultralight_tag_(nfc::NfcTagUid &uid, nfc::NdefMessage *message); bool clean_mifare_ultralight_(); diff --git a/esphome/components/pn532/pn532_mifare_classic.cpp b/esphome/components/pn532/pn532_mifare_classic.cpp index b762d5d936..cca6acd96d 100644 --- a/esphome/components/pn532/pn532_mifare_classic.cpp +++ b/esphome/components/pn532/pn532_mifare_classic.cpp @@ -1,3 +1,4 @@ +#include #include #include "pn532.h" @@ -106,10 +107,10 @@ bool PN532::auth_mifare_classic_block_(nfc::NfcTagUid &uid, uint8_t block_num, u } bool PN532::format_mifare_classic_mifare_(nfc::NfcTagUid &uid) { - std::vector blank_buffer( - {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}); - std::vector trailer_buffer( - {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x07, 0x80, 0x69, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}); + static constexpr std::array BLANK_BUFFER = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + static constexpr std::array TRAILER_BUFFER = { + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x07, 0x80, 0x69, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; bool error = false; @@ -118,20 +119,20 @@ bool PN532::format_mifare_classic_mifare_(nfc::NfcTagUid &uid) { continue; } if (block != 0) { - if (!this->write_mifare_classic_block_(block, blank_buffer)) { + if (!this->write_mifare_classic_block_(block, BLANK_BUFFER.data(), BLANK_BUFFER.size())) { ESP_LOGE(TAG, "Unable to write block %d", block); error = true; } } - if (!this->write_mifare_classic_block_(block + 1, blank_buffer)) { + if (!this->write_mifare_classic_block_(block + 1, BLANK_BUFFER.data(), BLANK_BUFFER.size())) { ESP_LOGE(TAG, "Unable to write block %d", block + 1); error = true; } - if (!this->write_mifare_classic_block_(block + 2, blank_buffer)) { + if (!this->write_mifare_classic_block_(block + 2, BLANK_BUFFER.data(), BLANK_BUFFER.size())) { ESP_LOGE(TAG, "Unable to write block %d", block + 2); error = true; } - if (!this->write_mifare_classic_block_(block + 3, trailer_buffer)) { + if (!this->write_mifare_classic_block_(block + 3, TRAILER_BUFFER.data(), TRAILER_BUFFER.size())) { ESP_LOGE(TAG, "Unable to write block %d", block + 3); error = true; } @@ -141,28 +142,28 @@ bool PN532::format_mifare_classic_mifare_(nfc::NfcTagUid &uid) { } bool PN532::format_mifare_classic_ndef_(nfc::NfcTagUid &uid) { - std::vector empty_ndef_message( - {0x03, 0x03, 0xD0, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}); - std::vector blank_block( - {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}); - std::vector block_1_data( - {0x14, 0x01, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1}); - std::vector block_2_data( - {0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1}); - std::vector block_3_trailer( - {0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0x78, 0x77, 0x88, 0xC1, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}); - std::vector ndef_trailer( - {0xD3, 0xF7, 0xD3, 0xF7, 0xD3, 0xF7, 0x7F, 0x07, 0x88, 0x40, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}); + static constexpr std::array EMPTY_NDEF_MESSAGE = { + 0x03, 0x03, 0xD0, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + static constexpr std::array BLANK_BLOCK = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + static constexpr std::array BLOCK_1_DATA = { + 0x14, 0x01, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1}; + static constexpr std::array BLOCK_2_DATA = { + 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1}; + static constexpr std::array BLOCK_3_TRAILER = { + 0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0x78, 0x77, 0x88, 0xC1, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; + static constexpr std::array NDEF_TRAILER = { + 0xD3, 0xF7, 0xD3, 0xF7, 0xD3, 0xF7, 0x7F, 0x07, 0x88, 0x40, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; if (!this->auth_mifare_classic_block_(uid, 0, nfc::MIFARE_CMD_AUTH_B, nfc::DEFAULT_KEY)) { ESP_LOGE(TAG, "Unable to authenticate block 0 for formatting!"); return false; } - if (!this->write_mifare_classic_block_(1, block_1_data)) + if (!this->write_mifare_classic_block_(1, BLOCK_1_DATA.data(), BLOCK_1_DATA.size())) return false; - if (!this->write_mifare_classic_block_(2, block_2_data)) + if (!this->write_mifare_classic_block_(2, BLOCK_2_DATA.data(), BLOCK_2_DATA.size())) return false; - if (!this->write_mifare_classic_block_(3, block_3_trailer)) + if (!this->write_mifare_classic_block_(3, BLOCK_3_TRAILER.data(), BLOCK_3_TRAILER.size())) return false; ESP_LOGD(TAG, "Sector 0 formatted to NDEF"); @@ -172,36 +173,36 @@ bool PN532::format_mifare_classic_ndef_(nfc::NfcTagUid &uid) { return false; } if (block == 4) { - if (!this->write_mifare_classic_block_(block, empty_ndef_message)) { + if (!this->write_mifare_classic_block_(block, EMPTY_NDEF_MESSAGE.data(), EMPTY_NDEF_MESSAGE.size())) { ESP_LOGE(TAG, "Unable to write block %d", block); } } else { - if (!this->write_mifare_classic_block_(block, blank_block)) { + if (!this->write_mifare_classic_block_(block, BLANK_BLOCK.data(), BLANK_BLOCK.size())) { ESP_LOGE(TAG, "Unable to write block %d", block); } } - if (!this->write_mifare_classic_block_(block + 1, blank_block)) { + if (!this->write_mifare_classic_block_(block + 1, BLANK_BLOCK.data(), BLANK_BLOCK.size())) { ESP_LOGE(TAG, "Unable to write block %d", block + 1); } - if (!this->write_mifare_classic_block_(block + 2, blank_block)) { + if (!this->write_mifare_classic_block_(block + 2, BLANK_BLOCK.data(), BLANK_BLOCK.size())) { ESP_LOGE(TAG, "Unable to write block %d", block + 2); } - if (!this->write_mifare_classic_block_(block + 3, ndef_trailer)) { + if (!this->write_mifare_classic_block_(block + 3, NDEF_TRAILER.data(), NDEF_TRAILER.size())) { ESP_LOGE(TAG, "Unable to write trailer block %d", block + 3); } } return true; } -bool PN532::write_mifare_classic_block_(uint8_t block_num, std::vector &write_data) { - std::vector data({ +bool PN532::write_mifare_classic_block_(uint8_t block_num, const uint8_t *data, size_t len) { + std::vector cmd({ PN532_COMMAND_INDATAEXCHANGE, 0x01, // One card nfc::MIFARE_CMD_WRITE, block_num, }); - data.insert(data.end(), write_data.begin(), write_data.end()); - if (!this->write_command_(data)) { + cmd.insert(cmd.end(), data, data + len); + if (!this->write_command_(cmd)) { ESP_LOGE(TAG, "Error writing block %d", block_num); return false; } @@ -243,8 +244,7 @@ bool PN532::write_mifare_classic_tag_(nfc::NfcTagUid &uid, nfc::NdefMessage *mes } } - std::vector data(encoded.begin() + index, encoded.begin() + index + nfc::MIFARE_CLASSIC_BLOCK_SIZE); - if (!this->write_mifare_classic_block_(current_block, data)) { + if (!this->write_mifare_classic_block_(current_block, encoded.data() + index, nfc::MIFARE_CLASSIC_BLOCK_SIZE)) { return false; } index += nfc::MIFARE_CLASSIC_BLOCK_SIZE; diff --git a/esphome/components/pn532/pn532_mifare_ultralight.cpp b/esphome/components/pn532/pn532_mifare_ultralight.cpp index 01e41df5c0..a8a8e2d573 100644 --- a/esphome/components/pn532/pn532_mifare_ultralight.cpp +++ b/esphome/components/pn532/pn532_mifare_ultralight.cpp @@ -1,3 +1,4 @@ +#include #include #include "pn532.h" @@ -143,8 +144,7 @@ bool PN532::write_mifare_ultralight_tag_(nfc::NfcTagUid &uid, nfc::NdefMessage * uint8_t current_page = nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE; while (index < buffer_length) { - std::vector data(encoded.begin() + index, encoded.begin() + index + nfc::MIFARE_ULTRALIGHT_PAGE_SIZE); - if (!this->write_mifare_ultralight_page_(current_page, data)) { + if (!this->write_mifare_ultralight_page_(current_page, encoded.data() + index, nfc::MIFARE_ULTRALIGHT_PAGE_SIZE)) { return false; } index += nfc::MIFARE_ULTRALIGHT_PAGE_SIZE; @@ -157,25 +157,25 @@ bool PN532::clean_mifare_ultralight_() { uint32_t capacity = this->read_mifare_ultralight_capacity_(); uint8_t pages = (capacity / nfc::MIFARE_ULTRALIGHT_PAGE_SIZE) + nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE; - std::vector blank_data = {0x00, 0x00, 0x00, 0x00}; + static constexpr std::array BLANK_DATA = {0x00, 0x00, 0x00, 0x00}; for (int i = nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE; i < pages; i++) { - if (!this->write_mifare_ultralight_page_(i, blank_data)) { + if (!this->write_mifare_ultralight_page_(i, BLANK_DATA.data(), BLANK_DATA.size())) { return false; } } return true; } -bool PN532::write_mifare_ultralight_page_(uint8_t page_num, std::vector &write_data) { - std::vector data({ +bool PN532::write_mifare_ultralight_page_(uint8_t page_num, const uint8_t *write_data, size_t len) { + std::vector cmd({ PN532_COMMAND_INDATAEXCHANGE, 0x01, // One card nfc::MIFARE_CMD_WRITE_ULTRALIGHT, page_num, }); - data.insert(data.end(), write_data.begin(), write_data.end()); - if (!this->write_command_(data)) { + cmd.insert(cmd.end(), write_data, write_data + len); + if (!this->write_command_(cmd)) { ESP_LOGE(TAG, "Error writing page %u", page_num); return false; } diff --git a/esphome/components/pn7150/pn7150.h b/esphome/components/pn7150/pn7150.h index a5dcef9f99..5feba17d21 100644 --- a/esphome/components/pn7150/pn7150.h +++ b/esphome/components/pn7150/pn7150.h @@ -236,7 +236,7 @@ class PN7150 : public nfc::Nfcc, public Component { uint8_t read_mifare_classic_tag_(nfc::NfcTag &tag); uint8_t read_mifare_classic_block_(uint8_t block_num, std::vector &data); - uint8_t write_mifare_classic_block_(uint8_t block_num, std::vector &data); + uint8_t write_mifare_classic_block_(uint8_t block_num, const uint8_t *data, size_t len); uint8_t auth_mifare_classic_block_(uint8_t block_num, uint8_t key_num, const uint8_t *key); uint8_t sect_to_auth_(uint8_t block_num); uint8_t format_mifare_classic_mifare_(); @@ -250,7 +250,7 @@ class PN7150 : public nfc::Nfcc, public Component { uint16_t read_mifare_ultralight_capacity_(); uint8_t find_mifare_ultralight_ndef_(const std::vector &page_3_to_6, uint8_t &message_length, uint8_t &message_start_index); - uint8_t write_mifare_ultralight_page_(uint8_t page_num, std::vector &write_data); + uint8_t write_mifare_ultralight_page_(uint8_t page_num, const uint8_t *write_data, size_t len); uint8_t write_mifare_ultralight_tag_(nfc::NfcTagUid &uid, const std::shared_ptr &message); uint8_t clean_mifare_ultralight_(); diff --git a/esphome/components/pn7150/pn7150_mifare_classic.cpp b/esphome/components/pn7150/pn7150_mifare_classic.cpp index dee81b610a..61434cdb28 100644 --- a/esphome/components/pn7150/pn7150_mifare_classic.cpp +++ b/esphome/components/pn7150/pn7150_mifare_classic.cpp @@ -1,3 +1,4 @@ +#include #include #include "pn7150.h" @@ -139,10 +140,10 @@ uint8_t PN7150::sect_to_auth_(const uint8_t block_num) { } uint8_t PN7150::format_mifare_classic_mifare_() { - std::vector blank_buffer( - {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}); - std::vector trailer_buffer( - {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x07, 0x80, 0x69, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}); + static constexpr std::array BLANK_BUFFER = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + static constexpr std::array TRAILER_BUFFER = { + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x07, 0x80, 0x69, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; auto status = nfc::STATUS_OK; @@ -151,20 +152,20 @@ uint8_t PN7150::format_mifare_classic_mifare_() { continue; } if (block != 0) { - if (this->write_mifare_classic_block_(block, blank_buffer) != nfc::STATUS_OK) { + if (this->write_mifare_classic_block_(block, BLANK_BUFFER.data(), BLANK_BUFFER.size()) != nfc::STATUS_OK) { ESP_LOGE(TAG, "Unable to write block %u", block); status = nfc::STATUS_FAILED; } } - if (this->write_mifare_classic_block_(block + 1, blank_buffer) != nfc::STATUS_OK) { + if (this->write_mifare_classic_block_(block + 1, BLANK_BUFFER.data(), BLANK_BUFFER.size()) != nfc::STATUS_OK) { ESP_LOGE(TAG, "Unable to write block %u", block + 1); status = nfc::STATUS_FAILED; } - if (this->write_mifare_classic_block_(block + 2, blank_buffer) != nfc::STATUS_OK) { + if (this->write_mifare_classic_block_(block + 2, BLANK_BUFFER.data(), BLANK_BUFFER.size()) != nfc::STATUS_OK) { ESP_LOGE(TAG, "Unable to write block %u", block + 2); status = nfc::STATUS_FAILED; } - if (this->write_mifare_classic_block_(block + 3, trailer_buffer) != nfc::STATUS_OK) { + if (this->write_mifare_classic_block_(block + 3, TRAILER_BUFFER.data(), TRAILER_BUFFER.size()) != nfc::STATUS_OK) { ESP_LOGE(TAG, "Unable to write block %u", block + 3); status = nfc::STATUS_FAILED; } @@ -174,30 +175,30 @@ uint8_t PN7150::format_mifare_classic_mifare_() { } uint8_t PN7150::format_mifare_classic_ndef_() { - std::vector empty_ndef_message( - {0x03, 0x03, 0xD0, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}); - std::vector blank_block( - {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}); - std::vector block_1_data( - {0x14, 0x01, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1}); - std::vector block_2_data( - {0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1}); - std::vector block_3_trailer( - {0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0x78, 0x77, 0x88, 0xC1, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}); - std::vector ndef_trailer( - {0xD3, 0xF7, 0xD3, 0xF7, 0xD3, 0xF7, 0x7F, 0x07, 0x88, 0x40, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}); + static constexpr std::array EMPTY_NDEF_MESSAGE = { + 0x03, 0x03, 0xD0, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + static constexpr std::array BLANK_BLOCK = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + static constexpr std::array BLOCK_1_DATA = { + 0x14, 0x01, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1}; + static constexpr std::array BLOCK_2_DATA = { + 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1}; + static constexpr std::array BLOCK_3_TRAILER = { + 0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0x78, 0x77, 0x88, 0xC1, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; + static constexpr std::array NDEF_TRAILER = { + 0xD3, 0xF7, 0xD3, 0xF7, 0xD3, 0xF7, 0x7F, 0x07, 0x88, 0x40, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; if (this->auth_mifare_classic_block_(0, nfc::MIFARE_CMD_AUTH_B, nfc::DEFAULT_KEY) != nfc::STATUS_OK) { ESP_LOGE(TAG, "Unable to authenticate block 0 for formatting"); return nfc::STATUS_FAILED; } - if (this->write_mifare_classic_block_(1, block_1_data) != nfc::STATUS_OK) { + if (this->write_mifare_classic_block_(1, BLOCK_1_DATA.data(), BLOCK_1_DATA.size()) != nfc::STATUS_OK) { return nfc::STATUS_FAILED; } - if (this->write_mifare_classic_block_(2, block_2_data) != nfc::STATUS_OK) { + if (this->write_mifare_classic_block_(2, BLOCK_2_DATA.data(), BLOCK_2_DATA.size()) != nfc::STATUS_OK) { return nfc::STATUS_FAILED; } - if (this->write_mifare_classic_block_(3, block_3_trailer) != nfc::STATUS_OK) { + if (this->write_mifare_classic_block_(3, BLOCK_3_TRAILER.data(), BLOCK_3_TRAILER.size()) != nfc::STATUS_OK) { return nfc::STATUS_FAILED; } @@ -210,25 +211,26 @@ uint8_t PN7150::format_mifare_classic_ndef_() { return nfc::STATUS_FAILED; } if (block == 4) { - if (this->write_mifare_classic_block_(block, empty_ndef_message) != nfc::STATUS_OK) { + if (this->write_mifare_classic_block_(block, EMPTY_NDEF_MESSAGE.data(), EMPTY_NDEF_MESSAGE.size()) != + nfc::STATUS_OK) { ESP_LOGE(TAG, "Unable to write block %u", block); status = nfc::STATUS_FAILED; } } else { - if (this->write_mifare_classic_block_(block, blank_block) != nfc::STATUS_OK) { + if (this->write_mifare_classic_block_(block, BLANK_BLOCK.data(), BLANK_BLOCK.size()) != nfc::STATUS_OK) { ESP_LOGE(TAG, "Unable to write block %u", block); status = nfc::STATUS_FAILED; } } - if (this->write_mifare_classic_block_(block + 1, blank_block) != nfc::STATUS_OK) { + if (this->write_mifare_classic_block_(block + 1, BLANK_BLOCK.data(), BLANK_BLOCK.size()) != nfc::STATUS_OK) { ESP_LOGE(TAG, "Unable to write block %u", block + 1); status = nfc::STATUS_FAILED; } - if (this->write_mifare_classic_block_(block + 2, blank_block) != nfc::STATUS_OK) { + if (this->write_mifare_classic_block_(block + 2, BLANK_BLOCK.data(), BLANK_BLOCK.size()) != nfc::STATUS_OK) { ESP_LOGE(TAG, "Unable to write block %u", block + 2); status = nfc::STATUS_FAILED; } - if (this->write_mifare_classic_block_(block + 3, ndef_trailer) != nfc::STATUS_OK) { + if (this->write_mifare_classic_block_(block + 3, NDEF_TRAILER.data(), NDEF_TRAILER.size()) != nfc::STATUS_OK) { ESP_LOGE(TAG, "Unable to write trailer block %u", block + 3); status = nfc::STATUS_FAILED; } @@ -236,7 +238,7 @@ uint8_t PN7150::format_mifare_classic_ndef_() { return status; } -uint8_t PN7150::write_mifare_classic_block_(uint8_t block_num, std::vector &write_data) { +uint8_t PN7150::write_mifare_classic_block_(uint8_t block_num, const uint8_t *data, size_t len) { nfc::NciMessage rx; nfc::NciMessage tx(nfc::NCI_PKT_MT_DATA, {XCHG_DATA_OID, nfc::MIFARE_CMD_WRITE, block_num}); @@ -248,7 +250,7 @@ uint8_t PN7150::write_mifare_classic_block_(uint8_t block_num, std::vectortransceive_(tx, rx, NFCC_TAG_WRITE_TIMEOUT) != nfc::STATUS_OK) { @@ -294,8 +296,8 @@ uint8_t PN7150::write_mifare_classic_tag_(const std::shared_ptr data(encoded.begin() + index, encoded.begin() + index + nfc::MIFARE_CLASSIC_BLOCK_SIZE); - if (this->write_mifare_classic_block_(current_block, data) != nfc::STATUS_OK) { + if (this->write_mifare_classic_block_(current_block, encoded.data() + index, nfc::MIFARE_CLASSIC_BLOCK_SIZE) != + nfc::STATUS_OK) { return nfc::STATUS_FAILED; } index += nfc::MIFARE_CLASSIC_BLOCK_SIZE; diff --git a/esphome/components/pn7150/pn7150_mifare_ultralight.cpp b/esphome/components/pn7150/pn7150_mifare_ultralight.cpp index 166065f6c1..46f5dba2b7 100644 --- a/esphome/components/pn7150/pn7150_mifare_ultralight.cpp +++ b/esphome/components/pn7150/pn7150_mifare_ultralight.cpp @@ -1,3 +1,4 @@ +#include #include #include @@ -144,8 +145,8 @@ uint8_t PN7150::write_mifare_ultralight_tag_(nfc::NfcTagUid &uid, const std::sha uint8_t current_page = nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE; while (index < buffer_length) { - std::vector data(encoded.begin() + index, encoded.begin() + index + nfc::MIFARE_ULTRALIGHT_PAGE_SIZE); - if (this->write_mifare_ultralight_page_(current_page, data) != nfc::STATUS_OK) { + if (this->write_mifare_ultralight_page_(current_page, encoded.data() + index, nfc::MIFARE_ULTRALIGHT_PAGE_SIZE) != + nfc::STATUS_OK) { return nfc::STATUS_FAILED; } index += nfc::MIFARE_ULTRALIGHT_PAGE_SIZE; @@ -158,19 +159,19 @@ uint8_t PN7150::clean_mifare_ultralight_() { uint32_t capacity = this->read_mifare_ultralight_capacity_(); uint8_t pages = (capacity / nfc::MIFARE_ULTRALIGHT_PAGE_SIZE) + nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE; - std::vector blank_data = {0x00, 0x00, 0x00, 0x00}; + static constexpr std::array BLANK_DATA = {0x00, 0x00, 0x00, 0x00}; for (int i = nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE; i < pages; i++) { - if (this->write_mifare_ultralight_page_(i, blank_data) != nfc::STATUS_OK) { + if (this->write_mifare_ultralight_page_(i, BLANK_DATA.data(), BLANK_DATA.size()) != nfc::STATUS_OK) { return nfc::STATUS_FAILED; } } return nfc::STATUS_OK; } -uint8_t PN7150::write_mifare_ultralight_page_(uint8_t page_num, std::vector &write_data) { +uint8_t PN7150::write_mifare_ultralight_page_(uint8_t page_num, const uint8_t *write_data, size_t len) { std::vector payload = {nfc::MIFARE_CMD_WRITE_ULTRALIGHT, page_num}; - payload.insert(payload.end(), write_data.begin(), write_data.end()); + payload.insert(payload.end(), write_data, write_data + len); nfc::NciMessage rx; nfc::NciMessage tx(nfc::NCI_PKT_MT_DATA, payload); diff --git a/esphome/components/pn7160/pn7160.h b/esphome/components/pn7160/pn7160.h index 572fab3351..9f2d10c2d5 100644 --- a/esphome/components/pn7160/pn7160.h +++ b/esphome/components/pn7160/pn7160.h @@ -253,7 +253,7 @@ class PN7160 : public nfc::Nfcc, public Component { uint8_t read_mifare_classic_tag_(nfc::NfcTag &tag); uint8_t read_mifare_classic_block_(uint8_t block_num, std::vector &data); - uint8_t write_mifare_classic_block_(uint8_t block_num, std::vector &data); + uint8_t write_mifare_classic_block_(uint8_t block_num, const uint8_t *data, size_t len); uint8_t auth_mifare_classic_block_(uint8_t block_num, uint8_t key_num, const uint8_t *key); uint8_t sect_to_auth_(uint8_t block_num); uint8_t format_mifare_classic_mifare_(); @@ -267,7 +267,7 @@ class PN7160 : public nfc::Nfcc, public Component { uint16_t read_mifare_ultralight_capacity_(); uint8_t find_mifare_ultralight_ndef_(const std::vector &page_3_to_6, uint8_t &message_length, uint8_t &message_start_index); - uint8_t write_mifare_ultralight_page_(uint8_t page_num, std::vector &write_data); + uint8_t write_mifare_ultralight_page_(uint8_t page_num, const uint8_t *write_data, size_t len); uint8_t write_mifare_ultralight_tag_(nfc::NfcTagUid &uid, const std::shared_ptr &message); uint8_t clean_mifare_ultralight_(); diff --git a/esphome/components/pn7160/pn7160_mifare_classic.cpp b/esphome/components/pn7160/pn7160_mifare_classic.cpp index 57d2042eaa..710a7198c6 100644 --- a/esphome/components/pn7160/pn7160_mifare_classic.cpp +++ b/esphome/components/pn7160/pn7160_mifare_classic.cpp @@ -1,3 +1,4 @@ +#include #include #include "pn7160.h" @@ -139,10 +140,10 @@ uint8_t PN7160::sect_to_auth_(const uint8_t block_num) { } uint8_t PN7160::format_mifare_classic_mifare_() { - std::vector blank_buffer( - {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}); - std::vector trailer_buffer( - {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x07, 0x80, 0x69, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}); + static constexpr std::array BLANK_BUFFER = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + static constexpr std::array TRAILER_BUFFER = { + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x07, 0x80, 0x69, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; auto status = nfc::STATUS_OK; @@ -151,20 +152,20 @@ uint8_t PN7160::format_mifare_classic_mifare_() { continue; } if (block != 0) { - if (this->write_mifare_classic_block_(block, blank_buffer) != nfc::STATUS_OK) { + if (this->write_mifare_classic_block_(block, BLANK_BUFFER.data(), BLANK_BUFFER.size()) != nfc::STATUS_OK) { ESP_LOGE(TAG, "Unable to write block %u", block); status = nfc::STATUS_FAILED; } } - if (this->write_mifare_classic_block_(block + 1, blank_buffer) != nfc::STATUS_OK) { + if (this->write_mifare_classic_block_(block + 1, BLANK_BUFFER.data(), BLANK_BUFFER.size()) != nfc::STATUS_OK) { ESP_LOGE(TAG, "Unable to write block %u", block + 1); status = nfc::STATUS_FAILED; } - if (this->write_mifare_classic_block_(block + 2, blank_buffer) != nfc::STATUS_OK) { + if (this->write_mifare_classic_block_(block + 2, BLANK_BUFFER.data(), BLANK_BUFFER.size()) != nfc::STATUS_OK) { ESP_LOGE(TAG, "Unable to write block %u", block + 2); status = nfc::STATUS_FAILED; } - if (this->write_mifare_classic_block_(block + 3, trailer_buffer) != nfc::STATUS_OK) { + if (this->write_mifare_classic_block_(block + 3, TRAILER_BUFFER.data(), TRAILER_BUFFER.size()) != nfc::STATUS_OK) { ESP_LOGE(TAG, "Unable to write block %u", block + 3); status = nfc::STATUS_FAILED; } @@ -174,30 +175,30 @@ uint8_t PN7160::format_mifare_classic_mifare_() { } uint8_t PN7160::format_mifare_classic_ndef_() { - std::vector empty_ndef_message( - {0x03, 0x03, 0xD0, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}); - std::vector blank_block( - {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}); - std::vector block_1_data( - {0x14, 0x01, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1}); - std::vector block_2_data( - {0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1}); - std::vector block_3_trailer( - {0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0x78, 0x77, 0x88, 0xC1, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}); - std::vector ndef_trailer( - {0xD3, 0xF7, 0xD3, 0xF7, 0xD3, 0xF7, 0x7F, 0x07, 0x88, 0x40, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}); + static constexpr std::array EMPTY_NDEF_MESSAGE = { + 0x03, 0x03, 0xD0, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + static constexpr std::array BLANK_BLOCK = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + static constexpr std::array BLOCK_1_DATA = { + 0x14, 0x01, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1}; + static constexpr std::array BLOCK_2_DATA = { + 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1}; + static constexpr std::array BLOCK_3_TRAILER = { + 0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0x78, 0x77, 0x88, 0xC1, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; + static constexpr std::array NDEF_TRAILER = { + 0xD3, 0xF7, 0xD3, 0xF7, 0xD3, 0xF7, 0x7F, 0x07, 0x88, 0x40, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; if (this->auth_mifare_classic_block_(0, nfc::MIFARE_CMD_AUTH_B, nfc::DEFAULT_KEY) != nfc::STATUS_OK) { ESP_LOGE(TAG, "Unable to authenticate block 0 for formatting"); return nfc::STATUS_FAILED; } - if (this->write_mifare_classic_block_(1, block_1_data) != nfc::STATUS_OK) { + if (this->write_mifare_classic_block_(1, BLOCK_1_DATA.data(), BLOCK_1_DATA.size()) != nfc::STATUS_OK) { return nfc::STATUS_FAILED; } - if (this->write_mifare_classic_block_(2, block_2_data) != nfc::STATUS_OK) { + if (this->write_mifare_classic_block_(2, BLOCK_2_DATA.data(), BLOCK_2_DATA.size()) != nfc::STATUS_OK) { return nfc::STATUS_FAILED; } - if (this->write_mifare_classic_block_(3, block_3_trailer) != nfc::STATUS_OK) { + if (this->write_mifare_classic_block_(3, BLOCK_3_TRAILER.data(), BLOCK_3_TRAILER.size()) != nfc::STATUS_OK) { return nfc::STATUS_FAILED; } @@ -210,25 +211,26 @@ uint8_t PN7160::format_mifare_classic_ndef_() { return nfc::STATUS_FAILED; } if (block == 4) { - if (this->write_mifare_classic_block_(block, empty_ndef_message) != nfc::STATUS_OK) { + if (this->write_mifare_classic_block_(block, EMPTY_NDEF_MESSAGE.data(), EMPTY_NDEF_MESSAGE.size()) != + nfc::STATUS_OK) { ESP_LOGE(TAG, "Unable to write block %u", block); status = nfc::STATUS_FAILED; } } else { - if (this->write_mifare_classic_block_(block, blank_block) != nfc::STATUS_OK) { + if (this->write_mifare_classic_block_(block, BLANK_BLOCK.data(), BLANK_BLOCK.size()) != nfc::STATUS_OK) { ESP_LOGE(TAG, "Unable to write block %u", block); status = nfc::STATUS_FAILED; } } - if (this->write_mifare_classic_block_(block + 1, blank_block) != nfc::STATUS_OK) { + if (this->write_mifare_classic_block_(block + 1, BLANK_BLOCK.data(), BLANK_BLOCK.size()) != nfc::STATUS_OK) { ESP_LOGE(TAG, "Unable to write block %u", block + 1); status = nfc::STATUS_FAILED; } - if (this->write_mifare_classic_block_(block + 2, blank_block) != nfc::STATUS_OK) { + if (this->write_mifare_classic_block_(block + 2, BLANK_BLOCK.data(), BLANK_BLOCK.size()) != nfc::STATUS_OK) { ESP_LOGE(TAG, "Unable to write block %u", block + 2); status = nfc::STATUS_FAILED; } - if (this->write_mifare_classic_block_(block + 3, ndef_trailer) != nfc::STATUS_OK) { + if (this->write_mifare_classic_block_(block + 3, NDEF_TRAILER.data(), NDEF_TRAILER.size()) != nfc::STATUS_OK) { ESP_LOGE(TAG, "Unable to write trailer block %u", block + 3); status = nfc::STATUS_FAILED; } @@ -236,7 +238,7 @@ uint8_t PN7160::format_mifare_classic_ndef_() { return status; } -uint8_t PN7160::write_mifare_classic_block_(uint8_t block_num, std::vector &write_data) { +uint8_t PN7160::write_mifare_classic_block_(uint8_t block_num, const uint8_t *data, size_t len) { nfc::NciMessage rx; nfc::NciMessage tx(nfc::NCI_PKT_MT_DATA, {XCHG_DATA_OID, nfc::MIFARE_CMD_WRITE, block_num}); char buf[nfc::FORMAT_BYTES_BUFFER_SIZE]; @@ -248,7 +250,7 @@ uint8_t PN7160::write_mifare_classic_block_(uint8_t block_num, std::vectortransceive_(tx, rx, NFCC_TAG_WRITE_TIMEOUT) != nfc::STATUS_OK) { @@ -294,8 +296,8 @@ uint8_t PN7160::write_mifare_classic_tag_(const std::shared_ptr data(encoded.begin() + index, encoded.begin() + index + nfc::MIFARE_CLASSIC_BLOCK_SIZE); - if (this->write_mifare_classic_block_(current_block, data) != nfc::STATUS_OK) { + if (this->write_mifare_classic_block_(current_block, encoded.data() + index, nfc::MIFARE_CLASSIC_BLOCK_SIZE) != + nfc::STATUS_OK) { return nfc::STATUS_FAILED; } index += nfc::MIFARE_CLASSIC_BLOCK_SIZE; diff --git a/esphome/components/pn7160/pn7160_mifare_ultralight.cpp b/esphome/components/pn7160/pn7160_mifare_ultralight.cpp index c473ff48d9..9dc8d3dd2d 100644 --- a/esphome/components/pn7160/pn7160_mifare_ultralight.cpp +++ b/esphome/components/pn7160/pn7160_mifare_ultralight.cpp @@ -1,3 +1,4 @@ +#include #include #include @@ -144,8 +145,8 @@ uint8_t PN7160::write_mifare_ultralight_tag_(nfc::NfcTagUid &uid, const std::sha uint8_t current_page = nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE; while (index < buffer_length) { - std::vector data(encoded.begin() + index, encoded.begin() + index + nfc::MIFARE_ULTRALIGHT_PAGE_SIZE); - if (this->write_mifare_ultralight_page_(current_page, data) != nfc::STATUS_OK) { + if (this->write_mifare_ultralight_page_(current_page, encoded.data() + index, nfc::MIFARE_ULTRALIGHT_PAGE_SIZE) != + nfc::STATUS_OK) { return nfc::STATUS_FAILED; } index += nfc::MIFARE_ULTRALIGHT_PAGE_SIZE; @@ -158,19 +159,19 @@ uint8_t PN7160::clean_mifare_ultralight_() { uint32_t capacity = this->read_mifare_ultralight_capacity_(); uint8_t pages = (capacity / nfc::MIFARE_ULTRALIGHT_PAGE_SIZE) + nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE; - std::vector blank_data = {0x00, 0x00, 0x00, 0x00}; + static constexpr std::array BLANK_DATA = {0x00, 0x00, 0x00, 0x00}; for (int i = nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE; i < pages; i++) { - if (this->write_mifare_ultralight_page_(i, blank_data) != nfc::STATUS_OK) { + if (this->write_mifare_ultralight_page_(i, BLANK_DATA.data(), BLANK_DATA.size()) != nfc::STATUS_OK) { return nfc::STATUS_FAILED; } } return nfc::STATUS_OK; } -uint8_t PN7160::write_mifare_ultralight_page_(uint8_t page_num, std::vector &write_data) { +uint8_t PN7160::write_mifare_ultralight_page_(uint8_t page_num, const uint8_t *write_data, size_t len) { std::vector payload = {nfc::MIFARE_CMD_WRITE_ULTRALIGHT, page_num}; - payload.insert(payload.end(), write_data.begin(), write_data.end()); + payload.insert(payload.end(), write_data, write_data + len); nfc::NciMessage rx; nfc::NciMessage tx(nfc::NCI_PKT_MT_DATA, payload); From 931b47673c49e3f29324290b52d591aacab5faed Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 13 Feb 2026 16:22:26 -0600 Subject: [PATCH 022/197] Bump github/codeql-action from 4.32.2 to 4.32.3 (#13981) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/codeql.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 51ea4085e0..376825bad6 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -58,7 +58,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v4.32.2 + uses: github/codeql-action/init@9e907b5e64f6b83e7804b09294d44122997950d6 # v4.32.3 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} @@ -86,6 +86,6 @@ jobs: exit 1 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v4.32.2 + uses: github/codeql-action/analyze@9e907b5e64f6b83e7804b09294d44122997950d6 # v4.32.3 with: category: "/language:${{matrix.language}}" From 7c70b2e04ed80804eb38feac3b4c39abcbc61409 Mon Sep 17 00:00:00 2001 From: Guillermo Ruffino Date: Thu, 12 Feb 2026 13:23:59 -0300 Subject: [PATCH 023/197] [schema-gen] fix Windows: ensure UTF-8 encoding when reading component files (#13952) --- script/build_language_schema.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/build_language_schema.py b/script/build_language_schema.py index c9501cb193..bea540dc63 100755 --- a/script/build_language_schema.py +++ b/script/build_language_schema.py @@ -369,7 +369,7 @@ def get_logger_tags(): "api.service", ] for file in CORE_COMPONENTS_PATH.rglob("*.cpp"): - data = file.read_text() + data = file.read_text(encoding="utf-8") match = pattern.search(data) if match: tags.append(match.group(1)) From 844210519a4a662c76e159a45c7bf09760053254 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 12 Feb 2026 10:26:10 -0600 Subject: [PATCH 024/197] [uart] Remove redundant mutex, fix flush race, conditional event queue (#13955) --- .../uart/uart_component_esp_idf.cpp | 49 ++++++++++--------- .../components/uart/uart_component_esp_idf.h | 17 +++++-- 2 files changed, 41 insertions(+), 25 deletions(-) diff --git a/esphome/components/uart/uart_component_esp_idf.cpp b/esphome/components/uart/uart_component_esp_idf.cpp index 19b9a4077f..6c242220a6 100644 --- a/esphome/components/uart/uart_component_esp_idf.cpp +++ b/esphome/components/uart/uart_component_esp_idf.cpp @@ -90,7 +90,6 @@ void IDFUARTComponent::setup() { return; } this->uart_num_ = static_cast(next_uart_num++); - this->lock_ = xSemaphoreCreateMutex(); #if (SOC_UART_LP_NUM >= 1) size_t fifo_len = ((this->uart_num_ < SOC_UART_HP_NUM) ? SOC_UART_FIFO_LEN : SOC_LP_UART_FIFO_LEN); @@ -102,11 +101,7 @@ void IDFUARTComponent::setup() { this->rx_buffer_size_ = fifo_len * 2; } - xSemaphoreTake(this->lock_, portMAX_DELAY); - this->load_settings(false); - - xSemaphoreGive(this->lock_); } void IDFUARTComponent::load_settings(bool dump_config) { @@ -126,13 +121,20 @@ void IDFUARTComponent::load_settings(bool dump_config) { return; } } +#ifdef USE_UART_WAKE_LOOP_ON_RX + constexpr int event_queue_size = 20; + QueueHandle_t *event_queue_ptr = &this->uart_event_queue_; +#else + constexpr int event_queue_size = 0; + QueueHandle_t *event_queue_ptr = nullptr; +#endif err = uart_driver_install(this->uart_num_, // UART number this->rx_buffer_size_, // RX ring buffer size - 0, // TX ring buffer size. If zero, driver will not use a TX buffer and TX function will - // block task until all data has been sent out - 20, // event queue size/depth - &this->uart_event_queue_, // event queue - 0 // Flags used to allocate the interrupt + 0, // TX ring buffer size. If zero, driver will not use a TX buffer and TX function will + // block task until all data has been sent out + event_queue_size, // event queue size/depth + event_queue_ptr, // event queue + 0 // Flags used to allocate the interrupt ); if (err != ESP_OK) { ESP_LOGW(TAG, "uart_driver_install failed: %s", esp_err_to_name(err)); @@ -282,9 +284,7 @@ void IDFUARTComponent::set_rx_timeout(size_t rx_timeout) { } void IDFUARTComponent::write_array(const uint8_t *data, size_t len) { - xSemaphoreTake(this->lock_, portMAX_DELAY); int32_t write_len = uart_write_bytes(this->uart_num_, data, len); - xSemaphoreGive(this->lock_); if (write_len != (int32_t) len) { ESP_LOGW(TAG, "uart_write_bytes failed: %d != %zu", write_len, len); this->mark_failed(); @@ -299,7 +299,6 @@ void IDFUARTComponent::write_array(const uint8_t *data, size_t len) { bool IDFUARTComponent::peek_byte(uint8_t *data) { if (!this->check_read_timeout_()) return false; - xSemaphoreTake(this->lock_, portMAX_DELAY); if (this->has_peek_) { *data = this->peek_byte_; } else { @@ -311,7 +310,6 @@ bool IDFUARTComponent::peek_byte(uint8_t *data) { this->peek_byte_ = *data; } } - xSemaphoreGive(this->lock_); return true; } @@ -320,7 +318,6 @@ bool IDFUARTComponent::read_array(uint8_t *data, size_t len) { int32_t read_len = 0; if (!this->check_read_timeout_(len)) return false; - xSemaphoreTake(this->lock_, portMAX_DELAY); if (this->has_peek_) { length_to_read--; *data = this->peek_byte_; @@ -329,7 +326,6 @@ bool IDFUARTComponent::read_array(uint8_t *data, size_t len) { } if (length_to_read > 0) read_len = uart_read_bytes(this->uart_num_, data, length_to_read, 20 / portTICK_PERIOD_MS); - xSemaphoreGive(this->lock_); #ifdef USE_UART_DEBUGGER for (size_t i = 0; i < len; i++) { this->debug_callback_.call(UART_DIRECTION_RX, data[i]); @@ -342,9 +338,7 @@ size_t IDFUARTComponent::available() { size_t available = 0; esp_err_t err; - xSemaphoreTake(this->lock_, portMAX_DELAY); err = uart_get_buffered_data_len(this->uart_num_, &available); - xSemaphoreGive(this->lock_); if (err != ESP_OK) { ESP_LOGW(TAG, "uart_get_buffered_data_len failed: %s", esp_err_to_name(err)); @@ -358,9 +352,7 @@ size_t IDFUARTComponent::available() { void IDFUARTComponent::flush() { ESP_LOGVV(TAG, " Flushing"); - xSemaphoreTake(this->lock_, portMAX_DELAY); uart_wait_tx_done(this->uart_num_, portMAX_DELAY); - xSemaphoreGive(this->lock_); } void IDFUARTComponent::check_logger_conflict() {} @@ -384,6 +376,13 @@ void IDFUARTComponent::start_rx_event_task_() { ESP_LOGV(TAG, "RX event task started"); } +// FreeRTOS task that relays UART ISR events to the main loop. +// This task exists because wake_loop_threadsafe() is not ISR-safe (it uses a +// UDP loopback socket), so we need a task as an ISR-to-main-loop trampoline. +// IMPORTANT: This task must NOT call any UART wrapper methods (read_array, +// write_array, peek_byte, etc.) or touch has_peek_/peek_byte_ — all reading +// is done by the main loop. This task only reads from the event queue and +// calls App.wake_loop_threadsafe(). void IDFUARTComponent::rx_event_task_func(void *param) { auto *self = static_cast(param); uart_event_t event; @@ -405,8 +404,14 @@ void IDFUARTComponent::rx_event_task_func(void *param) { case UART_FIFO_OVF: case UART_BUFFER_FULL: - ESP_LOGW(TAG, "FIFO overflow or ring buffer full - clearing"); - uart_flush_input(self->uart_num_); + // Don't call uart_flush_input() here — this task does not own the read side. + // ESP-IDF examples flush on overflow because the same task handles both events + // and reads, so flush and read are serialized. Here, reads happen on the main + // loop, so flushing from this task races with read_array() and can destroy data + // mid-read. The driver self-heals without an explicit flush: uart_read_bytes() + // calls uart_check_buf_full() after each chunk, which moves stashed FIFO bytes + // into the ring buffer and re-enables RX interrupts once space is freed. + ESP_LOGW(TAG, "FIFO overflow or ring buffer full"); #if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) App.wake_loop_threadsafe(); #endif diff --git a/esphome/components/uart/uart_component_esp_idf.h b/esphome/components/uart/uart_component_esp_idf.h index 1ecb02d7ab..1517eab509 100644 --- a/esphome/components/uart/uart_component_esp_idf.h +++ b/esphome/components/uart/uart_component_esp_idf.h @@ -8,6 +8,13 @@ namespace esphome::uart { +/// ESP-IDF UART driver wrapper. +/// +/// Thread safety: All public methods must only be called from the main loop. +/// The ESP-IDF UART driver API does not guarantee thread safety, and ESPHome's +/// peek byte state (has_peek_/peek_byte_) is not synchronized. The rx_event_task +/// (when enabled) must not call any of these methods — it communicates with the +/// main loop exclusively via App.wake_loop_threadsafe(). class IDFUARTComponent : public UARTComponent, public Component { public: void setup() override; @@ -26,7 +33,9 @@ class IDFUARTComponent : public UARTComponent, public Component { void flush() override; uint8_t get_hw_serial_number() { return this->uart_num_; } +#ifdef USE_UART_WAKE_LOOP_ON_RX QueueHandle_t *get_uart_event_queue() { return &this->uart_event_queue_; } +#endif /** * Load the UART with the current settings. @@ -46,18 +55,20 @@ class IDFUARTComponent : public UARTComponent, public Component { protected: void check_logger_conflict() override; uart_port_t uart_num_; - QueueHandle_t uart_event_queue_; uart_config_t get_config_(); - SemaphoreHandle_t lock_; bool has_peek_{false}; uint8_t peek_byte_; #ifdef USE_UART_WAKE_LOOP_ON_RX - // RX notification support + // RX notification support — runs on a separate FreeRTOS task. + // IMPORTANT: rx_event_task_func must NOT call any UART wrapper methods (read_array, + // write_array, etc.) or touch has_peek_/peek_byte_. It must only read from the + // event queue and call App.wake_loop_threadsafe(). void start_rx_event_task_(); static void rx_event_task_func(void *param); + QueueHandle_t uart_event_queue_; TaskHandle_t rx_event_task_handle_{nullptr}; #endif // USE_UART_WAKE_LOOP_ON_RX }; From ead7937dbf74a26b92c9c2dbf14b0d875356edcd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 12 Feb 2026 11:04:23 -0600 Subject: [PATCH 025/197] [api] Extract cold code from APIServer::loop() hot path (#13902) --- esphome/components/api/api_server.cpp | 130 ++++++++++++++------------ esphome/components/api/api_server.h | 5 + 2 files changed, 74 insertions(+), 61 deletions(-) diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 53b41a5c14..5503cf4db8 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -117,37 +117,7 @@ void APIServer::setup() { void APIServer::loop() { // Accept new clients only if the socket exists and has incoming connections if (this->socket_ && this->socket_->ready()) { - while (true) { - struct sockaddr_storage source_addr; - socklen_t addr_len = sizeof(source_addr); - - auto sock = this->socket_->accept_loop_monitored((struct sockaddr *) &source_addr, &addr_len); - if (!sock) - break; - - char peername[socket::SOCKADDR_STR_LEN]; - sock->getpeername_to(peername); - - // Check if we're at the connection limit - if (this->clients_.size() >= this->max_connections_) { - ESP_LOGW(TAG, "Max connections (%d), rejecting %s", this->max_connections_, peername); - // Immediately close - socket destructor will handle cleanup - sock.reset(); - continue; - } - - ESP_LOGD(TAG, "Accept %s", peername); - - auto *conn = new APIConnection(std::move(sock), this); - this->clients_.emplace_back(conn); - conn->start(); - - // First client connected - clear warning and update timestamp - if (this->clients_.size() == 1 && this->reboot_timeout_ != 0) { - this->status_clear_warning(); - this->last_connected_ = App.get_loop_component_start_time(); - } - } + this->accept_new_connections_(); } if (this->clients_.empty()) { @@ -178,46 +148,84 @@ void APIServer::loop() { while (client_index < this->clients_.size()) { auto &client = this->clients_[client_index]; - if (!client->flags_.remove) { + if (client->flags_.remove) { + // Rare case: handle disconnection (don't increment - swapped element needs processing) + this->remove_client_(client_index); + } else { // Common case: process active client client->loop(); client_index++; + } + } +} + +void APIServer::remove_client_(size_t client_index) { + auto &client = this->clients_[client_index]; + +#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES + this->unregister_active_action_calls_for_connection(client.get()); +#endif + ESP_LOGV(TAG, "Remove connection %s", client->get_name()); + +#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER + // Save client info before closing socket and removal for the trigger + char peername_buf[socket::SOCKADDR_STR_LEN]; + std::string client_name(client->get_name()); + std::string client_peername(client->get_peername_to(peername_buf)); +#endif + + // Close socket now (was deferred from on_fatal_error to allow getpeername) + client->helper_->close(); + + // Swap with the last element and pop (avoids expensive vector shifts) + if (client_index < this->clients_.size() - 1) { + std::swap(this->clients_[client_index], this->clients_.back()); + } + this->clients_.pop_back(); + + // Last client disconnected - set warning and start tracking for reboot timeout + if (this->clients_.empty() && this->reboot_timeout_ != 0) { + this->status_set_warning(); + this->last_connected_ = App.get_loop_component_start_time(); + } + +#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER + // Fire trigger after client is removed so api.connected reflects the true state + this->client_disconnected_trigger_.trigger(client_name, client_peername); +#endif +} + +void APIServer::accept_new_connections_() { + while (true) { + struct sockaddr_storage source_addr; + socklen_t addr_len = sizeof(source_addr); + + auto sock = this->socket_->accept_loop_monitored((struct sockaddr *) &source_addr, &addr_len); + if (!sock) + break; + + char peername[socket::SOCKADDR_STR_LEN]; + sock->getpeername_to(peername); + + // Check if we're at the connection limit + if (this->clients_.size() >= this->max_connections_) { + ESP_LOGW(TAG, "Max connections (%d), rejecting %s", this->max_connections_, peername); + // Immediately close - socket destructor will handle cleanup + sock.reset(); continue; } - // Rare case: handle disconnection -#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES - this->unregister_active_action_calls_for_connection(client.get()); -#endif - ESP_LOGV(TAG, "Remove connection %s", client->get_name()); + ESP_LOGD(TAG, "Accept %s", peername); -#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER - // Save client info before closing socket and removal for the trigger - char peername_buf[socket::SOCKADDR_STR_LEN]; - std::string client_name(client->get_name()); - std::string client_peername(client->get_peername_to(peername_buf)); -#endif + auto *conn = new APIConnection(std::move(sock), this); + this->clients_.emplace_back(conn); + conn->start(); - // Close socket now (was deferred from on_fatal_error to allow getpeername) - client->helper_->close(); - - // Swap with the last element and pop (avoids expensive vector shifts) - if (client_index < this->clients_.size() - 1) { - std::swap(this->clients_[client_index], this->clients_.back()); - } - this->clients_.pop_back(); - - // Last client disconnected - set warning and start tracking for reboot timeout - if (this->clients_.empty() && this->reboot_timeout_ != 0) { - this->status_set_warning(); + // First client connected - clear warning and update timestamp + if (this->clients_.size() == 1 && this->reboot_timeout_ != 0) { + this->status_clear_warning(); this->last_connected_ = App.get_loop_component_start_time(); } - -#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER - // Fire trigger after client is removed so api.connected reflects the true state - this->client_disconnected_trigger_.trigger(client_name, client_peername); -#endif - // Don't increment client_index since we need to process the swapped element } } diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index 6ab3cdc576..28f60343e0 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -234,6 +234,11 @@ class APIServer : public Component, #endif protected: + // Accept incoming socket connections. Only called when socket has pending connections. + void __attribute__((noinline)) accept_new_connections_(); + // Remove a disconnected client by index. Swaps with last element and pops. + void __attribute__((noinline)) remove_client_(size_t client_index); + #ifdef USE_API_NOISE bool update_noise_psk_(const SavedNoisePsk &new_psk, const LogString *save_log_msg, const LogString *fail_log_msg, const psk_t &active_psk, bool make_active); From e9bf9bc691196e4fff13a2c0d866c1e588968f50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Ma=C5=88as?= Date: Thu, 12 Feb 2026 18:20:54 +0100 Subject: [PATCH 026/197] [pulse_meter] Fix early edge detection (#12360) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- .../pulse_meter/pulse_meter_sensor.cpp | 75 ++++++++++--------- .../pulse_meter/pulse_meter_sensor.h | 11 ++- 2 files changed, 44 insertions(+), 42 deletions(-) diff --git a/esphome/components/pulse_meter/pulse_meter_sensor.cpp b/esphome/components/pulse_meter/pulse_meter_sensor.cpp index 007deb66e5..433e1f0b7e 100644 --- a/esphome/components/pulse_meter/pulse_meter_sensor.cpp +++ b/esphome/components/pulse_meter/pulse_meter_sensor.cpp @@ -38,8 +38,7 @@ void PulseMeterSensor::setup() { } void PulseMeterSensor::loop() { - // Reset the count in get before we pass it back to the ISR as set - this->get_->count_ = 0; + State state; { // Lock the interrupt so the interrupt code doesn't interfere with itself @@ -58,31 +57,35 @@ void PulseMeterSensor::loop() { } this->last_pin_val_ = current; - // Swap out set and get to get the latest state from the ISR - std::swap(this->set_, this->get_); + // Get the latest state from the ISR and reset the count in the ISR + state.last_detected_edge_us_ = this->state_.last_detected_edge_us_; + state.last_rising_edge_us_ = this->state_.last_rising_edge_us_; + state.count_ = this->state_.count_; + this->state_.count_ = 0; } const uint32_t now = micros(); // If an edge was peeked, repay the debt - if (this->peeked_edge_ && this->get_->count_ > 0) { + if (this->peeked_edge_ && state.count_ > 0) { this->peeked_edge_ = false; - this->get_->count_--; // NOLINT(clang-diagnostic-deprecated-volatile) + state.count_--; } - // If there is an unprocessed edge, and filter_us_ has passed since, count this edge early - if (this->get_->last_rising_edge_us_ != this->get_->last_detected_edge_us_ && - now - this->get_->last_rising_edge_us_ >= this->filter_us_) { + // If there is an unprocessed edge, and filter_us_ has passed since, count this edge early. + // Wait for the debt to be repaid before counting another unprocessed edge early. + if (!this->peeked_edge_ && state.last_rising_edge_us_ != state.last_detected_edge_us_ && + now - state.last_rising_edge_us_ >= this->filter_us_) { this->peeked_edge_ = true; - this->get_->last_detected_edge_us_ = this->get_->last_rising_edge_us_; - this->get_->count_++; // NOLINT(clang-diagnostic-deprecated-volatile) + state.last_detected_edge_us_ = state.last_rising_edge_us_; + state.count_++; } // Check if we detected a pulse this loop - if (this->get_->count_ > 0) { + if (state.count_ > 0) { // Keep a running total of pulses if a total sensor is configured if (this->total_sensor_ != nullptr) { - this->total_pulses_ += this->get_->count_; + this->total_pulses_ += state.count_; const uint32_t total = this->total_pulses_; this->total_sensor_->publish_state(total); } @@ -94,15 +97,15 @@ void PulseMeterSensor::loop() { this->meter_state_ = MeterState::RUNNING; } break; case MeterState::RUNNING: { - uint32_t delta_us = this->get_->last_detected_edge_us_ - this->last_processed_edge_us_; - float pulse_width_us = delta_us / float(this->get_->count_); - ESP_LOGV(TAG, "New pulse, delta: %" PRIu32 " µs, count: %" PRIu32 ", width: %.5f µs", delta_us, - this->get_->count_, pulse_width_us); + uint32_t delta_us = state.last_detected_edge_us_ - this->last_processed_edge_us_; + float pulse_width_us = delta_us / float(state.count_); + ESP_LOGV(TAG, "New pulse, delta: %" PRIu32 " µs, count: %" PRIu32 ", width: %.5f µs", delta_us, state.count_, + pulse_width_us); this->publish_state((60.0f * 1000000.0f) / pulse_width_us); } break; } - this->last_processed_edge_us_ = this->get_->last_detected_edge_us_; + this->last_processed_edge_us_ = state.last_detected_edge_us_; } // No detected edges this loop else { @@ -141,14 +144,14 @@ void IRAM_ATTR PulseMeterSensor::edge_intr(PulseMeterSensor *sensor) { // This is an interrupt handler - we can't call any virtual method from this method // Get the current time before we do anything else so the measurements are consistent const uint32_t now = micros(); - auto &state = sensor->edge_state_; - auto &set = *sensor->set_; + auto &edge_state = sensor->edge_state_; + auto &state = sensor->state_; - if ((now - state.last_sent_edge_us_) >= sensor->filter_us_) { - state.last_sent_edge_us_ = now; - set.last_detected_edge_us_ = now; - set.last_rising_edge_us_ = now; - set.count_++; // NOLINT(clang-diagnostic-deprecated-volatile) + if ((now - edge_state.last_sent_edge_us_) >= sensor->filter_us_) { + edge_state.last_sent_edge_us_ = now; + state.last_detected_edge_us_ = now; + state.last_rising_edge_us_ = now; + state.count_++; // NOLINT(clang-diagnostic-deprecated-volatile) } // This ISR is bound to rising edges, so the pin is high @@ -160,26 +163,26 @@ void IRAM_ATTR PulseMeterSensor::pulse_intr(PulseMeterSensor *sensor) { // Get the current time before we do anything else so the measurements are consistent const uint32_t now = micros(); const bool pin_val = sensor->isr_pin_.digital_read(); - auto &state = sensor->pulse_state_; - auto &set = *sensor->set_; + auto &pulse_state = sensor->pulse_state_; + auto &state = sensor->state_; // Filter length has passed since the last interrupt - const bool length = now - state.last_intr_ >= sensor->filter_us_; + const bool length = now - pulse_state.last_intr_ >= sensor->filter_us_; - if (length && state.latched_ && !sensor->last_pin_val_) { // Long enough low edge - state.latched_ = false; - } else if (length && !state.latched_ && sensor->last_pin_val_) { // Long enough high edge - state.latched_ = true; - set.last_detected_edge_us_ = state.last_intr_; - set.count_++; // NOLINT(clang-diagnostic-deprecated-volatile) + if (length && pulse_state.latched_ && !sensor->last_pin_val_) { // Long enough low edge + pulse_state.latched_ = false; + } else if (length && !pulse_state.latched_ && sensor->last_pin_val_) { // Long enough high edge + pulse_state.latched_ = true; + state.last_detected_edge_us_ = pulse_state.last_intr_; + state.count_++; // NOLINT(clang-diagnostic-deprecated-volatile) } // Due to order of operations this includes // length && latched && rising (just reset from a long low edge) // !latched && (rising || high) (noise on the line resetting the potential rising edge) - set.last_rising_edge_us_ = !state.latched_ && pin_val ? now : set.last_detected_edge_us_; + state.last_rising_edge_us_ = !pulse_state.latched_ && pin_val ? now : state.last_detected_edge_us_; - state.last_intr_ = now; + pulse_state.last_intr_ = now; sensor->last_pin_val_ = pin_val; } diff --git a/esphome/components/pulse_meter/pulse_meter_sensor.h b/esphome/components/pulse_meter/pulse_meter_sensor.h index 5800c4ec42..e46f1e615f 100644 --- a/esphome/components/pulse_meter/pulse_meter_sensor.h +++ b/esphome/components/pulse_meter/pulse_meter_sensor.h @@ -46,17 +46,16 @@ class PulseMeterSensor : public sensor::Sensor, public Component { uint32_t total_pulses_ = 0; uint32_t last_processed_edge_us_ = 0; - // This struct (and the two pointers) are used to pass data between the ISR and loop. - // These two pointers are exchanged each loop. - // Use these to send data from the ISR to the loop not the other way around (except for resetting the values). + // This struct and variable are used to pass data between the ISR and loop. + // The data from state_ is read and then count_ in state_ is reset in each loop. + // This must be done while guarded by an InterruptLock. Use this variable to send data + // from the ISR to the loop not the other way around (except for resetting count_). struct State { uint32_t last_detected_edge_us_ = 0; uint32_t last_rising_edge_us_ = 0; uint32_t count_ = 0; }; - State state_[2]; - volatile State *set_ = state_; - volatile State *get_ = state_ + 1; + volatile State state_{}; // Only use the following variables in the ISR or while guarded by an InterruptLock ISRInternalGPIOPin isr_pin_; From c08356b0c1d973963cf7d615849b45184b47e4ed Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 12 Feb 2026 16:04:39 -0500 Subject: [PATCH 027/197] [alarm_control_panel] Fix flaky integration test race condition (#13964) Co-authored-by: Claude Opus 4.6 --- ...t_alarm_control_panel_state_transitions.py | 25 +++++++++++++++---- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/tests/integration/test_alarm_control_panel_state_transitions.py b/tests/integration/test_alarm_control_panel_state_transitions.py index 09348f5bea..0b07710961 100644 --- a/tests/integration/test_alarm_control_panel_state_transitions.py +++ b/tests/integration/test_alarm_control_panel_state_transitions.py @@ -270,6 +270,14 @@ async def test_alarm_control_panel_state_transitions( # The chime_sensor has chime: true, so opening it while disarmed # should trigger on_chime callback + # Set up future for the on_ready from opening the chime sensor + # (alarm becomes "not ready" when chime sensor opens). + # We must wait for this BEFORE creating the close future, otherwise + # the open event's log can arrive late and resolve the close future, + # causing the test to proceed before the chime close is processed. + ready_after_chime_open: asyncio.Future[bool] = loop.create_future() + ready_futures.append(ready_after_chime_open) + # We're currently DISARMED - open the chime sensor client.switch_command(chime_switch_info.key, True) @@ -279,11 +287,18 @@ async def test_alarm_control_panel_state_transitions( except TimeoutError: pytest.fail(f"on_chime callback not fired. Log lines: {log_lines[-20:]}") - # Close the chime sensor and wait for alarm to become ready again - # We need to wait for this transition before testing door sensor, - # otherwise there's a race where the door sensor state change could - # arrive before the chime sensor state change, leaving the alarm in - # a continuous "not ready" state with no on_ready callback fired. + # Wait for the on_ready from the chime sensor opening + try: + await asyncio.wait_for(ready_after_chime_open, timeout=2.0) + except TimeoutError: + pytest.fail( + f"on_ready callback not fired when chime sensor opened. " + f"Log lines: {log_lines[-20:]}" + ) + + # Now create the future for the close event and close the sensor. + # Since we waited for the open event above, the close event's + # on_ready log cannot be confused with the open event's. ready_after_chime_close: asyncio.Future[bool] = loop.create_future() ready_futures.append(ready_after_chime_close) From 297dfb0db41174bdb786e419e8100f0c30233d23 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 12 Feb 2026 16:12:17 -0500 Subject: [PATCH 028/197] [docker] Suppress git detached HEAD advice (#13962) Co-authored-by: Claude Opus 4.6 --- docker/Dockerfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 8ebdd1e49b..540d28be7f 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -9,7 +9,8 @@ FROM ghcr.io/esphome/docker-base:${BUILD_OS}-ha-addon-${BUILD_BASE_VERSION} AS b ARG BUILD_TYPE FROM base-source-${BUILD_TYPE} AS base -RUN git config --system --add safe.directory "*" +RUN git config --system --add safe.directory "*" \ + && git config --system advice.detachedHead false # Install build tools for Python packages that require compilation # (e.g., ruamel.yaml.clibz used by ESP-IDF's idf-component-manager) From f6aeef2e68f1c891f401f80c5886215baf811892 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 12 Feb 2026 18:20:58 -0600 Subject: [PATCH 029/197] [api] Fix ESP8266 noise API handshake deadlock and prompt socket cleanup (#13972) --- esphome/components/api/api_frame_helper_noise.cpp | 10 ++++++---- esphome/components/api/api_server.cpp | 8 ++++++-- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/esphome/components/api/api_frame_helper_noise.cpp b/esphome/components/api/api_frame_helper_noise.cpp index c1641b398a..1ae848dead 100644 --- a/esphome/components/api/api_frame_helper_noise.cpp +++ b/esphome/components/api/api_frame_helper_noise.cpp @@ -138,10 +138,12 @@ APIError APINoiseFrameHelper::handle_noise_error_(int err, const LogString *func /// Run through handshake messages (if in that phase) APIError APINoiseFrameHelper::loop() { - // During handshake phase, process as many actions as possible until we can't progress - // socket_->ready() stays true until next main loop, but state_action() will return - // WOULD_BLOCK when no more data is available to read - while (state_ != State::DATA && this->socket_->ready()) { + // Cache ready() outside the loop. On ESP8266 LWIP raw TCP, ready() returns false once + // the rx buffer is consumed. Re-checking each iteration would block handshake writes + // that must follow reads, deadlocking the handshake. state_action() will return + // WOULD_BLOCK when no more data is available to read. + bool socket_ready = this->socket_->ready(); + while (state_ != State::DATA && socket_ready) { APIError err = state_action_(); if (err == APIError::WOULD_BLOCK) { break; diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 5503cf4db8..28128d39bc 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -148,12 +148,16 @@ void APIServer::loop() { while (client_index < this->clients_.size()) { auto &client = this->clients_[client_index]; + // Common case: process active client + if (!client->flags_.remove) { + client->loop(); + } + // Handle disconnection promptly - close socket to free LWIP PCB + // resources and prevent retransmit crashes on ESP8266. if (client->flags_.remove) { // Rare case: handle disconnection (don't increment - swapped element needs processing) this->remove_client_(client_index); } else { - // Common case: process active client - client->loop(); client_index++; } } From a8a324cbfbdcb2b41de9de3a544230bf372a104a Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Sat, 14 Feb 2026 13:53:54 +1300 Subject: [PATCH 030/197] Bump version to 2026.2.0b2 --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index 16516a387f..572e20a694 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 = 2026.2.0b1 +PROJECT_NUMBER = 2026.2.0b2 # 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 3b5cccfb25..247b2b7e4e 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2026.2.0b1" +__version__ = "2026.2.0b2" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 36776b40c2010b6635c9bad297e4985c9db4cbcb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 14 Feb 2026 08:21:04 -0700 Subject: [PATCH 031/197] [wifi] Fix ESP8266 DHCP state corruption from premature dhcp_renew() (#13983) Co-authored-by: Claude Opus 4.6 --- .../components/wifi/wifi_component_esp8266.cpp | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index c87345f0bf..cbf7d7d80f 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -216,23 +216,16 @@ bool WiFiComponent::wifi_apply_hostname_() { ESP_LOGV(TAG, "Set hostname failed"); } - // inform dhcp server of hostname change using dhcp_renew() + // Update hostname on all lwIP interfaces so DHCP packets include it. + // lwIP includes the hostname in DHCP DISCOVER/REQUEST automatically + // via LWIP_NETIF_HOSTNAME — no dhcp_renew() needed. The hostname is + // fixed at compile time and never changes at runtime. for (netif *intf = netif_list; intf; intf = intf->next) { - // unconditionally update all known interfaces #if LWIP_VERSION_MAJOR == 1 intf->hostname = (char *) wifi_station_get_hostname(); #else intf->hostname = wifi_station_get_hostname(); #endif - if (netif_dhcp_data(intf) != nullptr) { - // renew already started DHCP leases - err_t lwipret = dhcp_renew(intf); - if (lwipret != ERR_OK) { - ESP_LOGW(TAG, "wifi_apply_hostname_(%s): lwIP error %d on interface %c%c (index %d)", intf->hostname, - (int) lwipret, intf->name[0], intf->name[1], intf->num); - ret = false; - } - } } return ret; From 5a6d64814a3a5f51f6e238620938416523947e94 Mon Sep 17 00:00:00 2001 From: AndreKR Date: Sat, 14 Feb 2026 18:08:26 +0100 Subject: [PATCH 032/197] [http_request] Improve TLS logging on ESP8266 (#13985) --- .../http_request/http_request_arduino.cpp | 44 ++++++++++++++++++- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/esphome/components/http_request/http_request_arduino.cpp b/esphome/components/http_request/http_request_arduino.cpp index aee1f651bf..e5b919e380 100644 --- a/esphome/components/http_request/http_request_arduino.cpp +++ b/esphome/components/http_request/http_request_arduino.cpp @@ -9,9 +9,20 @@ #include "esphome/core/defines.h" #include "esphome/core/log.h" +// Include BearSSL error constants for TLS failure diagnostics +#ifdef USE_ESP8266 +#include +#endif + namespace esphome::http_request { static const char *const TAG = "http_request.arduino"; +#ifdef USE_ESP8266 +static constexpr int RX_BUFFER_SIZE = 512; +static constexpr int TX_BUFFER_SIZE = 512; +// ESP8266 Arduino core (WiFiClientSecureBearSSL.cpp) returns -1000 on OOM +static constexpr int ESP8266_SSL_ERR_OOM = -1000; +#endif std::shared_ptr HttpRequestArduino::perform(const std::string &url, const std::string &method, const std::string &body, @@ -47,7 +58,7 @@ std::shared_ptr HttpRequestArduino::perform(const std::string &ur ESP_LOGV(TAG, "ESP8266 HTTPS connection with WiFiClientSecure"); stream_ptr = std::make_unique(); WiFiClientSecure *secure_client = static_cast(stream_ptr.get()); - secure_client->setBufferSizes(512, 512); + secure_client->setBufferSizes(RX_BUFFER_SIZE, TX_BUFFER_SIZE); secure_client->setInsecure(); } else { stream_ptr = std::make_unique(); @@ -107,13 +118,42 @@ std::shared_ptr HttpRequestArduino::perform(const std::string &ur container->status_code = container->client_.sendRequest(method.c_str(), body.c_str()); App.feed_wdt(); if (container->status_code < 0) { +#if defined(USE_ESP8266) && defined(USE_HTTP_REQUEST_ESP8266_HTTPS) + if (secure) { + WiFiClientSecure *secure_client = static_cast(stream_ptr.get()); + int last_error = secure_client->getLastSSLError(); + + if (last_error != 0) { + const LogString *error_msg; + switch (last_error) { + case ESP8266_SSL_ERR_OOM: + error_msg = LOG_STR("Unable to allocate buffer memory"); + break; + case BR_ERR_TOO_LARGE: + error_msg = LOG_STR("Incoming TLS record does not fit in receive buffer (BR_ERR_TOO_LARGE)"); + break; + default: + error_msg = LOG_STR("Unknown SSL error"); + break; + } + ESP_LOGW(TAG, "SSL failure: %s (Code: %d)", LOG_STR_ARG(error_msg), last_error); + if (last_error == ESP8266_SSL_ERR_OOM) { + ESP_LOGW(TAG, "Heap free: %u bytes, configured buffer sizes: %u bytes", ESP.getFreeHeap(), + static_cast(RX_BUFFER_SIZE + TX_BUFFER_SIZE)); + } + } else { + ESP_LOGW(TAG, "Connection failure with no error code"); + } + } +#endif + ESP_LOGW(TAG, "HTTP Request failed; URL: %s; Error: %s", url.c_str(), HTTPClient::errorToString(container->status_code).c_str()); + this->status_momentary_error("failed", 1000); container->end(); return nullptr; } - if (!is_success(container->status_code)) { ESP_LOGE(TAG, "HTTP Request failed; URL: %s; Code: %d", url.c_str(), container->status_code); this->status_momentary_error("failed", 1000); From 38404b2013f031c921ac43cbec4f3d4b898deccf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 14 Feb 2026 15:11:17 -0700 Subject: [PATCH 033/197] Bump ruff from 0.15.0 to 0.15.1 (#13980) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston --- .pre-commit-config.yaml | 2 +- requirements_test.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 991e053d5a..6d89060b0d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,7 +11,7 @@ ci: repos: - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.15.0 + rev: v0.15.1 hooks: # Run the linter. - id: ruff diff --git a/requirements_test.txt b/requirements_test.txt index 2cf6f6456e..9e99855f6f 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,6 +1,6 @@ pylint==4.0.4 flake8==7.3.0 # also change in .pre-commit-config.yaml when updating -ruff==0.15.0 # also change in .pre-commit-config.yaml when updating +ruff==0.15.1 # also change in .pre-commit-config.yaml when updating pyupgrade==3.21.2 # also change in .pre-commit-config.yaml when updating pre-commit From f48c8a64443e56e06f90271cbfd8d0e67116093d Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Sun, 15 Feb 2026 12:11:36 -0500 Subject: [PATCH 034/197] [combination] Fix 'coeffecient' typo with backward-compatible deprecation (#14004) Co-authored-by: Claude Opus 4.6 --- .../components/combination/combination.cpp | 2 +- esphome/components/combination/sensor.py | 46 +++++++++++++++---- tests/components/combination/common.yaml | 4 +- 3 files changed, 40 insertions(+), 12 deletions(-) diff --git a/esphome/components/combination/combination.cpp b/esphome/components/combination/combination.cpp index 716d270390..ece7cca482 100644 --- a/esphome/components/combination/combination.cpp +++ b/esphome/components/combination/combination.cpp @@ -126,7 +126,7 @@ void LinearCombinationComponent::setup() { } void LinearCombinationComponent::handle_new_value(float value) { - // Multiplies each sensor state by a configured coeffecient and then sums + // Multiplies each sensor state by a configured coefficient and then sums if (!std::isfinite(value)) return; diff --git a/esphome/components/combination/sensor.py b/esphome/components/combination/sensor.py index f5255fec03..0204162e8d 100644 --- a/esphome/components/combination/sensor.py +++ b/esphome/components/combination/sensor.py @@ -1,3 +1,5 @@ +import logging + import esphome.codegen as cg from esphome.components import sensor import esphome.config_validation as cv @@ -15,6 +17,8 @@ from esphome.const import ( ) from esphome.core.entity_helpers import inherit_property_from +_LOGGER = logging.getLogger(__name__) + CODEOWNERS = ["@Cat-Ion", "@kahrendt"] combination_ns = cg.esphome_ns.namespace("combination") @@ -47,7 +51,8 @@ SumCombinationComponent = combination_ns.class_( "SumCombinationComponent", cg.Component, sensor.Sensor ) -CONF_COEFFECIENT = "coeffecient" +CONF_COEFFICIENT = "coefficient" +CONF_COEFFECIENT = "coeffecient" # Deprecated, remove before 2026.12.0 CONF_ERROR = "error" CONF_KALMAN = "kalman" CONF_LINEAR = "linear" @@ -68,11 +73,34 @@ KALMAN_SOURCE_SCHEMA = cv.Schema( } ) -LINEAR_SOURCE_SCHEMA = cv.Schema( - { - cv.Required(CONF_SOURCE): cv.use_id(sensor.Sensor), - cv.Required(CONF_COEFFECIENT): cv.templatable(cv.float_), - } + +def _migrate_coeffecient(config): + """Migrate deprecated 'coeffecient' spelling to 'coefficient'.""" + if CONF_COEFFECIENT in config: + if CONF_COEFFICIENT in config: + raise cv.Invalid( + f"Cannot specify both '{CONF_COEFFICIENT}' and '{CONF_COEFFECIENT}'" + ) + _LOGGER.warning( + "'%s' is deprecated, use '%s' instead. Will be removed in 2026.12.0", + CONF_COEFFECIENT, + CONF_COEFFICIENT, + ) + config[CONF_COEFFICIENT] = config.pop(CONF_COEFFECIENT) + elif CONF_COEFFICIENT not in config: + raise cv.Invalid(f"'{CONF_COEFFICIENT}' is a required option") + return config + + +LINEAR_SOURCE_SCHEMA = cv.All( + cv.Schema( + { + cv.Required(CONF_SOURCE): cv.use_id(sensor.Sensor), + cv.Optional(CONF_COEFFICIENT): cv.templatable(cv.float_), + cv.Optional(CONF_COEFFECIENT): cv.templatable(cv.float_), + } + ), + _migrate_coeffecient, ) SENSOR_ONLY_SOURCE_SCHEMA = cv.Schema( @@ -162,12 +190,12 @@ async def to_code(config): ) cg.add(var.add_source(source, error)) elif config[CONF_TYPE] == CONF_LINEAR: - coeffecient = await cg.templatable( - source_conf[CONF_COEFFECIENT], + coefficient = await cg.templatable( + source_conf[CONF_COEFFICIENT], [(float, "x")], cg.float_, ) - cg.add(var.add_source(source, coeffecient)) + cg.add(var.add_source(source, coefficient)) else: cg.add(var.add_source(source)) diff --git a/tests/components/combination/common.yaml b/tests/components/combination/common.yaml index 0e5d512d08..5d46419399 100644 --- a/tests/components/combination/common.yaml +++ b/tests/components/combination/common.yaml @@ -27,9 +27,9 @@ sensor: name: Linearly combined temperatures sources: - source: template_temperature1 - coeffecient: !lambda "return 0.4 + std::abs(x - 25) * 0.023;" + coefficient: !lambda "return 0.4 + std::abs(x - 25) * 0.023;" - source: template_temperature2 - coeffecient: 1.5 + coefficient: 1.5 - platform: combination type: max name: Max of combined temperatures From 0f4dc6702df0d9646cfd28d85ae86a34bc2f6b6d Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Sun, 15 Feb 2026 12:11:50 -0500 Subject: [PATCH 035/197] [fan] Fix preset_mode not restored on boot (#14002) Co-authored-by: Claude Opus 4.6 --- esphome/components/fan/fan.cpp | 8 +++++++- esphome/components/fan/fan.h | 4 +++- esphome/components/hbridge/fan/hbridge_fan.cpp | 8 ++++---- esphome/components/speed/fan/speed_fan.cpp | 8 ++++---- esphome/components/template/fan/template_fan.cpp | 10 +++++----- 5 files changed, 23 insertions(+), 15 deletions(-) diff --git a/esphome/components/fan/fan.cpp b/esphome/components/fan/fan.cpp index d70a2940bc..c1e0a3dc2e 100644 --- a/esphome/components/fan/fan.cpp +++ b/esphome/components/fan/fan.cpp @@ -221,12 +221,17 @@ void Fan::publish_state() { } // Random 32-bit value, change this every time the layout of the FanRestoreState struct changes. -constexpr uint32_t RESTORE_STATE_VERSION = 0x71700ABA; +constexpr uint32_t RESTORE_STATE_VERSION = 0x71700ABB; optional Fan::restore_state_() { FanRestoreState recovered{}; this->rtc_ = this->make_entity_preference(RESTORE_STATE_VERSION); bool restored = this->rtc_.load(&recovered); + if (!restored) { + // No valid saved data; ensure preset_mode sentinel is set + recovered.preset_mode = FanRestoreState::NO_PRESET; + } + switch (this->restore_mode_) { case FanRestoreMode::NO_RESTORE: return {}; @@ -264,6 +269,7 @@ void Fan::save_state_() { state.oscillating = this->oscillating; state.speed = this->speed; state.direction = this->direction; + state.preset_mode = FanRestoreState::NO_PRESET; if (this->has_preset_mode()) { const auto &preset_modes = traits.supported_preset_modes(); diff --git a/esphome/components/fan/fan.h b/esphome/components/fan/fan.h index 55d4ba8825..2caf3a712a 100644 --- a/esphome/components/fan/fan.h +++ b/esphome/components/fan/fan.h @@ -91,11 +91,13 @@ class FanCall { }; struct FanRestoreState { + static constexpr uint8_t NO_PRESET = UINT8_MAX; + bool state; int speed; bool oscillating; FanDirection direction; - uint8_t preset_mode; + uint8_t preset_mode{NO_PRESET}; /// Convert this struct to a fan call that can be performed. FanCall to_call(Fan &fan); diff --git a/esphome/components/hbridge/fan/hbridge_fan.cpp b/esphome/components/hbridge/fan/hbridge_fan.cpp index 9bf58f9d1e..38e4129e66 100644 --- a/esphome/components/hbridge/fan/hbridge_fan.cpp +++ b/esphome/components/hbridge/fan/hbridge_fan.cpp @@ -28,15 +28,15 @@ fan::FanCall HBridgeFan::brake() { } void HBridgeFan::setup() { + // Construct traits before restore so preset modes can be looked up by index + this->traits_ = fan::FanTraits(this->oscillating_ != nullptr, true, true, this->speed_count_); + this->traits_.set_supported_preset_modes(this->preset_modes_); + auto restore = this->restore_state_(); if (restore.has_value()) { restore->apply(*this); this->write_state_(); } - - // Construct traits - this->traits_ = fan::FanTraits(this->oscillating_ != nullptr, true, true, this->speed_count_); - this->traits_.set_supported_preset_modes(this->preset_modes_); } void HBridgeFan::dump_config() { diff --git a/esphome/components/speed/fan/speed_fan.cpp b/esphome/components/speed/fan/speed_fan.cpp index af98e3a51f..55f7fd162c 100644 --- a/esphome/components/speed/fan/speed_fan.cpp +++ b/esphome/components/speed/fan/speed_fan.cpp @@ -7,15 +7,15 @@ namespace speed { static const char *const TAG = "speed.fan"; void SpeedFan::setup() { + // Construct traits before restore so preset modes can be looked up by index + this->traits_ = fan::FanTraits(this->oscillating_ != nullptr, true, this->direction_ != nullptr, this->speed_count_); + this->traits_.set_supported_preset_modes(this->preset_modes_); + auto restore = this->restore_state_(); if (restore.has_value()) { restore->apply(*this); this->write_state_(); } - - // Construct traits - this->traits_ = fan::FanTraits(this->oscillating_ != nullptr, true, this->direction_ != nullptr, this->speed_count_); - this->traits_.set_supported_preset_modes(this->preset_modes_); } void SpeedFan::dump_config() { LOG_FAN("", "Speed Fan", this); } diff --git a/esphome/components/template/fan/template_fan.cpp b/esphome/components/template/fan/template_fan.cpp index 0e1920a984..cd267bd552 100644 --- a/esphome/components/template/fan/template_fan.cpp +++ b/esphome/components/template/fan/template_fan.cpp @@ -6,15 +6,15 @@ namespace esphome::template_ { static const char *const TAG = "template.fan"; void TemplateFan::setup() { + // Construct traits before restore so preset modes can be looked up by index + this->traits_ = + fan::FanTraits(this->has_oscillating_, this->speed_count_ > 0, this->has_direction_, this->speed_count_); + this->traits_.set_supported_preset_modes(this->preset_modes_); + auto restore = this->restore_state_(); if (restore.has_value()) { restore->apply(*this); } - - // Construct traits - this->traits_ = - fan::FanTraits(this->has_oscillating_, this->speed_count_ > 0, this->has_direction_, this->speed_count_); - this->traits_.set_supported_preset_modes(this->preset_modes_); } void TemplateFan::dump_config() { LOG_FAN("", "Template Fan", this); } From 6303bc3e355bd73c22658a2745d8d5e19e195755 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Sun, 15 Feb 2026 13:23:06 -0500 Subject: [PATCH 036/197] [esp32_rmt] Handle ESP32 variants without RMT hardware (#14001) Co-authored-by: Claude Opus 4.6 --- esphome/components/esp32_rmt/__init__.py | 22 +++++ .../components/esp32_rmt_led_strip/light.py | 6 +- esphome/components/remote_base/remote_base.h | 5 +- .../components/remote_receiver/__init__.py | 41 +++++++- .../remote_receiver/remote_receiver.cpp | 2 +- .../remote_receiver/remote_receiver.h | 17 ++-- .../remote_receiver/remote_receiver_esp32.cpp | 5 +- .../components/remote_transmitter/__init__.py | 93 ++++++++++++------- .../remote_transmitter/automation.h | 6 +- .../remote_transmitter/remote_transmitter.cpp | 8 +- .../remote_transmitter/remote_transmitter.h | 21 +++-- .../remote_transmitter_esp32.cpp | 11 ++- .../remote_receiver/test.esp32-c2-idf.yaml | 12 +++ .../remote_transmitter/test.esp32-c2-idf.yaml | 7 ++ 14 files changed, 181 insertions(+), 75 deletions(-) create mode 100644 tests/components/remote_receiver/test.esp32-c2-idf.yaml create mode 100644 tests/components/remote_transmitter/test.esp32-c2-idf.yaml diff --git a/esphome/components/esp32_rmt/__init__.py b/esphome/components/esp32_rmt/__init__.py index 272c7c81ba..1076bcabdc 100644 --- a/esphome/components/esp32_rmt/__init__.py +++ b/esphome/components/esp32_rmt/__init__.py @@ -1,8 +1,30 @@ from esphome.components import esp32 import esphome.config_validation as cv +from esphome.core import CORE CODEOWNERS = ["@jesserockz"] +VARIANTS_NO_RMT = {esp32.VARIANT_ESP32C2, esp32.VARIANT_ESP32C61} + + +def validate_rmt_not_supported(rmt_only_keys): + """Validate that RMT-only config keys are not used on variants without RMT hardware.""" + rmt_only_keys = set(rmt_only_keys) + + def _validator(config): + if CORE.is_esp32: + variant = esp32.get_esp32_variant() + if variant in VARIANTS_NO_RMT: + unsupported = rmt_only_keys.intersection(config) + if unsupported: + keys = ", ".join(sorted(f"'{k}'" for k in unsupported)) + raise cv.Invalid( + f"{keys} not available on {variant} (no RMT hardware)" + ) + return config + + return _validator + def validate_clock_resolution(): def _validator(value): diff --git a/esphome/components/esp32_rmt_led_strip/light.py b/esphome/components/esp32_rmt_led_strip/light.py index 6d41f6b5b8..1c6943b003 100644 --- a/esphome/components/esp32_rmt_led_strip/light.py +++ b/esphome/components/esp32_rmt_led_strip/light.py @@ -3,7 +3,7 @@ import logging from esphome import pins import esphome.codegen as cg -from esphome.components import esp32, light +from esphome.components import esp32, esp32_rmt, light from esphome.components.const import CONF_USE_PSRAM from esphome.components.esp32 import include_builtin_idf_component import esphome.config_validation as cv @@ -71,6 +71,10 @@ CONF_RESET_LOW = "reset_low" CONFIG_SCHEMA = cv.All( + esp32.only_on_variant( + unsupported=list(esp32_rmt.VARIANTS_NO_RMT), + msg_prefix="ESP32 RMT LED strip", + ), light.ADDRESSABLE_LIGHT_SCHEMA.extend( { cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(ESP32RMTLEDStripLightOutput), diff --git a/esphome/components/remote_base/remote_base.h b/esphome/components/remote_base/remote_base.h index 0cac28506f..d73fff2b0a 100644 --- a/esphome/components/remote_base/remote_base.h +++ b/esphome/components/remote_base/remote_base.h @@ -119,6 +119,8 @@ class RemoteComponentBase { }; #ifdef USE_ESP32 +#include +#if SOC_RMT_SUPPORTED class RemoteRMTChannel { public: void set_clock_resolution(uint32_t clock_resolution) { this->clock_resolution_ = clock_resolution; } @@ -137,7 +139,8 @@ class RemoteRMTChannel { uint32_t clock_resolution_{1000000}; uint32_t rmt_symbols_; }; -#endif +#endif // SOC_RMT_SUPPORTED +#endif // USE_ESP32 class RemoteTransmitterBase : public RemoteComponentBase { public: diff --git a/esphome/components/remote_receiver/__init__.py b/esphome/components/remote_receiver/__init__.py index b3dc213c5f..362f6e99db 100644 --- a/esphome/components/remote_receiver/__init__.py +++ b/esphome/components/remote_receiver/__init__.py @@ -65,6 +65,8 @@ RemoteReceiverComponent = remote_receiver_ns.class_( def validate_config(config): if CORE.is_esp32: variant = esp32.get_esp32_variant() + if variant in esp32_rmt.VARIANTS_NO_RMT: + return config if variant in (esp32.VARIANT_ESP32, esp32.VARIANT_ESP32S2): max_idle = 65535 else: @@ -110,6 +112,8 @@ CONFIG_SCHEMA = remote_base.validate_triggers( cv.SplitDefault( CONF_BUFFER_SIZE, esp32="10000b", + esp32_c2="1000b", + esp32_c61="1000b", esp8266="1000b", bk72xx="1000b", ln882x="1000b", @@ -131,9 +135,11 @@ CONFIG_SCHEMA = remote_base.validate_triggers( cv.SplitDefault( CONF_RMT_SYMBOLS, esp32=192, + esp32_c2=cv.UNDEFINED, esp32_c3=96, esp32_c5=96, esp32_c6=96, + esp32_c61=cv.UNDEFINED, esp32_h2=96, esp32_p4=192, esp32_s2=192, @@ -145,6 +151,8 @@ CONFIG_SCHEMA = remote_base.validate_triggers( cv.SplitDefault( CONF_RECEIVE_SYMBOLS, esp32=192, + esp32_c2=cv.UNDEFINED, + esp32_c61=cv.UNDEFINED, ): cv.All(cv.only_on_esp32, cv.int_range(min=2)), cv.Optional(CONF_USE_DMA): cv.All( esp32.only_on_variant( @@ -152,24 +160,45 @@ CONFIG_SCHEMA = remote_base.validate_triggers( ), cv.boolean, ), - cv.SplitDefault(CONF_CARRIER_DUTY_PERCENT, esp32=100): cv.All( + cv.SplitDefault( + CONF_CARRIER_DUTY_PERCENT, + esp32=100, + esp32_c2=cv.UNDEFINED, + esp32_c61=cv.UNDEFINED, + ): cv.All( cv.only_on_esp32, cv.percentage_int, cv.Range(min=1, max=100), ), - cv.SplitDefault(CONF_CARRIER_FREQUENCY, esp32="0Hz"): cv.All( - cv.only_on_esp32, cv.frequency, cv.int_ - ), + cv.SplitDefault( + CONF_CARRIER_FREQUENCY, + esp32="0Hz", + esp32_c2=cv.UNDEFINED, + esp32_c61=cv.UNDEFINED, + ): cv.All(cv.only_on_esp32, cv.frequency, cv.int_), } ) .extend(cv.COMPONENT_SCHEMA) + .add_extra( + esp32_rmt.validate_rmt_not_supported( + [ + CONF_CLOCK_RESOLUTION, + CONF_USE_DMA, + CONF_RMT_SYMBOLS, + CONF_FILTER_SYMBOLS, + CONF_RECEIVE_SYMBOLS, + CONF_CARRIER_DUTY_PERCENT, + CONF_CARRIER_FREQUENCY, + ] + ) + ) .add_extra(validate_config) ) async def to_code(config): pin = await cg.gpio_pin_expression(config[CONF_PIN]) - if CORE.is_esp32: + if CORE.is_esp32 and esp32.get_esp32_variant() not in esp32_rmt.VARIANTS_NO_RMT: # Re-enable ESP-IDF's RMT driver (excluded by default to save compile time) esp32.include_builtin_idf_component("esp_driver_rmt") @@ -213,6 +242,8 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform( PlatformFramework.ESP32_IDF, }, "remote_receiver.cpp": { + PlatformFramework.ESP32_ARDUINO, + PlatformFramework.ESP32_IDF, PlatformFramework.ESP8266_ARDUINO, PlatformFramework.BK72XX_ARDUINO, PlatformFramework.RTL87XX_ARDUINO, diff --git a/esphome/components/remote_receiver/remote_receiver.cpp b/esphome/components/remote_receiver/remote_receiver.cpp index de47457dac..d59ee63695 100644 --- a/esphome/components/remote_receiver/remote_receiver.cpp +++ b/esphome/components/remote_receiver/remote_receiver.cpp @@ -3,7 +3,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -#if defined(USE_LIBRETINY) || defined(USE_ESP8266) || defined(USE_RP2040) +#if defined(USE_LIBRETINY) || defined(USE_ESP8266) || defined(USE_RP2040) || (defined(USE_ESP32) && !SOC_RMT_SUPPORTED) namespace esphome::remote_receiver { diff --git a/esphome/components/remote_receiver/remote_receiver.h b/esphome/components/remote_receiver/remote_receiver.h index 3d9199a904..5da9283a6e 100644 --- a/esphome/components/remote_receiver/remote_receiver.h +++ b/esphome/components/remote_receiver/remote_receiver.h @@ -6,12 +6,15 @@ #include #if defined(USE_ESP32) +#include +#if SOC_RMT_SUPPORTED #include -#endif +#endif // SOC_RMT_SUPPORTED +#endif // USE_ESP32 namespace esphome::remote_receiver { -#if defined(USE_ESP8266) || defined(USE_LIBRETINY) || defined(USE_RP2040) +#if defined(USE_ESP8266) || defined(USE_LIBRETINY) || defined(USE_RP2040) || (defined(USE_ESP32) && !SOC_RMT_SUPPORTED) struct RemoteReceiverComponentStore { static void gpio_intr(RemoteReceiverComponentStore *arg); @@ -35,7 +38,7 @@ struct RemoteReceiverComponentStore { volatile bool prev_level{false}; volatile bool overflow{false}; }; -#elif defined(USE_ESP32) +#elif defined(USE_ESP32) && SOC_RMT_SUPPORTED struct RemoteReceiverComponentStore { /// Stores RMT symbols and rx done event data volatile uint8_t *buffer{nullptr}; @@ -54,7 +57,7 @@ struct RemoteReceiverComponentStore { class RemoteReceiverComponent : public remote_base::RemoteReceiverBase, public Component -#ifdef USE_ESP32 +#if defined(USE_ESP32) && SOC_RMT_SUPPORTED , public remote_base::RemoteRMTChannel #endif @@ -66,7 +69,7 @@ class RemoteReceiverComponent : public remote_base::RemoteReceiverBase, void dump_config() override; void loop() override; -#ifdef USE_ESP32 +#if defined(USE_ESP32) && SOC_RMT_SUPPORTED void set_filter_symbols(uint32_t filter_symbols) { this->filter_symbols_ = filter_symbols; } void set_receive_symbols(uint32_t receive_symbols) { this->receive_symbols_ = receive_symbols; } void set_with_dma(bool with_dma) { this->with_dma_ = with_dma; } @@ -78,7 +81,7 @@ class RemoteReceiverComponent : public remote_base::RemoteReceiverBase, void set_idle_us(uint32_t idle_us) { this->idle_us_ = idle_us; } protected: -#ifdef USE_ESP32 +#if defined(USE_ESP32) && SOC_RMT_SUPPORTED void decode_rmt_(rmt_symbol_word_t *item, size_t item_count); rmt_channel_handle_t channel_{NULL}; uint32_t filter_symbols_{0}; @@ -94,7 +97,7 @@ class RemoteReceiverComponent : public remote_base::RemoteReceiverBase, RemoteReceiverComponentStore store_; #endif -#if defined(USE_ESP8266) || defined(USE_LIBRETINY) || defined(USE_RP2040) +#if defined(USE_ESP8266) || defined(USE_LIBRETINY) || defined(USE_RP2040) || (defined(USE_ESP32) && !SOC_RMT_SUPPORTED) HighFrequencyLoopRequester high_freq_; #endif diff --git a/esphome/components/remote_receiver/remote_receiver_esp32.cpp b/esphome/components/remote_receiver/remote_receiver_esp32.cpp index f95244ea45..357a36d052 100644 --- a/esphome/components/remote_receiver/remote_receiver_esp32.cpp +++ b/esphome/components/remote_receiver/remote_receiver_esp32.cpp @@ -2,6 +2,8 @@ #include "esphome/core/log.h" #ifdef USE_ESP32 +#include +#if SOC_RMT_SUPPORTED #include #include @@ -248,4 +250,5 @@ void RemoteReceiverComponent::decode_rmt_(rmt_symbol_word_t *item, size_t item_c } // namespace esphome::remote_receiver -#endif +#endif // SOC_RMT_SUPPORTED +#endif // USE_ESP32 diff --git a/esphome/components/remote_transmitter/__init__.py b/esphome/components/remote_transmitter/__init__.py index 8383b9dd75..fc772f88b2 100644 --- a/esphome/components/remote_transmitter/__init__.py +++ b/esphome/components/remote_transmitter/__init__.py @@ -40,45 +40,66 @@ DigitalWriteAction = remote_transmitter_ns.class_( cg.Parented.template(RemoteTransmitterComponent), ) + MULTI_CONF = True -CONFIG_SCHEMA = cv.Schema( - { - cv.GenerateID(): cv.declare_id(RemoteTransmitterComponent), - cv.Required(CONF_PIN): pins.gpio_output_pin_schema, - cv.Required(CONF_CARRIER_DUTY_PERCENT): cv.All( - cv.percentage_int, cv.Range(min=1, max=100) - ), - cv.Optional(CONF_CLOCK_RESOLUTION): cv.All( - cv.only_on_esp32, - esp32_rmt.validate_clock_resolution(), - ), - cv.Optional(CONF_EOT_LEVEL): cv.All(cv.only_on_esp32, cv.boolean), - cv.Optional(CONF_USE_DMA): cv.All( - esp32.only_on_variant( - supported=[esp32.VARIANT_ESP32P4, esp32.VARIANT_ESP32S3] +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(RemoteTransmitterComponent), + cv.Required(CONF_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_CARRIER_DUTY_PERCENT): cv.All( + cv.percentage_int, cv.Range(min=1, max=100) ), - cv.boolean, - ), - cv.SplitDefault( - CONF_RMT_SYMBOLS, - esp32=64, - esp32_c3=48, - esp32_c5=48, - esp32_c6=48, - esp32_h2=48, - esp32_p4=48, - esp32_s2=64, - esp32_s3=48, - ): cv.All(cv.only_on_esp32, cv.int_range(min=2)), - cv.Optional(CONF_NON_BLOCKING): cv.All(cv.only_on_esp32, cv.boolean), - cv.Optional(CONF_ON_TRANSMIT): automation.validate_automation(single=True), - cv.Optional(CONF_ON_COMPLETE): automation.validate_automation(single=True), - } -).extend(cv.COMPONENT_SCHEMA) + cv.Optional(CONF_CLOCK_RESOLUTION): cv.All( + cv.only_on_esp32, + esp32_rmt.validate_clock_resolution(), + ), + cv.Optional(CONF_EOT_LEVEL): cv.All(cv.only_on_esp32, cv.boolean), + cv.Optional(CONF_USE_DMA): cv.All( + esp32.only_on_variant( + supported=[esp32.VARIANT_ESP32P4, esp32.VARIANT_ESP32S3] + ), + cv.boolean, + ), + cv.SplitDefault( + CONF_RMT_SYMBOLS, + esp32=64, + esp32_c2=cv.UNDEFINED, + esp32_c3=48, + esp32_c5=48, + esp32_c6=48, + esp32_c61=cv.UNDEFINED, + esp32_h2=48, + esp32_p4=48, + esp32_s2=64, + esp32_s3=48, + ): cv.All(cv.only_on_esp32, cv.int_range(min=2)), + cv.Optional(CONF_NON_BLOCKING): cv.All(cv.only_on_esp32, cv.boolean), + cv.Optional(CONF_ON_TRANSMIT): automation.validate_automation(single=True), + cv.Optional(CONF_ON_COMPLETE): automation.validate_automation(single=True), + } + ) + .extend(cv.COMPONENT_SCHEMA) + .add_extra( + esp32_rmt.validate_rmt_not_supported( + [ + CONF_CLOCK_RESOLUTION, + CONF_EOT_LEVEL, + CONF_USE_DMA, + CONF_RMT_SYMBOLS, + CONF_NON_BLOCKING, + ] + ) + ) +) def _validate_non_blocking(config): - if CORE.is_esp32 and CONF_NON_BLOCKING not in config: + if ( + CORE.is_esp32 + and esp32.get_esp32_variant() not in esp32_rmt.VARIANTS_NO_RMT + and CONF_NON_BLOCKING not in config + ): _LOGGER.warning( "'non_blocking' is not set for 'remote_transmitter' and will default to 'true'.\n" "The default behavior changed in 2025.11.0; previously blocking mode was used.\n" @@ -111,7 +132,7 @@ async def digital_write_action_to_code(config, action_id, template_arg, args): async def to_code(config): pin = await cg.gpio_pin_expression(config[CONF_PIN]) - if CORE.is_esp32: + if CORE.is_esp32 and esp32.get_esp32_variant() not in esp32_rmt.VARIANTS_NO_RMT: # Re-enable ESP-IDF's RMT driver (excluded by default to save compile time) esp32.include_builtin_idf_component("esp_driver_rmt") @@ -155,6 +176,8 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform( PlatformFramework.ESP32_IDF, }, "remote_transmitter.cpp": { + PlatformFramework.ESP32_ARDUINO, + PlatformFramework.ESP32_IDF, PlatformFramework.ESP8266_ARDUINO, PlatformFramework.BK72XX_ARDUINO, PlatformFramework.RTL87XX_ARDUINO, diff --git a/esphome/components/remote_transmitter/automation.h b/esphome/components/remote_transmitter/automation.h index bee1d0be8a..8da4cfd95d 100644 --- a/esphome/components/remote_transmitter/automation.h +++ b/esphome/components/remote_transmitter/automation.h @@ -5,8 +5,7 @@ #include "esphome/core/component.h" #include "esphome/core/helpers.h" -namespace esphome { -namespace remote_transmitter { +namespace esphome::remote_transmitter { template class DigitalWriteAction : public Action, public Parented { public: @@ -14,5 +13,4 @@ template class DigitalWriteAction : public Action, public void play(const Ts &...x) override { this->parent_->digital_write(this->value_.value(x...)); } }; -} // namespace remote_transmitter -} // namespace esphome +} // namespace esphome::remote_transmitter diff --git a/esphome/components/remote_transmitter/remote_transmitter.cpp b/esphome/components/remote_transmitter/remote_transmitter.cpp index d35541e2e1..51a3c0b1d4 100644 --- a/esphome/components/remote_transmitter/remote_transmitter.cpp +++ b/esphome/components/remote_transmitter/remote_transmitter.cpp @@ -2,10 +2,9 @@ #include "esphome/core/log.h" #include "esphome/core/application.h" -#if defined(USE_LIBRETINY) || defined(USE_ESP8266) || defined(USE_RP2040) +#if defined(USE_LIBRETINY) || defined(USE_ESP8266) || defined(USE_RP2040) || (defined(USE_ESP32) && !SOC_RMT_SUPPORTED) -namespace esphome { -namespace remote_transmitter { +namespace esphome::remote_transmitter { static const char *const TAG = "remote_transmitter"; @@ -105,7 +104,6 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen this->complete_trigger_.trigger(); } -} // namespace remote_transmitter -} // namespace esphome +} // namespace esphome::remote_transmitter #endif diff --git a/esphome/components/remote_transmitter/remote_transmitter.h b/esphome/components/remote_transmitter/remote_transmitter.h index 65bd2ac8b2..aee52ea170 100644 --- a/esphome/components/remote_transmitter/remote_transmitter.h +++ b/esphome/components/remote_transmitter/remote_transmitter.h @@ -6,13 +6,15 @@ #include #if defined(USE_ESP32) +#include +#if SOC_RMT_SUPPORTED #include -#endif +#endif // SOC_RMT_SUPPORTED +#endif // USE_ESP32 -namespace esphome { -namespace remote_transmitter { +namespace esphome::remote_transmitter { -#ifdef USE_ESP32 +#if defined(USE_ESP32) && SOC_RMT_SUPPORTED #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 1) // IDF version 5.5.1 and above is required because of a bug in // the RMT encoder: https://github.com/espressif/esp-idf/issues/17244 @@ -33,7 +35,7 @@ struct RemoteTransmitterComponentStore { class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase, public Component -#ifdef USE_ESP32 +#if defined(USE_ESP32) && SOC_RMT_SUPPORTED , public remote_base::RemoteRMTChannel #endif @@ -51,7 +53,7 @@ class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase, void digital_write(bool value); -#if defined(USE_ESP32) +#if defined(USE_ESP32) && SOC_RMT_SUPPORTED void set_with_dma(bool with_dma) { this->with_dma_ = with_dma; } void set_eot_level(bool eot_level) { this->eot_level_ = eot_level; } void set_non_blocking(bool non_blocking) { this->non_blocking_ = non_blocking; } @@ -62,7 +64,7 @@ class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase, protected: void send_internal(uint32_t send_times, uint32_t send_wait) override; -#if defined(USE_ESP8266) || defined(USE_LIBRETINY) || defined(USE_RP2040) +#if defined(USE_ESP8266) || defined(USE_LIBRETINY) || defined(USE_RP2040) || (defined(USE_ESP32) && !SOC_RMT_SUPPORTED) void calculate_on_off_time_(uint32_t carrier_frequency, uint32_t *on_time_period, uint32_t *off_time_period); void mark_(uint32_t on_time, uint32_t off_time, uint32_t usec); @@ -73,7 +75,7 @@ class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase, uint32_t target_time_; #endif -#ifdef USE_ESP32 +#if defined(USE_ESP32) && SOC_RMT_SUPPORTED void configure_rmt_(); void wait_for_rmt_(); @@ -100,5 +102,4 @@ class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase, Trigger<> complete_trigger_; }; -} // namespace remote_transmitter -} // namespace esphome +} // namespace esphome::remote_transmitter diff --git a/esphome/components/remote_transmitter/remote_transmitter_esp32.cpp b/esphome/components/remote_transmitter/remote_transmitter_esp32.cpp index 89d97895b2..71773e3ddf 100644 --- a/esphome/components/remote_transmitter/remote_transmitter_esp32.cpp +++ b/esphome/components/remote_transmitter/remote_transmitter_esp32.cpp @@ -3,10 +3,11 @@ #include "esphome/core/application.h" #ifdef USE_ESP32 +#include +#if SOC_RMT_SUPPORTED #include -namespace esphome { -namespace remote_transmitter { +namespace esphome::remote_transmitter { static const char *const TAG = "remote_transmitter"; @@ -358,7 +359,7 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen } #endif -} // namespace remote_transmitter -} // namespace esphome +} // namespace esphome::remote_transmitter -#endif +#endif // SOC_RMT_SUPPORTED +#endif // USE_ESP32 diff --git a/tests/components/remote_receiver/test.esp32-c2-idf.yaml b/tests/components/remote_receiver/test.esp32-c2-idf.yaml new file mode 100644 index 0000000000..87154e19fc --- /dev/null +++ b/tests/components/remote_receiver/test.esp32-c2-idf.yaml @@ -0,0 +1,12 @@ +remote_receiver: + id: rcvr + pin: GPIO2 + dump: all + <<: !include common-actions.yaml + +binary_sensor: + - platform: remote_receiver + name: Panasonic Remote Input + panasonic: + address: 0x4004 + command: 0x100BCBD diff --git a/tests/components/remote_transmitter/test.esp32-c2-idf.yaml b/tests/components/remote_transmitter/test.esp32-c2-idf.yaml new file mode 100644 index 0000000000..424cd8d249 --- /dev/null +++ b/tests/components/remote_transmitter/test.esp32-c2-idf.yaml @@ -0,0 +1,7 @@ +remote_transmitter: + id: xmitr + pin: GPIO2 + carrier_duty_percent: 50% + +packages: + buttons: !include common-buttons.yaml From 15da6d0a0b3a7ec14462e49c00f329af3808ce7b Mon Sep 17 00:00:00 2001 From: Pawelo <81100874+pgolawsk@users.noreply.github.com> Date: Sun, 15 Feb 2026 21:58:51 +0100 Subject: [PATCH 037/197] [epaper_spi] Add WeAct 3-color e-paper display support (#13894) --- esphome/components/epaper_spi/display.py | 4 - .../components/epaper_spi/epaper_weact_3c.cpp | 231 ++++++++++++++++++ .../components/epaper_spi/epaper_weact_3c.h | 39 +++ .../components/epaper_spi/models/weact_bwr.py | 75 ++++++ .../epaper_spi/test.esp32-s3-idf.yaml | 63 +++++ 5 files changed, 408 insertions(+), 4 deletions(-) create mode 100644 esphome/components/epaper_spi/epaper_weact_3c.cpp create mode 100644 esphome/components/epaper_spi/epaper_weact_3c.h create mode 100644 esphome/components/epaper_spi/models/weact_bwr.py diff --git a/esphome/components/epaper_spi/display.py b/esphome/components/epaper_spi/display.py index 13f66691b2..2657071f45 100644 --- a/esphome/components/epaper_spi/display.py +++ b/esphome/components/epaper_spi/display.py @@ -49,10 +49,6 @@ EPaperBase = epaper_spi_ns.class_( ) Transform = epaper_spi_ns.enum("Transform") -EPaperSpectraE6 = epaper_spi_ns.class_("EPaperSpectraE6", EPaperBase) -EPaper7p3InSpectraE6 = epaper_spi_ns.class_("EPaper7p3InSpectraE6", EPaperSpectraE6) - - # Import all models dynamically from the models package for module_info in pkgutil.iter_modules(models.__path__): importlib.import_module(f".models.{module_info.name}", package=__package__) diff --git a/esphome/components/epaper_spi/epaper_weact_3c.cpp b/esphome/components/epaper_spi/epaper_weact_3c.cpp new file mode 100644 index 0000000000..bd83105dd7 --- /dev/null +++ b/esphome/components/epaper_spi/epaper_weact_3c.cpp @@ -0,0 +1,231 @@ +#include "epaper_weact_3c.h" +#include "esphome/core/log.h" + +namespace esphome::epaper_spi { + +static constexpr const char *const TAG = "epaper_weact_3c"; + +// SSD1680 3-color display notes: +// - Buffer uses 1 bit per pixel, 8 pixels per byte +// - Buffer first half (black_offset): Black/White plane (1=black, 0=white) +// - Buffer second half (red_offset): Red plane (1=red, 0=no red) +// - Total buffer: width * height / 4 bytes = 2 * (width * height / 8) +// - For 128x296: 128*296/4 = 9472 bytes total (4736 per color) + +void EPaperWeAct3C::draw_pixel_at(int x, int y, Color color) { + if (!this->rotate_coordinates_(x, y)) + return; + + // Calculate position in the 1-bit buffer + const uint32_t pos = (x / 8) + (y * this->row_width_); + const uint8_t bit = 0x80 >> (x & 0x07); + const uint32_t red_offset = this->buffer_length_ / 2u; + + // Use luminance threshold for B/W mapping + // Split at halfway point (382 = (255*3)/2) + bool is_white = (static_cast(color.r) + color.g + color.b) > 382; + + // Update black/white plane (first half of buffer) + if (is_white) { + // White pixel - clear bit in black plane + this->buffer_[pos] &= ~bit; + } else { + // Black pixel - set bit in black plane + this->buffer_[pos] |= bit; + } + + // Update red plane (second half of buffer) + // Red if red component is dominant (r > g+b) + if (color.r > color.g + color.b) { + // Red pixel - set bit in red plane + this->buffer_[red_offset + pos] |= bit; + } else { + // Not red - clear bit in red plane + this->buffer_[red_offset + pos] &= ~bit; + } +} + +void EPaperWeAct3C::fill(Color color) { + // For 3-color e-paper with 1-bit buffer format: + // - Black buffer: 1=black, 0=white + // - Red buffer: 1=red, 0=no red + // The buffer is stored as two halves: [black plane][red plane] + const size_t half_buffer = this->buffer_length_ / 2u; + + // Use luminance threshold for B/W mapping + bool is_white = (static_cast(color.r) + color.g + color.b) > 382; + bool is_red = color.r > color.g + color.b; + + // Fill both planes + if (is_white) { + // White - both planes = 0x00 + this->buffer_.fill(0x00); + } else if (is_red) { + // Red - black plane = 0x00, red plane = 0xFF + for (size_t i = 0; i < half_buffer; i++) + this->buffer_[i] = 0x00; + for (size_t i = 0; i < half_buffer; i++) + this->buffer_[half_buffer + i] = 0xFF; + } else { + // Black - black plane = 0xFF, red plane = 0x00 + for (size_t i = 0; i < half_buffer; i++) + this->buffer_[i] = 0xFF; + for (size_t i = 0; i < half_buffer; i++) + this->buffer_[half_buffer + i] = 0x00; + } +} + +void EPaperWeAct3C::clear() { + // Clear buffer to white, just like real paper. + this->fill(COLOR_ON); +} + +void EPaperWeAct3C::set_window_() { + // For full screen refresh, we always start from (0,0) + // The y_low_/y_high_ values track the dirty region for optimization, + // but for display refresh we need to write from the beginning + uint16_t x_start = 0; + uint16_t x_end = this->width_ - 1; + uint16_t y_start = 0; + uint16_t y_end = this->height_ - 1; // height = 296 for 2.9" display + + // Set RAM X address boundaries (0x44) + // X coordinates are byte-aligned (divided by 8) + this->cmd_data(0x44, {(uint8_t) (x_start / 8), (uint8_t) (x_end / 8)}); + + // Set RAM Y address boundaries (0x45) + // Format: Y start (LSB, MSB), Y end (LSB, MSB) + this->cmd_data(0x45, {(uint8_t) y_start, (uint8_t) (y_start >> 8), (uint8_t) (y_end & 0xFF), (uint8_t) (y_end >> 8)}); + + // Reset RAM X counter to start (0x4E) - 1 byte + this->cmd_data(0x4E, {(uint8_t) (x_start / 8)}); + + // Reset RAM Y counter to start (0x4F) - 2 bytes (LSB, MSB) + this->cmd_data(0x4F, {(uint8_t) y_start, (uint8_t) (y_start >> 8)}); +} + +bool HOT EPaperWeAct3C::transfer_data() { + const uint32_t start_time = millis(); + const size_t buffer_length = this->buffer_length_; + const size_t half_buffer = buffer_length / 2u; + + ESP_LOGV(TAG, "transfer_data: buffer_length=%u, half_buffer=%u", buffer_length, half_buffer); + + // Use a local buffer for SPI transfers + static constexpr size_t MAX_TRANSFER_SIZE = 128; + uint8_t bytes_to_send[MAX_TRANSFER_SIZE]; + + // First, send the RED buffer (0x26 = WRITE_COLOR) + // The red plane is in the second half of our buffer + // NOTE: Must set RAM window first to reset address counters! + if (this->current_data_index_ < half_buffer) { + if (this->current_data_index_ == 0) { + ESP_LOGV(TAG, "transfer_data: sending RED buffer (0x26)"); + this->set_window_(); // Reset RAM X/Y counters to start position + this->command(0x26); + } + + this->start_data_(); + size_t red_offset = half_buffer; + while (this->current_data_index_ < half_buffer) { + size_t bytes_to_copy = std::min(MAX_TRANSFER_SIZE, half_buffer - this->current_data_index_); + + for (size_t i = 0; i < bytes_to_copy; i++) { + bytes_to_send[i] = this->buffer_[red_offset + this->current_data_index_ + i]; + } + + this->write_array(bytes_to_send, bytes_to_copy); + + this->current_data_index_ += bytes_to_copy; + + if (millis() - start_time > MAX_TRANSFER_TIME) { + // Let the main loop run and come back next loop + this->disable(); + return false; + } + } + this->disable(); + } + + // Finished the red buffer, now send the BLACK buffer (0x24 = WRITE_BLACK) + // The black plane is in the first half of our buffer + if (this->current_data_index_ < buffer_length) { + if (this->current_data_index_ == half_buffer) { + ESP_LOGV(TAG, "transfer_data: finished red buffer, sending BLACK buffer (0x24)"); + + // Do NOT reset RAM counters here for WeAct displays (Reference implementation behavior) + // this->set_window(); + this->command(0x24); + // Continue using current_data_index_, but we need to map it to the start of the buffer + } + + this->start_data_(); + while (this->current_data_index_ < buffer_length) { + size_t remaining = buffer_length - this->current_data_index_; + size_t bytes_to_copy = std::min(MAX_TRANSFER_SIZE, remaining); + + // Calculate offset into the BLACK buffer (which is at the start of this->buffer_) + // current_data_index_ goes from half_buffer to buffer_length + size_t buffer_offset = this->current_data_index_ - half_buffer; + + for (size_t i = 0; i < bytes_to_copy; i++) { + bytes_to_send[i] = this->buffer_[buffer_offset + i]; + } + + this->write_array(bytes_to_send, bytes_to_copy); + + this->current_data_index_ += bytes_to_copy; + + if (millis() - start_time > MAX_TRANSFER_TIME) { + // Let the main loop run and come back next loop + this->disable(); + return false; + } + } + this->disable(); + } + + this->current_data_index_ = 0; + ESP_LOGV(TAG, "transfer_data: completed (red=%u, black=%u bytes)", half_buffer, half_buffer); + return true; +} + +void EPaperWeAct3C::refresh_screen(bool partial) { + // SSD1680 refresh sequence: + // Reset RAM X/Y address counters to 0,0 so display reads from start + // 0x4E: RAM X counter - 1 byte (X / 8) + // 0x4F: RAM Y counter - 2 bytes (Y LSB, Y MSB) + this->cmd_data(0x4E, {0x00}); // RAM X counter = 0 (1 byte) + this->cmd_data(0x4F, {0x00, 0x00}); // RAM Y counter = 0 (2 bytes) + + // Send UPDATE_FULL command (0x22) with display update control parameter + // Both WeAct and waveshare reference use 0xF7: {0x22, 0xF7} + // 0xF7 = Display update: Load temperature, Load LUT, Enable RAM content + this->cmd_data(0x22, {0xF7}); // Command 0x22 with parameter 0xF7 + this->command(0x20); // Activate display update + + // COMMAND TERMINATE FRAME READ WRITE (required by SSD1680) + // Removed 0xFF based on working reference implementation + // this->command(0xFF); +} + +void EPaperWeAct3C::power_on() { + // Power on sequence - send command to turn on power + // According to SSD1680 spec: 0x22, 0xF8 powers on the display + this->cmd_data(0x22, {0xF8}); // Power on + this->command(0x20); // Activate +} + +void EPaperWeAct3C::power_off() { + // Power off sequence - send command to turn off power + // According to SSD1680 spec: 0x22, 0x83 powers off the display + this->cmd_data(0x22, {0x83}); // Power off + this->command(0x20); // Activate +} + +void EPaperWeAct3C::deep_sleep() { + // Deep sleep sequence + this->cmd_data(0x10, {0x01}); // Deep sleep mode +} + +} // namespace esphome::epaper_spi diff --git a/esphome/components/epaper_spi/epaper_weact_3c.h b/esphome/components/epaper_spi/epaper_weact_3c.h new file mode 100644 index 0000000000..2df6f1ba09 --- /dev/null +++ b/esphome/components/epaper_spi/epaper_weact_3c.h @@ -0,0 +1,39 @@ +#pragma once + +#include "epaper_spi.h" + +namespace esphome::epaper_spi { + +/** + * WeAct 3-color e-paper displays (SSD1683 controller). + * Supports multiple sizes: 2.9" (128x296), 4.2" (400x300), etc. + * + * Color scheme: Black, White, Red (BWR) + * Buffer layout: 1 bit per pixel, separate planes + * - Buffer first half: Black/White plane (1=black, 0=white) + * - Buffer second half: Red plane (1=red, 0=no red) + * - Total buffer: width * height / 4 bytes (2 * width * height / 8) + */ +class EPaperWeAct3C : public EPaperBase { + public: + EPaperWeAct3C(const char *name, uint16_t width, uint16_t height, const uint8_t *init_sequence, + size_t init_sequence_length) + : EPaperBase(name, width, height, init_sequence, init_sequence_length, DISPLAY_TYPE_BINARY) { + this->buffer_length_ = this->row_width_ * height * 2; + } + + void fill(Color color) override; + void clear() override; + + protected: + void set_window_(); + void refresh_screen(bool partial) override; + void power_on() override; + void power_off() override; + void deep_sleep() override; + void draw_pixel_at(int x, int y, Color color) override; + + bool transfer_data() override; +}; + +} // namespace esphome::epaper_spi diff --git a/esphome/components/epaper_spi/models/weact_bwr.py b/esphome/components/epaper_spi/models/weact_bwr.py new file mode 100644 index 0000000000..21a015b08d --- /dev/null +++ b/esphome/components/epaper_spi/models/weact_bwr.py @@ -0,0 +1,75 @@ +"""WeAct Black/White/Red e-paper displays using SSD1683 controller. + +Supported models: +- weact-2.13in-3c: 122x250 pixels (2.13" display) +- weact-2.9in-3c: 128x296 pixels (2.9" display) +- weact-4.2in-3c: 400x300 pixels (4.2" display) + +These displays use SSD1680 or SSD1683 controller and require a specific initialization +sequence. The DRV_OUT_CTL command is calculated from the display height. +""" + +from . import EpaperModel + + +class WeActBWR(EpaperModel): + """Base EpaperModel class for WeAct Black/White/Red displays using SSD1683 controller.""" + + def __init__(self, name, **defaults): + super().__init__(name, "EPaperWeAct3C", **defaults) + + def get_init_sequence(self, config): + """Generate initialization sequence for WeAct BWR displays. + + The initialization sequence is based on SSD1680 and SSD1683 controller datasheet + and the WeAct display specifications. + """ + _, height = self.get_dimensions(config) + # DRV_OUT_CTL: MSB of (height-1), LSB of (height-1), gate setting (0x00) + height_minus_1 = height - 1 + msb = height_minus_1 >> 8 + lsb = height_minus_1 & 0xFF + return ( + # Step 1: Software Reset (0x12) - REQUIRED per SSD1680, but works without it as well, so it's commented out for now + # (0x12,), + # Step 2: Wait 10ms after SWRESET (?) not sure how to implement wht waiting for 10ms after SWRESET, so it's commented out for now + # Step 3: DRV_OUT_CTL - driver output control (height-dependent) + # Format: (command, LSB, MSB, gate setting) + (0x01, lsb, msb, 0x00), + # Step 4: DATA_ENTRY - data entry mode (0x03 = decrement Y, increment X) + (0x11, 0x03), + # Step 5: BORDER_FULL - border waveform control + (0x3C, 0x05), + # Step 6: TEMP_SENS - internal temperature sensor + (0x18, 0x80), + # Step 7: DISPLAY_UPDATE - display update control + (0x21, 0x00, 0x80), + ) + + +# Model: WeAct 2.9" 3C - 128x296 pixels, SSD1680 controller +weact_2p9in3c = WeActBWR( + "weact-2.9in-3c", + width=128, + height=296, + data_rate="10MHz", + minimum_update_interval="1s", +) + +# Model: WeAct 2.13" 3C - 122x250 pixels, SSD1680 controller +weact_2p13in3c = WeActBWR( + "weact-2.13in-3c", + width=122, + height=250, + data_rate="10MHz", + minimum_update_interval="1s", +) + +# Model: WeAct 4.2" 3C - 400x300 pixels, SSD1683 controller +weact_4p2in3c = WeActBWR( + "weact-4.2in-3c", + width=400, + height=300, + data_rate="10MHz", + minimum_update_interval="10s", +) diff --git a/tests/components/epaper_spi/test.esp32-s3-idf.yaml b/tests/components/epaper_spi/test.esp32-s3-idf.yaml index 333ab567cd..aa454c73fa 100644 --- a/tests/components/epaper_spi/test.esp32-s3-idf.yaml +++ b/tests/components/epaper_spi/test.esp32-s3-idf.yaml @@ -64,3 +64,66 @@ display: # Override pins to avoid conflict with other display configs busy_pin: 43 dc_pin: 42 + + # WeAct 2.13" 3-color e-paper (122x250, SSD1680) + - platform: epaper_spi + spi_id: spi_bus + model: weact-2.13in-3c + cs_pin: + allow_other_uses: true + number: GPIO5 + dc_pin: + allow_other_uses: true + number: GPIO17 + reset_pin: + allow_other_uses: true + number: GPIO16 + busy_pin: + allow_other_uses: true + number: GPIO4 + lambda: |- + it.filled_rectangle(0, 0, it.get_width(), it.get_height(), Color::WHITE); + it.circle(it.get_width() / 2, it.get_height() / 2, 20, Color::BLACK); + it.circle(it.get_width() / 2, it.get_height() / 2, 15, Color(255, 0, 0)); + + # WeAct 2.9" 3-color e-paper (128x296, SSD1683) + - platform: epaper_spi + spi_id: spi_bus + model: weact-2.9in-3c + cs_pin: + allow_other_uses: true + number: GPIO5 + dc_pin: + allow_other_uses: true + number: GPIO17 + reset_pin: + allow_other_uses: true + number: GPIO16 + busy_pin: + allow_other_uses: true + number: GPIO4 + lambda: |- + it.filled_rectangle(0, 0, it.get_width(), it.get_height(), Color::WHITE); + it.circle(it.get_width() / 2, it.get_height() / 2, 20, Color::BLACK); + it.circle(it.get_width() / 2, it.get_height() / 2, 15, Color(255, 0, 0)); + + # WeAct 4.2" 3-color e-paper (400x300, SSD1683) + - platform: epaper_spi + spi_id: spi_bus + model: weact-4.2in-3c + cs_pin: + allow_other_uses: true + number: GPIO5 + dc_pin: + allow_other_uses: true + number: GPIO17 + reset_pin: + allow_other_uses: true + number: GPIO16 + busy_pin: + allow_other_uses: true + number: GPIO4 + lambda: |- + it.filled_rectangle(0, 0, it.get_width(), it.get_height(), Color::WHITE); + it.circle(it.get_width() / 2, it.get_height() / 2, 30, Color::BLACK); + it.circle(it.get_width() / 2, it.get_height() / 2, 20, Color(255, 0, 0)); From 066419019fb9997522788e940adbbf27243d5e6e Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Sun, 15 Feb 2026 15:09:35 -0600 Subject: [PATCH 038/197] [audio] Support reallocating non-empty AudioTransferBuffer (#13979) --- .../audio/audio_transfer_buffer.cpp | 34 +++++++++++++++---- .../components/audio/audio_transfer_buffer.h | 3 ++ 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/esphome/components/audio/audio_transfer_buffer.cpp b/esphome/components/audio/audio_transfer_buffer.cpp index 790cd62db0..ddb669e0eb 100644 --- a/esphome/components/audio/audio_transfer_buffer.cpp +++ b/esphome/components/audio/audio_transfer_buffer.cpp @@ -2,6 +2,8 @@ #ifdef USE_ESP32 +#include + #include "esphome/core/helpers.h" namespace esphome { @@ -75,12 +77,32 @@ bool AudioTransferBuffer::has_buffered_data() const { } bool AudioTransferBuffer::reallocate(size_t new_buffer_size) { - if (this->buffer_length_ > 0) { - // Buffer currently has data, so reallocation is impossible + if (this->buffer_ == nullptr) { + return this->allocate_buffer_(new_buffer_size); + } + + if (new_buffer_size < this->buffer_length_) { + // New size is too small to hold existing data return false; } - this->deallocate_buffer_(); - return this->allocate_buffer_(new_buffer_size); + + // Shift existing data to the start of the buffer so realloc preserves it + if ((this->buffer_length_ > 0) && (this->data_start_ != this->buffer_)) { + std::memmove(this->buffer_, this->data_start_, this->buffer_length_); + this->data_start_ = this->buffer_; + } + + RAMAllocator allocator; + uint8_t *new_buffer = allocator.reallocate(this->buffer_, new_buffer_size); + if (new_buffer == nullptr) { + // Reallocation failed, but the original buffer is still valid + return false; + } + + this->buffer_ = new_buffer; + this->data_start_ = this->buffer_; + this->buffer_size_ = new_buffer_size; + return true; } bool AudioTransferBuffer::allocate_buffer_(size_t buffer_size) { @@ -115,7 +137,7 @@ size_t AudioSourceTransferBuffer::transfer_data_from_source(TickType_t ticks_to_ if (pre_shift) { // Shift data in buffer to start if (this->buffer_length_ > 0) { - memmove(this->buffer_, this->data_start_, this->buffer_length_); + std::memmove(this->buffer_, this->data_start_, this->buffer_length_); } this->data_start_ = this->buffer_; } @@ -150,7 +172,7 @@ size_t AudioSinkTransferBuffer::transfer_data_to_sink(TickType_t ticks_to_wait, if (post_shift) { // Shift unwritten data to the start of the buffer - memmove(this->buffer_, this->data_start_, this->buffer_length_); + std::memmove(this->buffer_, this->data_start_, this->buffer_length_); this->data_start_ = this->buffer_; } diff --git a/esphome/components/audio/audio_transfer_buffer.h b/esphome/components/audio/audio_transfer_buffer.h index edb484e7d2..24c0670d1a 100644 --- a/esphome/components/audio/audio_transfer_buffer.h +++ b/esphome/components/audio/audio_transfer_buffer.h @@ -56,6 +56,9 @@ class AudioTransferBuffer { /// @return True if there is data, false otherwise. virtual bool has_buffered_data() const; + /// @brief Reallocates the transfer buffer, preserving any existing data. + /// @param new_buffer_size The new size in bytes. Must be at least as large as available(). + /// @return True if successful, false otherwise. On failure, the original buffer remains valid. bool reallocate(size_t new_buffer_size); protected: From f2cb5db9e0dd543d33dd45e561412adf3c616f0e Mon Sep 17 00:00:00 2001 From: "Cornelius A. Ludmann" Date: Mon, 16 Feb 2026 03:44:30 +0100 Subject: [PATCH 039/197] [epaper_spi] Add Waveshare 7.5in e-Paper (H) (#13991) --- .../components/epaper_spi/models/jd79660.py | 32 +++++++++++++++++++ .../epaper_spi/test.esp32-s3-idf.yaml | 17 ++++++++++ 2 files changed, 49 insertions(+) diff --git a/esphome/components/epaper_spi/models/jd79660.py b/esphome/components/epaper_spi/models/jd79660.py index 2d8830ebd2..a0457c5812 100644 --- a/esphome/components/epaper_spi/models/jd79660.py +++ b/esphome/components/epaper_spi/models/jd79660.py @@ -84,3 +84,35 @@ jd79660.extend( (0xA5, 0x00,), ), ) + +# Waveshare 7.5-H +# +# Vendor init derived from vendor sample code +# +# Compatible MIT license, see esphome/LICENSE file. +# +# Note: busy pin uses LOW=busy, HIGH=idle. Configure with inverted: true in YAML. +# +# fmt: off +jd79660.extend( + "Waveshare-7.5in-H", + width=800, + height=480, + + initsequence=( + (0x00, 0x0F, 0x29,), + (0x06, 0x0F, 0x8B, 0x93, 0xA1,), + (0x41, 0x00,), + (0x50, 0x37,), + (0x60, 0x02, 0x02,), + (0x61, 800 // 256, 800 % 256, 480 // 256, 480 % 256,), # RES: 800x480 + (0x62, 0x98, 0x98, 0x98, 0x75, 0xCA, 0xB2, 0x98, 0x7E,), + (0x65, 0x00, 0x00, 0x00, 0x00,), + (0xE7, 0x1C,), + (0xE3, 0x00,), + (0xE9, 0x01,), + (0x30, 0x08,), + # Power On (0x04): Must be early part of init seq = Disabled later! + (0x04,), + ), +) diff --git a/tests/components/epaper_spi/test.esp32-s3-idf.yaml b/tests/components/epaper_spi/test.esp32-s3-idf.yaml index aa454c73fa..9593d0f6f0 100644 --- a/tests/components/epaper_spi/test.esp32-s3-idf.yaml +++ b/tests/components/epaper_spi/test.esp32-s3-idf.yaml @@ -57,6 +57,23 @@ display: allow_other_uses: true number: GPIO4 + - platform: epaper_spi + spi_id: spi_bus + model: waveshare-7.5in-H + cs_pin: + allow_other_uses: true + number: GPIO5 + dc_pin: + allow_other_uses: true + number: GPIO17 + reset_pin: + allow_other_uses: true + number: GPIO16 + busy_pin: + allow_other_uses: true + number: GPIO4 + inverted: true + - platform: epaper_spi model: seeed-reterminal-e1002 - platform: epaper_spi From f2c827f9a2395ac34144bddbb3f9a3edd4c43bd0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 16 Feb 2026 08:08:43 -0600 Subject: [PATCH 040/197] [runtime_image] Remove stored RAMAllocator member (#13998) --- esphome/components/runtime_image/png_decoder.cpp | 6 ++++-- esphome/components/runtime_image/png_decoder.h | 1 - esphome/components/runtime_image/runtime_image.cpp | 8 +++++--- esphome/components/runtime_image/runtime_image.h | 1 - 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/esphome/components/runtime_image/png_decoder.cpp b/esphome/components/runtime_image/png_decoder.cpp index 9fe4a9c4ff..591504328d 100644 --- a/esphome/components/runtime_image/png_decoder.cpp +++ b/esphome/components/runtime_image/png_decoder.cpp @@ -50,7 +50,8 @@ static void draw_callback(pngle_t *pngle, uint32_t x, uint32_t y, uint32_t w, ui PngDecoder::PngDecoder(RuntimeImage *image) : ImageDecoder(image) { { - pngle_t *pngle = this->allocator_.allocate(1, PNGLE_T_SIZE); + RAMAllocator allocator; + pngle_t *pngle = allocator.allocate(1, PNGLE_T_SIZE); if (!pngle) { ESP_LOGE(TAG, "Failed to allocate memory for PNGLE engine!"); return; @@ -64,7 +65,8 @@ PngDecoder::PngDecoder(RuntimeImage *image) : ImageDecoder(image) { PngDecoder::~PngDecoder() { if (this->pngle_) { pngle_reset(this->pngle_); - this->allocator_.deallocate(this->pngle_, PNGLE_T_SIZE); + RAMAllocator allocator; + allocator.deallocate(this->pngle_, PNGLE_T_SIZE); } } diff --git a/esphome/components/runtime_image/png_decoder.h b/esphome/components/runtime_image/png_decoder.h index b5c1e70c2a..24521d33a8 100644 --- a/esphome/components/runtime_image/png_decoder.h +++ b/esphome/components/runtime_image/png_decoder.h @@ -29,7 +29,6 @@ class PngDecoder : public ImageDecoder { uint32_t get_pixels_decoded() const { return this->pixels_decoded_; } protected: - RAMAllocator allocator_; pngle_t *pngle_{nullptr}; uint32_t pixels_decoded_{0}; }; diff --git a/esphome/components/runtime_image/runtime_image.cpp b/esphome/components/runtime_image/runtime_image.cpp index 1d70f38d6b..603ea76f01 100644 --- a/esphome/components/runtime_image/runtime_image.cpp +++ b/esphome/components/runtime_image/runtime_image.cpp @@ -230,7 +230,8 @@ void RuntimeImage::release() { void RuntimeImage::release_buffer_() { if (this->buffer_) { ESP_LOGV(TAG, "Releasing buffer of size %zu", this->get_buffer_size_(this->buffer_width_, this->buffer_height_)); - this->allocator_.deallocate(this->buffer_, this->get_buffer_size_(this->buffer_width_, this->buffer_height_)); + RAMAllocator allocator; + allocator.deallocate(this->buffer_, this->get_buffer_size_(this->buffer_width_, this->buffer_height_)); this->buffer_ = nullptr; this->data_start_ = nullptr; this->width_ = 0; @@ -254,11 +255,12 @@ size_t RuntimeImage::resize_buffer_(int width, int height) { } ESP_LOGD(TAG, "Allocating buffer: %dx%d, %zu bytes", width, height, new_size); - this->buffer_ = this->allocator_.allocate(new_size); + RAMAllocator allocator; + this->buffer_ = allocator.allocate(new_size); if (!this->buffer_) { ESP_LOGE(TAG, "Failed to allocate %zu bytes. Largest free block: %zu", new_size, - this->allocator_.get_max_free_block_size()); + allocator.get_max_free_block_size()); return 0; } diff --git a/esphome/components/runtime_image/runtime_image.h b/esphome/components/runtime_image/runtime_image.h index 0a5279d86d..4bdcdcac9e 100644 --- a/esphome/components/runtime_image/runtime_image.h +++ b/esphome/components/runtime_image/runtime_image.h @@ -165,7 +165,6 @@ class RuntimeImage : public image::Image { std::unique_ptr create_decoder_(); // Memory management - RAMAllocator allocator_{}; uint8_t *buffer_{nullptr}; // Decoder management From ffb9a00e2683d6ad0aad47fc4c6a783ca615e101 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 16 Feb 2026 08:09:13 -0600 Subject: [PATCH 041/197] [online_image] Remove stored RAMAllocator member from DownloadBuffer (#13999) --- esphome/components/online_image/download_buffer.cpp | 10 ++++++---- esphome/components/online_image/download_buffer.h | 6 ++++-- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/esphome/components/online_image/download_buffer.cpp b/esphome/components/online_image/download_buffer.cpp index 999005df82..2ec6365aa1 100644 --- a/esphome/components/online_image/download_buffer.cpp +++ b/esphome/components/online_image/download_buffer.cpp @@ -7,7 +7,8 @@ namespace esphome::online_image { static const char *const TAG = "online_image.download_buffer"; DownloadBuffer::DownloadBuffer(size_t size) : size_(size) { - this->buffer_ = this->allocator_.allocate(size); + RAMAllocator allocator; + this->buffer_ = allocator.allocate(size); this->reset(); if (!this->buffer_) { ESP_LOGE(TAG, "Initial allocation of download buffer failed!"); @@ -38,15 +39,16 @@ size_t DownloadBuffer::resize(size_t size) { // Avoid useless reallocations; if the buffer is big enough, don't reallocate. return this->size_; } - this->allocator_.deallocate(this->buffer_, this->size_); - this->buffer_ = this->allocator_.allocate(size); + RAMAllocator allocator; + allocator.deallocate(this->buffer_, this->size_); + this->buffer_ = allocator.allocate(size); this->reset(); if (this->buffer_) { this->size_ = size; return size; } else { ESP_LOGE(TAG, "allocation of %zu bytes failed. Biggest block in heap: %zu Bytes", size, - this->allocator_.get_max_free_block_size()); + allocator.get_max_free_block_size()); this->size_ = 0; return 0; } diff --git a/esphome/components/online_image/download_buffer.h b/esphome/components/online_image/download_buffer.h index 110a4b608a..73061b23b5 100644 --- a/esphome/components/online_image/download_buffer.h +++ b/esphome/components/online_image/download_buffer.h @@ -15,7 +15,10 @@ namespace esphome::online_image { class DownloadBuffer { public: DownloadBuffer(size_t size); - ~DownloadBuffer() { this->allocator_.deallocate(this->buffer_, this->size_); } + ~DownloadBuffer() { + RAMAllocator allocator; + allocator.deallocate(this->buffer_, this->size_); + } uint8_t *data(size_t offset = 0); uint8_t *append() { return this->data(this->unread_); } @@ -34,7 +37,6 @@ class DownloadBuffer { size_t resize(size_t size); protected: - RAMAllocator allocator_{}; uint8_t *buffer_; size_t size_; /** Total number of downloaded bytes not yet read. */ From 81872d9822d1930e2408110acfcdb8e8e3dfd95a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 16 Feb 2026 08:09:26 -0600 Subject: [PATCH 042/197] [camera, camera_encoder] Remove stored RAMAllocator member (#13997) --- esphome/components/camera/buffer_impl.cpp | 12 ++++++++---- esphome/components/camera/buffer_impl.h | 1 - .../camera_encoder/encoder_buffer_impl.cpp | 9 ++++++--- .../components/camera_encoder/encoder_buffer_impl.h | 1 - 4 files changed, 14 insertions(+), 9 deletions(-) diff --git a/esphome/components/camera/buffer_impl.cpp b/esphome/components/camera/buffer_impl.cpp index d17a4e2707..97cdd2e104 100644 --- a/esphome/components/camera/buffer_impl.cpp +++ b/esphome/components/camera/buffer_impl.cpp @@ -3,18 +3,22 @@ namespace esphome::camera { BufferImpl::BufferImpl(size_t size) { - this->data_ = this->allocator_.allocate(size); + RAMAllocator allocator; + this->data_ = allocator.allocate(size); this->size_ = size; } BufferImpl::BufferImpl(CameraImageSpec *spec) { - this->data_ = this->allocator_.allocate(spec->bytes_per_image()); + RAMAllocator allocator; + this->data_ = allocator.allocate(spec->bytes_per_image()); this->size_ = spec->bytes_per_image(); } BufferImpl::~BufferImpl() { - if (this->data_ != nullptr) - this->allocator_.deallocate(this->data_, this->size_); + if (this->data_ != nullptr) { + RAMAllocator allocator; + allocator.deallocate(this->data_, this->size_); + } } } // namespace esphome::camera diff --git a/esphome/components/camera/buffer_impl.h b/esphome/components/camera/buffer_impl.h index 46398295fa..5e42df7957 100644 --- a/esphome/components/camera/buffer_impl.h +++ b/esphome/components/camera/buffer_impl.h @@ -18,7 +18,6 @@ class BufferImpl : public Buffer { ~BufferImpl() override; protected: - RAMAllocator allocator_; size_t size_{}; uint8_t *data_{}; }; diff --git a/esphome/components/camera_encoder/encoder_buffer_impl.cpp b/esphome/components/camera_encoder/encoder_buffer_impl.cpp index db84026496..f12c66f203 100644 --- a/esphome/components/camera_encoder/encoder_buffer_impl.cpp +++ b/esphome/components/camera_encoder/encoder_buffer_impl.cpp @@ -4,7 +4,8 @@ namespace esphome::camera_encoder { bool EncoderBufferImpl::set_buffer_size(size_t size) { if (size > this->capacity_) { - uint8_t *p = this->allocator_.reallocate(this->data_, size); + RAMAllocator allocator; + uint8_t *p = allocator.reallocate(this->data_, size); if (p == nullptr) return false; @@ -16,8 +17,10 @@ bool EncoderBufferImpl::set_buffer_size(size_t size) { } EncoderBufferImpl::~EncoderBufferImpl() { - if (this->data_ != nullptr) - this->allocator_.deallocate(this->data_, this->capacity_); + if (this->data_ != nullptr) { + RAMAllocator allocator; + allocator.deallocate(this->data_, this->capacity_); + } } } // namespace esphome::camera_encoder diff --git a/esphome/components/camera_encoder/encoder_buffer_impl.h b/esphome/components/camera_encoder/encoder_buffer_impl.h index 13eccb7d56..d394daff14 100644 --- a/esphome/components/camera_encoder/encoder_buffer_impl.h +++ b/esphome/components/camera_encoder/encoder_buffer_impl.h @@ -16,7 +16,6 @@ class EncoderBufferImpl : public camera::EncoderBuffer { ~EncoderBufferImpl() override; protected: - RAMAllocator allocator_; size_t capacity_{}; size_t size_{}; uint8_t *data_{}; From 0c4827d348e4f55807fa910490e59144711ef392 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 16 Feb 2026 08:09:53 -0600 Subject: [PATCH 043/197] [json, core] Remove stored RAMAllocator, make constructors constexpr (#14000) --- esphome/components/json/json_util.h | 11 ++++++----- esphome/core/helpers.h | 11 ++++------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/esphome/components/json/json_util.h b/esphome/components/json/json_util.h index ca074926bd..c472b9a9ec 100644 --- a/esphome/components/json/json_util.h +++ b/esphome/components/json/json_util.h @@ -18,7 +18,10 @@ namespace json { // Build an allocator for the JSON Library using the RAMAllocator class // This is only compiled when PSRAM is enabled struct SpiRamAllocator : ArduinoJson::Allocator { - void *allocate(size_t size) override { return allocator_.allocate(size); } + void *allocate(size_t size) override { + RAMAllocator allocator; + return allocator.allocate(size); + } void deallocate(void *ptr) override { // ArduinoJson's Allocator interface doesn't provide the size parameter in deallocate. @@ -31,11 +34,9 @@ struct SpiRamAllocator : ArduinoJson::Allocator { } void *reallocate(void *ptr, size_t new_size) override { - return allocator_.reallocate(static_cast(ptr), new_size); + RAMAllocator allocator; + return allocator.reallocate(static_cast(ptr), new_size); } - - protected: - RAMAllocator allocator_{RAMAllocator::NONE}; }; #endif diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 34c7452484..298b93fbc4 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -1673,13 +1673,10 @@ template class RAMAllocator { ALLOW_FAILURE = 1 << 2, // Does nothing. Kept for compatibility. }; - RAMAllocator() = default; - RAMAllocator(uint8_t flags) { - // default is both external and internal - flags &= ALLOC_INTERNAL | ALLOC_EXTERNAL; - if (flags != 0) - this->flags_ = flags; - } + constexpr RAMAllocator() = default; + constexpr RAMAllocator(uint8_t flags) + : flags_((flags & (ALLOC_INTERNAL | ALLOC_EXTERNAL)) != 0 ? (flags & (ALLOC_INTERNAL | ALLOC_EXTERNAL)) + : (ALLOC_INTERNAL | ALLOC_EXTERNAL)) {} template constexpr RAMAllocator(const RAMAllocator &other) : flags_{other.flags_} {} T *allocate(size_t n) { return this->allocate(n, sizeof(T)); } From 1517b7799a0e829a33555f02bd49915d53b7dd13 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 14 Feb 2026 08:21:04 -0700 Subject: [PATCH 044/197] [wifi] Fix ESP8266 DHCP state corruption from premature dhcp_renew() (#13983) Co-authored-by: Claude Opus 4.6 --- .../components/wifi/wifi_component_esp8266.cpp | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index c87345f0bf..cbf7d7d80f 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -216,23 +216,16 @@ bool WiFiComponent::wifi_apply_hostname_() { ESP_LOGV(TAG, "Set hostname failed"); } - // inform dhcp server of hostname change using dhcp_renew() + // Update hostname on all lwIP interfaces so DHCP packets include it. + // lwIP includes the hostname in DHCP DISCOVER/REQUEST automatically + // via LWIP_NETIF_HOSTNAME — no dhcp_renew() needed. The hostname is + // fixed at compile time and never changes at runtime. for (netif *intf = netif_list; intf; intf = intf->next) { - // unconditionally update all known interfaces #if LWIP_VERSION_MAJOR == 1 intf->hostname = (char *) wifi_station_get_hostname(); #else intf->hostname = wifi_station_get_hostname(); #endif - if (netif_dhcp_data(intf) != nullptr) { - // renew already started DHCP leases - err_t lwipret = dhcp_renew(intf); - if (lwipret != ERR_OK) { - ESP_LOGW(TAG, "wifi_apply_hostname_(%s): lwIP error %d on interface %c%c (index %d)", intf->hostname, - (int) lwipret, intf->name[0], intf->name[1], intf->num); - ret = false; - } - } } return ret; From f6362aa8da61be4ba14edaf8c4156ba75c5fe1ef Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Sun, 15 Feb 2026 12:11:36 -0500 Subject: [PATCH 045/197] [combination] Fix 'coeffecient' typo with backward-compatible deprecation (#14004) Co-authored-by: Claude Opus 4.6 --- .../components/combination/combination.cpp | 2 +- esphome/components/combination/sensor.py | 46 +++++++++++++++---- tests/components/combination/common.yaml | 4 +- 3 files changed, 40 insertions(+), 12 deletions(-) diff --git a/esphome/components/combination/combination.cpp b/esphome/components/combination/combination.cpp index 716d270390..ece7cca482 100644 --- a/esphome/components/combination/combination.cpp +++ b/esphome/components/combination/combination.cpp @@ -126,7 +126,7 @@ void LinearCombinationComponent::setup() { } void LinearCombinationComponent::handle_new_value(float value) { - // Multiplies each sensor state by a configured coeffecient and then sums + // Multiplies each sensor state by a configured coefficient and then sums if (!std::isfinite(value)) return; diff --git a/esphome/components/combination/sensor.py b/esphome/components/combination/sensor.py index f5255fec03..0204162e8d 100644 --- a/esphome/components/combination/sensor.py +++ b/esphome/components/combination/sensor.py @@ -1,3 +1,5 @@ +import logging + import esphome.codegen as cg from esphome.components import sensor import esphome.config_validation as cv @@ -15,6 +17,8 @@ from esphome.const import ( ) from esphome.core.entity_helpers import inherit_property_from +_LOGGER = logging.getLogger(__name__) + CODEOWNERS = ["@Cat-Ion", "@kahrendt"] combination_ns = cg.esphome_ns.namespace("combination") @@ -47,7 +51,8 @@ SumCombinationComponent = combination_ns.class_( "SumCombinationComponent", cg.Component, sensor.Sensor ) -CONF_COEFFECIENT = "coeffecient" +CONF_COEFFICIENT = "coefficient" +CONF_COEFFECIENT = "coeffecient" # Deprecated, remove before 2026.12.0 CONF_ERROR = "error" CONF_KALMAN = "kalman" CONF_LINEAR = "linear" @@ -68,11 +73,34 @@ KALMAN_SOURCE_SCHEMA = cv.Schema( } ) -LINEAR_SOURCE_SCHEMA = cv.Schema( - { - cv.Required(CONF_SOURCE): cv.use_id(sensor.Sensor), - cv.Required(CONF_COEFFECIENT): cv.templatable(cv.float_), - } + +def _migrate_coeffecient(config): + """Migrate deprecated 'coeffecient' spelling to 'coefficient'.""" + if CONF_COEFFECIENT in config: + if CONF_COEFFICIENT in config: + raise cv.Invalid( + f"Cannot specify both '{CONF_COEFFICIENT}' and '{CONF_COEFFECIENT}'" + ) + _LOGGER.warning( + "'%s' is deprecated, use '%s' instead. Will be removed in 2026.12.0", + CONF_COEFFECIENT, + CONF_COEFFICIENT, + ) + config[CONF_COEFFICIENT] = config.pop(CONF_COEFFECIENT) + elif CONF_COEFFICIENT not in config: + raise cv.Invalid(f"'{CONF_COEFFICIENT}' is a required option") + return config + + +LINEAR_SOURCE_SCHEMA = cv.All( + cv.Schema( + { + cv.Required(CONF_SOURCE): cv.use_id(sensor.Sensor), + cv.Optional(CONF_COEFFICIENT): cv.templatable(cv.float_), + cv.Optional(CONF_COEFFECIENT): cv.templatable(cv.float_), + } + ), + _migrate_coeffecient, ) SENSOR_ONLY_SOURCE_SCHEMA = cv.Schema( @@ -162,12 +190,12 @@ async def to_code(config): ) cg.add(var.add_source(source, error)) elif config[CONF_TYPE] == CONF_LINEAR: - coeffecient = await cg.templatable( - source_conf[CONF_COEFFECIENT], + coefficient = await cg.templatable( + source_conf[CONF_COEFFICIENT], [(float, "x")], cg.float_, ) - cg.add(var.add_source(source, coeffecient)) + cg.add(var.add_source(source, coefficient)) else: cg.add(var.add_source(source)) diff --git a/tests/components/combination/common.yaml b/tests/components/combination/common.yaml index 0e5d512d08..5d46419399 100644 --- a/tests/components/combination/common.yaml +++ b/tests/components/combination/common.yaml @@ -27,9 +27,9 @@ sensor: name: Linearly combined temperatures sources: - source: template_temperature1 - coeffecient: !lambda "return 0.4 + std::abs(x - 25) * 0.023;" + coefficient: !lambda "return 0.4 + std::abs(x - 25) * 0.023;" - source: template_temperature2 - coeffecient: 1.5 + coefficient: 1.5 - platform: combination type: max name: Max of combined temperatures From df29cdbf1742c0676e9bd5870a86d462c0a91dfd Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Sun, 15 Feb 2026 12:11:50 -0500 Subject: [PATCH 046/197] [fan] Fix preset_mode not restored on boot (#14002) Co-authored-by: Claude Opus 4.6 --- esphome/components/fan/fan.cpp | 8 +++++++- esphome/components/fan/fan.h | 4 +++- esphome/components/hbridge/fan/hbridge_fan.cpp | 8 ++++---- esphome/components/speed/fan/speed_fan.cpp | 8 ++++---- esphome/components/template/fan/template_fan.cpp | 10 +++++----- 5 files changed, 23 insertions(+), 15 deletions(-) diff --git a/esphome/components/fan/fan.cpp b/esphome/components/fan/fan.cpp index d70a2940bc..c1e0a3dc2e 100644 --- a/esphome/components/fan/fan.cpp +++ b/esphome/components/fan/fan.cpp @@ -221,12 +221,17 @@ void Fan::publish_state() { } // Random 32-bit value, change this every time the layout of the FanRestoreState struct changes. -constexpr uint32_t RESTORE_STATE_VERSION = 0x71700ABA; +constexpr uint32_t RESTORE_STATE_VERSION = 0x71700ABB; optional Fan::restore_state_() { FanRestoreState recovered{}; this->rtc_ = this->make_entity_preference(RESTORE_STATE_VERSION); bool restored = this->rtc_.load(&recovered); + if (!restored) { + // No valid saved data; ensure preset_mode sentinel is set + recovered.preset_mode = FanRestoreState::NO_PRESET; + } + switch (this->restore_mode_) { case FanRestoreMode::NO_RESTORE: return {}; @@ -264,6 +269,7 @@ void Fan::save_state_() { state.oscillating = this->oscillating; state.speed = this->speed; state.direction = this->direction; + state.preset_mode = FanRestoreState::NO_PRESET; if (this->has_preset_mode()) { const auto &preset_modes = traits.supported_preset_modes(); diff --git a/esphome/components/fan/fan.h b/esphome/components/fan/fan.h index 55d4ba8825..2caf3a712a 100644 --- a/esphome/components/fan/fan.h +++ b/esphome/components/fan/fan.h @@ -91,11 +91,13 @@ class FanCall { }; struct FanRestoreState { + static constexpr uint8_t NO_PRESET = UINT8_MAX; + bool state; int speed; bool oscillating; FanDirection direction; - uint8_t preset_mode; + uint8_t preset_mode{NO_PRESET}; /// Convert this struct to a fan call that can be performed. FanCall to_call(Fan &fan); diff --git a/esphome/components/hbridge/fan/hbridge_fan.cpp b/esphome/components/hbridge/fan/hbridge_fan.cpp index 9bf58f9d1e..38e4129e66 100644 --- a/esphome/components/hbridge/fan/hbridge_fan.cpp +++ b/esphome/components/hbridge/fan/hbridge_fan.cpp @@ -28,15 +28,15 @@ fan::FanCall HBridgeFan::brake() { } void HBridgeFan::setup() { + // Construct traits before restore so preset modes can be looked up by index + this->traits_ = fan::FanTraits(this->oscillating_ != nullptr, true, true, this->speed_count_); + this->traits_.set_supported_preset_modes(this->preset_modes_); + auto restore = this->restore_state_(); if (restore.has_value()) { restore->apply(*this); this->write_state_(); } - - // Construct traits - this->traits_ = fan::FanTraits(this->oscillating_ != nullptr, true, true, this->speed_count_); - this->traits_.set_supported_preset_modes(this->preset_modes_); } void HBridgeFan::dump_config() { diff --git a/esphome/components/speed/fan/speed_fan.cpp b/esphome/components/speed/fan/speed_fan.cpp index af98e3a51f..55f7fd162c 100644 --- a/esphome/components/speed/fan/speed_fan.cpp +++ b/esphome/components/speed/fan/speed_fan.cpp @@ -7,15 +7,15 @@ namespace speed { static const char *const TAG = "speed.fan"; void SpeedFan::setup() { + // Construct traits before restore so preset modes can be looked up by index + this->traits_ = fan::FanTraits(this->oscillating_ != nullptr, true, this->direction_ != nullptr, this->speed_count_); + this->traits_.set_supported_preset_modes(this->preset_modes_); + auto restore = this->restore_state_(); if (restore.has_value()) { restore->apply(*this); this->write_state_(); } - - // Construct traits - this->traits_ = fan::FanTraits(this->oscillating_ != nullptr, true, this->direction_ != nullptr, this->speed_count_); - this->traits_.set_supported_preset_modes(this->preset_modes_); } void SpeedFan::dump_config() { LOG_FAN("", "Speed Fan", this); } diff --git a/esphome/components/template/fan/template_fan.cpp b/esphome/components/template/fan/template_fan.cpp index 0e1920a984..cd267bd552 100644 --- a/esphome/components/template/fan/template_fan.cpp +++ b/esphome/components/template/fan/template_fan.cpp @@ -6,15 +6,15 @@ namespace esphome::template_ { static const char *const TAG = "template.fan"; void TemplateFan::setup() { + // Construct traits before restore so preset modes can be looked up by index + this->traits_ = + fan::FanTraits(this->has_oscillating_, this->speed_count_ > 0, this->has_direction_, this->speed_count_); + this->traits_.set_supported_preset_modes(this->preset_modes_); + auto restore = this->restore_state_(); if (restore.has_value()) { restore->apply(*this); } - - // Construct traits - this->traits_ = - fan::FanTraits(this->has_oscillating_, this->speed_count_ > 0, this->has_direction_, this->speed_count_); - this->traits_.set_supported_preset_modes(this->preset_modes_); } void TemplateFan::dump_config() { LOG_FAN("", "Template Fan", this); } From e945e9b6591602ec98c1db102aeb13586a76ab92 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Sun, 15 Feb 2026 13:23:06 -0500 Subject: [PATCH 047/197] [esp32_rmt] Handle ESP32 variants without RMT hardware (#14001) Co-authored-by: Claude Opus 4.6 --- esphome/components/esp32_rmt/__init__.py | 22 +++++ .../components/esp32_rmt_led_strip/light.py | 6 +- esphome/components/remote_base/remote_base.h | 5 +- .../components/remote_receiver/__init__.py | 41 +++++++- .../remote_receiver/remote_receiver.cpp | 2 +- .../remote_receiver/remote_receiver.h | 17 ++-- .../remote_receiver/remote_receiver_esp32.cpp | 5 +- .../components/remote_transmitter/__init__.py | 93 ++++++++++++------- .../remote_transmitter/automation.h | 6 +- .../remote_transmitter/remote_transmitter.cpp | 8 +- .../remote_transmitter/remote_transmitter.h | 21 +++-- .../remote_transmitter_esp32.cpp | 11 ++- .../remote_receiver/test.esp32-c2-idf.yaml | 12 +++ .../remote_transmitter/test.esp32-c2-idf.yaml | 7 ++ 14 files changed, 181 insertions(+), 75 deletions(-) create mode 100644 tests/components/remote_receiver/test.esp32-c2-idf.yaml create mode 100644 tests/components/remote_transmitter/test.esp32-c2-idf.yaml diff --git a/esphome/components/esp32_rmt/__init__.py b/esphome/components/esp32_rmt/__init__.py index 272c7c81ba..1076bcabdc 100644 --- a/esphome/components/esp32_rmt/__init__.py +++ b/esphome/components/esp32_rmt/__init__.py @@ -1,8 +1,30 @@ from esphome.components import esp32 import esphome.config_validation as cv +from esphome.core import CORE CODEOWNERS = ["@jesserockz"] +VARIANTS_NO_RMT = {esp32.VARIANT_ESP32C2, esp32.VARIANT_ESP32C61} + + +def validate_rmt_not_supported(rmt_only_keys): + """Validate that RMT-only config keys are not used on variants without RMT hardware.""" + rmt_only_keys = set(rmt_only_keys) + + def _validator(config): + if CORE.is_esp32: + variant = esp32.get_esp32_variant() + if variant in VARIANTS_NO_RMT: + unsupported = rmt_only_keys.intersection(config) + if unsupported: + keys = ", ".join(sorted(f"'{k}'" for k in unsupported)) + raise cv.Invalid( + f"{keys} not available on {variant} (no RMT hardware)" + ) + return config + + return _validator + def validate_clock_resolution(): def _validator(value): diff --git a/esphome/components/esp32_rmt_led_strip/light.py b/esphome/components/esp32_rmt_led_strip/light.py index 6d41f6b5b8..1c6943b003 100644 --- a/esphome/components/esp32_rmt_led_strip/light.py +++ b/esphome/components/esp32_rmt_led_strip/light.py @@ -3,7 +3,7 @@ import logging from esphome import pins import esphome.codegen as cg -from esphome.components import esp32, light +from esphome.components import esp32, esp32_rmt, light from esphome.components.const import CONF_USE_PSRAM from esphome.components.esp32 import include_builtin_idf_component import esphome.config_validation as cv @@ -71,6 +71,10 @@ CONF_RESET_LOW = "reset_low" CONFIG_SCHEMA = cv.All( + esp32.only_on_variant( + unsupported=list(esp32_rmt.VARIANTS_NO_RMT), + msg_prefix="ESP32 RMT LED strip", + ), light.ADDRESSABLE_LIGHT_SCHEMA.extend( { cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(ESP32RMTLEDStripLightOutput), diff --git a/esphome/components/remote_base/remote_base.h b/esphome/components/remote_base/remote_base.h index 0cac28506f..d73fff2b0a 100644 --- a/esphome/components/remote_base/remote_base.h +++ b/esphome/components/remote_base/remote_base.h @@ -119,6 +119,8 @@ class RemoteComponentBase { }; #ifdef USE_ESP32 +#include +#if SOC_RMT_SUPPORTED class RemoteRMTChannel { public: void set_clock_resolution(uint32_t clock_resolution) { this->clock_resolution_ = clock_resolution; } @@ -137,7 +139,8 @@ class RemoteRMTChannel { uint32_t clock_resolution_{1000000}; uint32_t rmt_symbols_; }; -#endif +#endif // SOC_RMT_SUPPORTED +#endif // USE_ESP32 class RemoteTransmitterBase : public RemoteComponentBase { public: diff --git a/esphome/components/remote_receiver/__init__.py b/esphome/components/remote_receiver/__init__.py index b3dc213c5f..362f6e99db 100644 --- a/esphome/components/remote_receiver/__init__.py +++ b/esphome/components/remote_receiver/__init__.py @@ -65,6 +65,8 @@ RemoteReceiverComponent = remote_receiver_ns.class_( def validate_config(config): if CORE.is_esp32: variant = esp32.get_esp32_variant() + if variant in esp32_rmt.VARIANTS_NO_RMT: + return config if variant in (esp32.VARIANT_ESP32, esp32.VARIANT_ESP32S2): max_idle = 65535 else: @@ -110,6 +112,8 @@ CONFIG_SCHEMA = remote_base.validate_triggers( cv.SplitDefault( CONF_BUFFER_SIZE, esp32="10000b", + esp32_c2="1000b", + esp32_c61="1000b", esp8266="1000b", bk72xx="1000b", ln882x="1000b", @@ -131,9 +135,11 @@ CONFIG_SCHEMA = remote_base.validate_triggers( cv.SplitDefault( CONF_RMT_SYMBOLS, esp32=192, + esp32_c2=cv.UNDEFINED, esp32_c3=96, esp32_c5=96, esp32_c6=96, + esp32_c61=cv.UNDEFINED, esp32_h2=96, esp32_p4=192, esp32_s2=192, @@ -145,6 +151,8 @@ CONFIG_SCHEMA = remote_base.validate_triggers( cv.SplitDefault( CONF_RECEIVE_SYMBOLS, esp32=192, + esp32_c2=cv.UNDEFINED, + esp32_c61=cv.UNDEFINED, ): cv.All(cv.only_on_esp32, cv.int_range(min=2)), cv.Optional(CONF_USE_DMA): cv.All( esp32.only_on_variant( @@ -152,24 +160,45 @@ CONFIG_SCHEMA = remote_base.validate_triggers( ), cv.boolean, ), - cv.SplitDefault(CONF_CARRIER_DUTY_PERCENT, esp32=100): cv.All( + cv.SplitDefault( + CONF_CARRIER_DUTY_PERCENT, + esp32=100, + esp32_c2=cv.UNDEFINED, + esp32_c61=cv.UNDEFINED, + ): cv.All( cv.only_on_esp32, cv.percentage_int, cv.Range(min=1, max=100), ), - cv.SplitDefault(CONF_CARRIER_FREQUENCY, esp32="0Hz"): cv.All( - cv.only_on_esp32, cv.frequency, cv.int_ - ), + cv.SplitDefault( + CONF_CARRIER_FREQUENCY, + esp32="0Hz", + esp32_c2=cv.UNDEFINED, + esp32_c61=cv.UNDEFINED, + ): cv.All(cv.only_on_esp32, cv.frequency, cv.int_), } ) .extend(cv.COMPONENT_SCHEMA) + .add_extra( + esp32_rmt.validate_rmt_not_supported( + [ + CONF_CLOCK_RESOLUTION, + CONF_USE_DMA, + CONF_RMT_SYMBOLS, + CONF_FILTER_SYMBOLS, + CONF_RECEIVE_SYMBOLS, + CONF_CARRIER_DUTY_PERCENT, + CONF_CARRIER_FREQUENCY, + ] + ) + ) .add_extra(validate_config) ) async def to_code(config): pin = await cg.gpio_pin_expression(config[CONF_PIN]) - if CORE.is_esp32: + if CORE.is_esp32 and esp32.get_esp32_variant() not in esp32_rmt.VARIANTS_NO_RMT: # Re-enable ESP-IDF's RMT driver (excluded by default to save compile time) esp32.include_builtin_idf_component("esp_driver_rmt") @@ -213,6 +242,8 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform( PlatformFramework.ESP32_IDF, }, "remote_receiver.cpp": { + PlatformFramework.ESP32_ARDUINO, + PlatformFramework.ESP32_IDF, PlatformFramework.ESP8266_ARDUINO, PlatformFramework.BK72XX_ARDUINO, PlatformFramework.RTL87XX_ARDUINO, diff --git a/esphome/components/remote_receiver/remote_receiver.cpp b/esphome/components/remote_receiver/remote_receiver.cpp index de47457dac..d59ee63695 100644 --- a/esphome/components/remote_receiver/remote_receiver.cpp +++ b/esphome/components/remote_receiver/remote_receiver.cpp @@ -3,7 +3,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -#if defined(USE_LIBRETINY) || defined(USE_ESP8266) || defined(USE_RP2040) +#if defined(USE_LIBRETINY) || defined(USE_ESP8266) || defined(USE_RP2040) || (defined(USE_ESP32) && !SOC_RMT_SUPPORTED) namespace esphome::remote_receiver { diff --git a/esphome/components/remote_receiver/remote_receiver.h b/esphome/components/remote_receiver/remote_receiver.h index 3d9199a904..5da9283a6e 100644 --- a/esphome/components/remote_receiver/remote_receiver.h +++ b/esphome/components/remote_receiver/remote_receiver.h @@ -6,12 +6,15 @@ #include #if defined(USE_ESP32) +#include +#if SOC_RMT_SUPPORTED #include -#endif +#endif // SOC_RMT_SUPPORTED +#endif // USE_ESP32 namespace esphome::remote_receiver { -#if defined(USE_ESP8266) || defined(USE_LIBRETINY) || defined(USE_RP2040) +#if defined(USE_ESP8266) || defined(USE_LIBRETINY) || defined(USE_RP2040) || (defined(USE_ESP32) && !SOC_RMT_SUPPORTED) struct RemoteReceiverComponentStore { static void gpio_intr(RemoteReceiverComponentStore *arg); @@ -35,7 +38,7 @@ struct RemoteReceiverComponentStore { volatile bool prev_level{false}; volatile bool overflow{false}; }; -#elif defined(USE_ESP32) +#elif defined(USE_ESP32) && SOC_RMT_SUPPORTED struct RemoteReceiverComponentStore { /// Stores RMT symbols and rx done event data volatile uint8_t *buffer{nullptr}; @@ -54,7 +57,7 @@ struct RemoteReceiverComponentStore { class RemoteReceiverComponent : public remote_base::RemoteReceiverBase, public Component -#ifdef USE_ESP32 +#if defined(USE_ESP32) && SOC_RMT_SUPPORTED , public remote_base::RemoteRMTChannel #endif @@ -66,7 +69,7 @@ class RemoteReceiverComponent : public remote_base::RemoteReceiverBase, void dump_config() override; void loop() override; -#ifdef USE_ESP32 +#if defined(USE_ESP32) && SOC_RMT_SUPPORTED void set_filter_symbols(uint32_t filter_symbols) { this->filter_symbols_ = filter_symbols; } void set_receive_symbols(uint32_t receive_symbols) { this->receive_symbols_ = receive_symbols; } void set_with_dma(bool with_dma) { this->with_dma_ = with_dma; } @@ -78,7 +81,7 @@ class RemoteReceiverComponent : public remote_base::RemoteReceiverBase, void set_idle_us(uint32_t idle_us) { this->idle_us_ = idle_us; } protected: -#ifdef USE_ESP32 +#if defined(USE_ESP32) && SOC_RMT_SUPPORTED void decode_rmt_(rmt_symbol_word_t *item, size_t item_count); rmt_channel_handle_t channel_{NULL}; uint32_t filter_symbols_{0}; @@ -94,7 +97,7 @@ class RemoteReceiverComponent : public remote_base::RemoteReceiverBase, RemoteReceiverComponentStore store_; #endif -#if defined(USE_ESP8266) || defined(USE_LIBRETINY) || defined(USE_RP2040) +#if defined(USE_ESP8266) || defined(USE_LIBRETINY) || defined(USE_RP2040) || (defined(USE_ESP32) && !SOC_RMT_SUPPORTED) HighFrequencyLoopRequester high_freq_; #endif diff --git a/esphome/components/remote_receiver/remote_receiver_esp32.cpp b/esphome/components/remote_receiver/remote_receiver_esp32.cpp index f95244ea45..357a36d052 100644 --- a/esphome/components/remote_receiver/remote_receiver_esp32.cpp +++ b/esphome/components/remote_receiver/remote_receiver_esp32.cpp @@ -2,6 +2,8 @@ #include "esphome/core/log.h" #ifdef USE_ESP32 +#include +#if SOC_RMT_SUPPORTED #include #include @@ -248,4 +250,5 @@ void RemoteReceiverComponent::decode_rmt_(rmt_symbol_word_t *item, size_t item_c } // namespace esphome::remote_receiver -#endif +#endif // SOC_RMT_SUPPORTED +#endif // USE_ESP32 diff --git a/esphome/components/remote_transmitter/__init__.py b/esphome/components/remote_transmitter/__init__.py index 8383b9dd75..fc772f88b2 100644 --- a/esphome/components/remote_transmitter/__init__.py +++ b/esphome/components/remote_transmitter/__init__.py @@ -40,45 +40,66 @@ DigitalWriteAction = remote_transmitter_ns.class_( cg.Parented.template(RemoteTransmitterComponent), ) + MULTI_CONF = True -CONFIG_SCHEMA = cv.Schema( - { - cv.GenerateID(): cv.declare_id(RemoteTransmitterComponent), - cv.Required(CONF_PIN): pins.gpio_output_pin_schema, - cv.Required(CONF_CARRIER_DUTY_PERCENT): cv.All( - cv.percentage_int, cv.Range(min=1, max=100) - ), - cv.Optional(CONF_CLOCK_RESOLUTION): cv.All( - cv.only_on_esp32, - esp32_rmt.validate_clock_resolution(), - ), - cv.Optional(CONF_EOT_LEVEL): cv.All(cv.only_on_esp32, cv.boolean), - cv.Optional(CONF_USE_DMA): cv.All( - esp32.only_on_variant( - supported=[esp32.VARIANT_ESP32P4, esp32.VARIANT_ESP32S3] +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(RemoteTransmitterComponent), + cv.Required(CONF_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_CARRIER_DUTY_PERCENT): cv.All( + cv.percentage_int, cv.Range(min=1, max=100) ), - cv.boolean, - ), - cv.SplitDefault( - CONF_RMT_SYMBOLS, - esp32=64, - esp32_c3=48, - esp32_c5=48, - esp32_c6=48, - esp32_h2=48, - esp32_p4=48, - esp32_s2=64, - esp32_s3=48, - ): cv.All(cv.only_on_esp32, cv.int_range(min=2)), - cv.Optional(CONF_NON_BLOCKING): cv.All(cv.only_on_esp32, cv.boolean), - cv.Optional(CONF_ON_TRANSMIT): automation.validate_automation(single=True), - cv.Optional(CONF_ON_COMPLETE): automation.validate_automation(single=True), - } -).extend(cv.COMPONENT_SCHEMA) + cv.Optional(CONF_CLOCK_RESOLUTION): cv.All( + cv.only_on_esp32, + esp32_rmt.validate_clock_resolution(), + ), + cv.Optional(CONF_EOT_LEVEL): cv.All(cv.only_on_esp32, cv.boolean), + cv.Optional(CONF_USE_DMA): cv.All( + esp32.only_on_variant( + supported=[esp32.VARIANT_ESP32P4, esp32.VARIANT_ESP32S3] + ), + cv.boolean, + ), + cv.SplitDefault( + CONF_RMT_SYMBOLS, + esp32=64, + esp32_c2=cv.UNDEFINED, + esp32_c3=48, + esp32_c5=48, + esp32_c6=48, + esp32_c61=cv.UNDEFINED, + esp32_h2=48, + esp32_p4=48, + esp32_s2=64, + esp32_s3=48, + ): cv.All(cv.only_on_esp32, cv.int_range(min=2)), + cv.Optional(CONF_NON_BLOCKING): cv.All(cv.only_on_esp32, cv.boolean), + cv.Optional(CONF_ON_TRANSMIT): automation.validate_automation(single=True), + cv.Optional(CONF_ON_COMPLETE): automation.validate_automation(single=True), + } + ) + .extend(cv.COMPONENT_SCHEMA) + .add_extra( + esp32_rmt.validate_rmt_not_supported( + [ + CONF_CLOCK_RESOLUTION, + CONF_EOT_LEVEL, + CONF_USE_DMA, + CONF_RMT_SYMBOLS, + CONF_NON_BLOCKING, + ] + ) + ) +) def _validate_non_blocking(config): - if CORE.is_esp32 and CONF_NON_BLOCKING not in config: + if ( + CORE.is_esp32 + and esp32.get_esp32_variant() not in esp32_rmt.VARIANTS_NO_RMT + and CONF_NON_BLOCKING not in config + ): _LOGGER.warning( "'non_blocking' is not set for 'remote_transmitter' and will default to 'true'.\n" "The default behavior changed in 2025.11.0; previously blocking mode was used.\n" @@ -111,7 +132,7 @@ async def digital_write_action_to_code(config, action_id, template_arg, args): async def to_code(config): pin = await cg.gpio_pin_expression(config[CONF_PIN]) - if CORE.is_esp32: + if CORE.is_esp32 and esp32.get_esp32_variant() not in esp32_rmt.VARIANTS_NO_RMT: # Re-enable ESP-IDF's RMT driver (excluded by default to save compile time) esp32.include_builtin_idf_component("esp_driver_rmt") @@ -155,6 +176,8 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform( PlatformFramework.ESP32_IDF, }, "remote_transmitter.cpp": { + PlatformFramework.ESP32_ARDUINO, + PlatformFramework.ESP32_IDF, PlatformFramework.ESP8266_ARDUINO, PlatformFramework.BK72XX_ARDUINO, PlatformFramework.RTL87XX_ARDUINO, diff --git a/esphome/components/remote_transmitter/automation.h b/esphome/components/remote_transmitter/automation.h index bee1d0be8a..8da4cfd95d 100644 --- a/esphome/components/remote_transmitter/automation.h +++ b/esphome/components/remote_transmitter/automation.h @@ -5,8 +5,7 @@ #include "esphome/core/component.h" #include "esphome/core/helpers.h" -namespace esphome { -namespace remote_transmitter { +namespace esphome::remote_transmitter { template class DigitalWriteAction : public Action, public Parented { public: @@ -14,5 +13,4 @@ template class DigitalWriteAction : public Action, public void play(const Ts &...x) override { this->parent_->digital_write(this->value_.value(x...)); } }; -} // namespace remote_transmitter -} // namespace esphome +} // namespace esphome::remote_transmitter diff --git a/esphome/components/remote_transmitter/remote_transmitter.cpp b/esphome/components/remote_transmitter/remote_transmitter.cpp index d35541e2e1..51a3c0b1d4 100644 --- a/esphome/components/remote_transmitter/remote_transmitter.cpp +++ b/esphome/components/remote_transmitter/remote_transmitter.cpp @@ -2,10 +2,9 @@ #include "esphome/core/log.h" #include "esphome/core/application.h" -#if defined(USE_LIBRETINY) || defined(USE_ESP8266) || defined(USE_RP2040) +#if defined(USE_LIBRETINY) || defined(USE_ESP8266) || defined(USE_RP2040) || (defined(USE_ESP32) && !SOC_RMT_SUPPORTED) -namespace esphome { -namespace remote_transmitter { +namespace esphome::remote_transmitter { static const char *const TAG = "remote_transmitter"; @@ -105,7 +104,6 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen this->complete_trigger_.trigger(); } -} // namespace remote_transmitter -} // namespace esphome +} // namespace esphome::remote_transmitter #endif diff --git a/esphome/components/remote_transmitter/remote_transmitter.h b/esphome/components/remote_transmitter/remote_transmitter.h index 65bd2ac8b2..aee52ea170 100644 --- a/esphome/components/remote_transmitter/remote_transmitter.h +++ b/esphome/components/remote_transmitter/remote_transmitter.h @@ -6,13 +6,15 @@ #include #if defined(USE_ESP32) +#include +#if SOC_RMT_SUPPORTED #include -#endif +#endif // SOC_RMT_SUPPORTED +#endif // USE_ESP32 -namespace esphome { -namespace remote_transmitter { +namespace esphome::remote_transmitter { -#ifdef USE_ESP32 +#if defined(USE_ESP32) && SOC_RMT_SUPPORTED #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 1) // IDF version 5.5.1 and above is required because of a bug in // the RMT encoder: https://github.com/espressif/esp-idf/issues/17244 @@ -33,7 +35,7 @@ struct RemoteTransmitterComponentStore { class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase, public Component -#ifdef USE_ESP32 +#if defined(USE_ESP32) && SOC_RMT_SUPPORTED , public remote_base::RemoteRMTChannel #endif @@ -51,7 +53,7 @@ class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase, void digital_write(bool value); -#if defined(USE_ESP32) +#if defined(USE_ESP32) && SOC_RMT_SUPPORTED void set_with_dma(bool with_dma) { this->with_dma_ = with_dma; } void set_eot_level(bool eot_level) { this->eot_level_ = eot_level; } void set_non_blocking(bool non_blocking) { this->non_blocking_ = non_blocking; } @@ -62,7 +64,7 @@ class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase, protected: void send_internal(uint32_t send_times, uint32_t send_wait) override; -#if defined(USE_ESP8266) || defined(USE_LIBRETINY) || defined(USE_RP2040) +#if defined(USE_ESP8266) || defined(USE_LIBRETINY) || defined(USE_RP2040) || (defined(USE_ESP32) && !SOC_RMT_SUPPORTED) void calculate_on_off_time_(uint32_t carrier_frequency, uint32_t *on_time_period, uint32_t *off_time_period); void mark_(uint32_t on_time, uint32_t off_time, uint32_t usec); @@ -73,7 +75,7 @@ class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase, uint32_t target_time_; #endif -#ifdef USE_ESP32 +#if defined(USE_ESP32) && SOC_RMT_SUPPORTED void configure_rmt_(); void wait_for_rmt_(); @@ -100,5 +102,4 @@ class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase, Trigger<> complete_trigger_; }; -} // namespace remote_transmitter -} // namespace esphome +} // namespace esphome::remote_transmitter diff --git a/esphome/components/remote_transmitter/remote_transmitter_esp32.cpp b/esphome/components/remote_transmitter/remote_transmitter_esp32.cpp index 89d97895b2..71773e3ddf 100644 --- a/esphome/components/remote_transmitter/remote_transmitter_esp32.cpp +++ b/esphome/components/remote_transmitter/remote_transmitter_esp32.cpp @@ -3,10 +3,11 @@ #include "esphome/core/application.h" #ifdef USE_ESP32 +#include +#if SOC_RMT_SUPPORTED #include -namespace esphome { -namespace remote_transmitter { +namespace esphome::remote_transmitter { static const char *const TAG = "remote_transmitter"; @@ -358,7 +359,7 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen } #endif -} // namespace remote_transmitter -} // namespace esphome +} // namespace esphome::remote_transmitter -#endif +#endif // SOC_RMT_SUPPORTED +#endif // USE_ESP32 diff --git a/tests/components/remote_receiver/test.esp32-c2-idf.yaml b/tests/components/remote_receiver/test.esp32-c2-idf.yaml new file mode 100644 index 0000000000..87154e19fc --- /dev/null +++ b/tests/components/remote_receiver/test.esp32-c2-idf.yaml @@ -0,0 +1,12 @@ +remote_receiver: + id: rcvr + pin: GPIO2 + dump: all + <<: !include common-actions.yaml + +binary_sensor: + - platform: remote_receiver + name: Panasonic Remote Input + panasonic: + address: 0x4004 + command: 0x100BCBD diff --git a/tests/components/remote_transmitter/test.esp32-c2-idf.yaml b/tests/components/remote_transmitter/test.esp32-c2-idf.yaml new file mode 100644 index 0000000000..424cd8d249 --- /dev/null +++ b/tests/components/remote_transmitter/test.esp32-c2-idf.yaml @@ -0,0 +1,7 @@ +remote_transmitter: + id: xmitr + pin: GPIO2 + carrier_duty_percent: 50% + +packages: + buttons: !include common-buttons.yaml From 5904808804b1f86e30583eb3ebbab0eb461905e0 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 17 Feb 2026 13:24:35 +1300 Subject: [PATCH 048/197] Bump version to 2026.2.0b3 --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index 572e20a694..b43b5a428e 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 = 2026.2.0b2 +PROJECT_NUMBER = 2026.2.0b3 # 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 247b2b7e4e..fd916da92b 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2026.2.0b2" +__version__ = "2026.2.0b3" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 4cd3f6c36a5485d6efa570e71cd861d705e7ac29 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 16 Feb 2026 21:30:57 -0600 Subject: [PATCH 049/197] [api] Remove unused reserve from APIServer constructor (#14017) --- esphome/components/api/api_server.cpp | 6 +----- esphome/components/api/api_server.h | 6 +++++- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index f25a9bc0e2..67a117e68f 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -28,11 +28,7 @@ static const char *const TAG = "api"; // APIServer APIServer *global_api_server = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -APIServer::APIServer() { - global_api_server = this; - // Pre-allocate shared write buffer - shared_write_buffer_.reserve(64); -} +APIServer::APIServer() { global_api_server = this; } void APIServer::setup() { ControllerRegistry::register_controller(this); diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index 28f60343e0..323acc2efb 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -268,7 +268,11 @@ class APIServer : public Component, // Vectors and strings (12 bytes each on 32-bit) std::vector> clients_; - std::vector shared_write_buffer_; // Shared proto write buffer for all connections + // Shared proto write buffer for all connections. + // Not pre-allocated: all send paths call prepare_first_message_buffer() which + // reserves the exact needed size. Pre-allocating here would cause heap fragmentation + // since the buffer would almost always reallocate on first use. + std::vector shared_write_buffer_; #ifdef USE_API_HOMEASSISTANT_STATES std::vector state_subs_; #endif From e826d71bd85b7a91b683c0502846fbcc9b9d3f02 Mon Sep 17 00:00:00 2001 From: schrob <83939986+schdro@users.noreply.github.com> Date: Tue, 17 Feb 2026 16:16:57 +0100 Subject: [PATCH 050/197] [openthread] Fix compiler format warning (#14030) --- esphome/components/openthread/openthread_esp.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/openthread/openthread_esp.cpp b/esphome/components/openthread/openthread_esp.cpp index 79cd809809..ec212d1f68 100644 --- a/esphome/components/openthread/openthread_esp.cpp +++ b/esphome/components/openthread/openthread_esp.cpp @@ -115,7 +115,7 @@ void OpenThreadComponent::ot_main() { ESP_LOGE(TAG, "Failed to set OpenThread pollperiod."); } uint32_t link_polling_period = otLinkGetPollPeriod(esp_openthread_get_instance()); - ESP_LOGD(TAG, "Link Polling Period: %d", link_polling_period); + ESP_LOGD(TAG, "Link Polling Period: %" PRIu32, link_polling_period); } link_mode_config.mRxOnWhenIdle = this->poll_period == 0; link_mode_config.mDeviceType = false; From 81ed70325c164612878457033c5f08b27b19f331 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Mart=C3=ADn?= Date: Tue, 17 Feb 2026 18:45:21 +0100 Subject: [PATCH 051/197] [esp32_ble_server] fix infinitely large characteristic value (#14011) --- .../components/esp32_ble_server/__init__.py | 2 +- .../esp32_ble_server/ble_characteristic.cpp | 28 +++++++++++++++---- .../esp32_ble_server/ble_characteristic.h | 1 - 3 files changed, 24 insertions(+), 7 deletions(-) diff --git a/esphome/components/esp32_ble_server/__init__.py b/esphome/components/esp32_ble_server/__init__.py index a7e2522fac..cb494ed1bc 100644 --- a/esphome/components/esp32_ble_server/__init__.py +++ b/esphome/components/esp32_ble_server/__init__.py @@ -527,7 +527,7 @@ async def to_code_characteristic(service_var, char_conf): action_conf, char_conf[CONF_CHAR_VALUE_ACTION_ID_], cg.TemplateArguments(), - {}, + [], ) cg.add(value_action.play()) else: diff --git a/esphome/components/esp32_ble_server/ble_characteristic.cpp b/esphome/components/esp32_ble_server/ble_characteristic.cpp index 0482848ea0..a1b1ff94bb 100644 --- a/esphome/components/esp32_ble_server/ble_characteristic.cpp +++ b/esphome/components/esp32_ble_server/ble_characteristic.cpp @@ -246,9 +246,27 @@ void BLECharacteristic::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt if (this->handle_ != param->write.handle) break; + esp_gatt_status_t status = ESP_GATT_OK; + if (param->write.is_prep) { - this->value_.insert(this->value_.end(), param->write.value, param->write.value + param->write.len); - this->write_event_ = true; + const size_t offset = param->write.offset; + const size_t write_len = param->write.len; + const size_t new_size = offset + write_len; + // Clean the buffer on the first prepared write event + if (offset == 0) { + this->value_.clear(); + } + + if (offset != this->value_.size()) { + status = ESP_GATT_INVALID_OFFSET; + } else if (new_size > ESP_GATT_MAX_ATTR_LEN) { + status = ESP_GATT_INVALID_ATTR_LEN; + } else { + if (this->value_.size() < new_size) { + this->value_.resize(new_size); + } + memcpy(this->value_.data() + offset, param->write.value, write_len); + } } else { this->set_value(ByteBuffer::wrap(param->write.value, param->write.len)); } @@ -263,7 +281,7 @@ void BLECharacteristic::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt memcpy(response.attr_value.value, param->write.value, param->write.len); esp_err_t err = - esp_ble_gatts_send_response(gatts_if, param->write.conn_id, param->write.trans_id, ESP_GATT_OK, &response); + esp_ble_gatts_send_response(gatts_if, param->write.conn_id, param->write.trans_id, status, &response); if (err != ESP_OK) { ESP_LOGE(TAG, "esp_ble_gatts_send_response failed: %d", err); @@ -280,9 +298,9 @@ void BLECharacteristic::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt } case ESP_GATTS_EXEC_WRITE_EVT: { - if (!this->write_event_) + // BLE stack will guarantee that ESP_GATTS_EXEC_WRITE_EVT is only received after prepared writes + if (this->value_.empty()) break; - this->write_event_ = false; if (param->exec_write.exec_write_flag == ESP_GATT_PREP_WRITE_EXEC) { if (this->on_write_callback_) { (*this->on_write_callback_)(this->value_, param->exec_write.conn_id); diff --git a/esphome/components/esp32_ble_server/ble_characteristic.h b/esphome/components/esp32_ble_server/ble_characteristic.h index b913915789..c2cdb1660c 100644 --- a/esphome/components/esp32_ble_server/ble_characteristic.h +++ b/esphome/components/esp32_ble_server/ble_characteristic.h @@ -77,7 +77,6 @@ class BLECharacteristic { } protected: - bool write_event_{false}; BLEService *service_{}; ESPBTUUID uuid_; esp_gatt_char_prop_t properties_; From 5bb863f7dadadedf3c8620d1e275d7d354fedae8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 17 Feb 2026 13:24:39 -0600 Subject: [PATCH 052/197] Bump actions/stale from 10.1.1 to 10.2.0 (#14036) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/stale.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 7e03e2a5f9..ba5c32e016 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -19,7 +19,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Stale - uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1 + uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0 with: debug-only: ${{ github.ref != 'refs/heads/dev' }} # Dry-run when not run on dev branch remove-stale-when-updated: true From a0c4fa649640eb7618049462765e293d6458c48d Mon Sep 17 00:00:00 2001 From: schrob <83939986+schdro@users.noreply.github.com> Date: Tue, 17 Feb 2026 16:16:57 +0100 Subject: [PATCH 053/197] [openthread] Fix compiler format warning (#14030) --- esphome/components/openthread/openthread_esp.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/openthread/openthread_esp.cpp b/esphome/components/openthread/openthread_esp.cpp index 79cd809809..ec212d1f68 100644 --- a/esphome/components/openthread/openthread_esp.cpp +++ b/esphome/components/openthread/openthread_esp.cpp @@ -115,7 +115,7 @@ void OpenThreadComponent::ot_main() { ESP_LOGE(TAG, "Failed to set OpenThread pollperiod."); } uint32_t link_polling_period = otLinkGetPollPeriod(esp_openthread_get_instance()); - ESP_LOGD(TAG, "Link Polling Period: %d", link_polling_period); + ESP_LOGD(TAG, "Link Polling Period: %" PRIu32, link_polling_period); } link_mode_config.mRxOnWhenIdle = this->poll_period == 0; link_mode_config.mDeviceType = false; From d9f493ab7ae31e199a94939a9e189b2bc105ea87 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 18 Feb 2026 10:13:41 +1300 Subject: [PATCH 054/197] Bump version to 2026.2.0b4 --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index b43b5a428e..f9deb32899 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 = 2026.2.0b3 +PROJECT_NUMBER = 2026.2.0b4 # 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 fd916da92b..f494b5d41e 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2026.2.0b3" +__version__ = "2026.2.0b4" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From fb35ddebb9915f6bda82e4050943ee5de1ba0360 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 08:22:07 -0600 Subject: [PATCH 055/197] [display] Make COLOR_OFF and COLOR_ON inline constexpr (#14044) --- esphome/components/display/display.cpp | 3 +-- esphome/components/display/display.h | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/esphome/components/display/display.cpp b/esphome/components/display/display.cpp index ebc3c0a9f6..53a087803c 100644 --- a/esphome/components/display/display.cpp +++ b/esphome/components/display/display.cpp @@ -9,8 +9,7 @@ namespace esphome { namespace display { static const char *const TAG = "display"; -const Color COLOR_OFF(0, 0, 0, 0); -const Color COLOR_ON(255, 255, 255, 255); +// COLOR_OFF and COLOR_ON are now inline constexpr in display.h void Display::fill(Color color) { this->filled_rectangle(0, 0, this->get_width(), this->get_height(), color); } void Display::clear() { this->fill(COLOR_OFF); } diff --git a/esphome/components/display/display.h b/esphome/components/display/display.h index 47d40915aa..e40f6ec963 100644 --- a/esphome/components/display/display.h +++ b/esphome/components/display/display.h @@ -298,9 +298,9 @@ using display_writer_t = DisplayWriter; } /// Turn the pixel OFF. -extern const Color COLOR_OFF; +inline constexpr Color COLOR_OFF(0, 0, 0, 0); /// Turn the pixel ON. -extern const Color COLOR_ON; +inline constexpr Color COLOR_ON(255, 255, 255, 255); class BaseImage { public: From fb89900c64f2a88d6f86feaa43e453af8d83b53c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 08:22:36 -0600 Subject: [PATCH 056/197] [core] Make setup_priority and component state constants constexpr (#14041) --- esphome/core/component.cpp | 32 ++--------------------- esphome/core/component.h | 52 +++++++++++++++++++------------------- 2 files changed, 28 insertions(+), 56 deletions(-) diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index 90aa36f4db..a452f4d400 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -76,36 +76,8 @@ void store_component_error_message(const Component *component, const char *messa } } // namespace -namespace setup_priority { - -const float BUS = 1000.0f; -const float IO = 900.0f; -const float HARDWARE = 800.0f; -const float DATA = 600.0f; -const float PROCESSOR = 400.0; -const float BLUETOOTH = 350.0f; -const float AFTER_BLUETOOTH = 300.0f; -const float WIFI = 250.0f; -const float ETHERNET = 250.0f; -const float BEFORE_CONNECTION = 220.0f; -const float AFTER_WIFI = 200.0f; -const float AFTER_CONNECTION = 100.0f; -const float LATE = -100.0f; - -} // namespace setup_priority - -// Component state uses bits 0-2 (8 states, 5 used) -const uint8_t COMPONENT_STATE_MASK = 0x07; -const uint8_t COMPONENT_STATE_CONSTRUCTION = 0x00; -const uint8_t COMPONENT_STATE_SETUP = 0x01; -const uint8_t COMPONENT_STATE_LOOP = 0x02; -const uint8_t COMPONENT_STATE_FAILED = 0x03; -const uint8_t COMPONENT_STATE_LOOP_DONE = 0x04; -// Status LED uses bits 3-4 -const uint8_t STATUS_LED_MASK = 0x18; -const uint8_t STATUS_LED_OK = 0x00; -const uint8_t STATUS_LED_WARNING = 0x08; // Bit 3 -const uint8_t STATUS_LED_ERROR = 0x10; // Bit 4 +// setup_priority, component state, and status LED constants are now +// constexpr in component.h const uint16_t WARN_IF_BLOCKING_OVER_MS = 50U; ///< Initial blocking time allowed without warning const uint16_t WARN_IF_BLOCKING_INCREMENT_MS = 10U; ///< How long the blocking time must be larger to warn again diff --git a/esphome/core/component.h b/esphome/core/component.h index 9ab77cc2f9..848bc0ba35 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -21,33 +21,31 @@ struct LogString; namespace setup_priority { /// For communication buses like i2c/spi -extern const float BUS; +inline constexpr float BUS = 1000.0f; /// For components that represent GPIO pins like PCF8573 -extern const float IO; +inline constexpr float IO = 900.0f; /// For components that deal with hardware and are very important like GPIO switch -extern const float HARDWARE; +inline constexpr float HARDWARE = 800.0f; /// For components that import data from directly connected sensors like DHT. -extern const float DATA; -/// Alias for DATA (here for compatibility reasons) -extern const float HARDWARE_LATE; +inline constexpr float DATA = 600.0f; /// For components that use data from sensors like displays -extern const float PROCESSOR; -extern const float BLUETOOTH; -extern const float AFTER_BLUETOOTH; -extern const float WIFI; -extern const float ETHERNET; +inline constexpr float PROCESSOR = 400.0f; +inline constexpr float BLUETOOTH = 350.0f; +inline constexpr float AFTER_BLUETOOTH = 300.0f; +inline constexpr float WIFI = 250.0f; +inline constexpr float ETHERNET = 250.0f; /// For components that should be initialized after WiFi and before API is connected. -extern const float BEFORE_CONNECTION; +inline constexpr float BEFORE_CONNECTION = 220.0f; /// For components that should be initialized after WiFi is connected. -extern const float AFTER_WIFI; +inline constexpr float AFTER_WIFI = 200.0f; /// For components that should be initialized after a data connection (API/MQTT) is connected. -extern const float AFTER_CONNECTION; +inline constexpr float AFTER_CONNECTION = 100.0f; /// For components that should be initialized at the very end of the setup process. -extern const float LATE; +inline constexpr float LATE = -100.0f; } // namespace setup_priority -static const uint32_t SCHEDULER_DONT_RUN = 4294967295UL; +inline constexpr uint32_t SCHEDULER_DONT_RUN = 4294967295UL; /// Type-safe scheduler IDs for core base classes. /// Uses a separate NameType (NUMERIC_ID_INTERNAL) so IDs can never collide @@ -65,16 +63,18 @@ void log_update_interval(const char *tag, PollingComponent *component); #define LOG_UPDATE_INTERVAL(this) log_update_interval(TAG, this) -extern const uint8_t COMPONENT_STATE_MASK; -extern const uint8_t COMPONENT_STATE_CONSTRUCTION; -extern const uint8_t COMPONENT_STATE_SETUP; -extern const uint8_t COMPONENT_STATE_LOOP; -extern const uint8_t COMPONENT_STATE_FAILED; -extern const uint8_t COMPONENT_STATE_LOOP_DONE; -extern const uint8_t STATUS_LED_MASK; -extern const uint8_t STATUS_LED_OK; -extern const uint8_t STATUS_LED_WARNING; -extern const uint8_t STATUS_LED_ERROR; +// Component state uses bits 0-2 (8 states, 5 used) +inline constexpr uint8_t COMPONENT_STATE_MASK = 0x07; +inline constexpr uint8_t COMPONENT_STATE_CONSTRUCTION = 0x00; +inline constexpr uint8_t COMPONENT_STATE_SETUP = 0x01; +inline constexpr uint8_t COMPONENT_STATE_LOOP = 0x02; +inline constexpr uint8_t COMPONENT_STATE_FAILED = 0x03; +inline constexpr uint8_t COMPONENT_STATE_LOOP_DONE = 0x04; +// Status LED uses bits 3-4 +inline constexpr uint8_t STATUS_LED_MASK = 0x18; +inline constexpr uint8_t STATUS_LED_OK = 0x00; +inline constexpr uint8_t STATUS_LED_WARNING = 0x08; +inline constexpr uint8_t STATUS_LED_ERROR = 0x10; // Remove before 2026.8.0 enum class RetryResult { DONE, RETRY }; From 652c669777ecaa125f575aa7eac72bf7f5cc5a1d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 18 Feb 2026 09:08:02 -0600 Subject: [PATCH 057/197] Bump pillow from 11.3.0 to 12.1.1 (#14048) Signed-off-by: dependabot[bot] 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 a0a29ad30a..77d30ecd3d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -18,7 +18,7 @@ puremagic==1.30 ruamel.yaml==0.19.1 # dashboard_import ruamel.yaml.clib==0.2.15 # dashboard_import esphome-glyphsets==0.2.0 -pillow==11.3.0 +pillow==12.1.1 resvg-py==0.2.6 freetype-py==2.5.1 jinja2==3.1.6 From f73bcc0e7bebba965a76859f680ac16681833a4f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 18 Feb 2026 09:08:12 -0600 Subject: [PATCH 058/197] Bump cryptography from 45.0.1 to 46.0.5 (#14049) Signed-off-by: dependabot[bot] 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 77d30ecd3d..2e6268284a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -cryptography==45.0.1 +cryptography==46.0.5 voluptuous==0.16.0 PyYAML==6.0.3 paho-mqtt==1.6.1 From 9cd7b0b32b1a4c27a3b0673b1d10187e69e55109 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 18 Feb 2026 13:09:33 -0500 Subject: [PATCH 059/197] [external_components] Clean up incomplete clone on failed ref fetch (#14051) Co-authored-by: Claude Opus 4.6 --- esphome/components/esp32/__init__.py | 4 +- esphome/git.py | 47 ++++++----- esphome/helpers.py | 20 ++++- esphome/writer.py | 27 +------ tests/unit_tests/test_git.py | 113 ++++++++++++++++++++++++++- 5 files changed, 162 insertions(+), 49 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index b78b945a24..8b3e1afea6 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -44,9 +44,9 @@ from esphome.const import ( from esphome.core import CORE, HexInt, TimePeriod from esphome.coroutine import CoroPriority, coroutine_with_priority import esphome.final_validate as fv -from esphome.helpers import copy_file_if_changed, write_file_if_changed +from esphome.helpers import copy_file_if_changed, rmtree, write_file_if_changed from esphome.types import ConfigType -from esphome.writer import clean_cmake_cache, rmtree +from esphome.writer import clean_cmake_cache from .boards import BOARDS, STANDARD_BOARDS from .const import ( # noqa diff --git a/esphome/git.py b/esphome/git.py index 4ff07ffe75..a45768b5cd 100644 --- a/esphome/git.py +++ b/esphome/git.py @@ -5,12 +5,12 @@ import hashlib import logging from pathlib import Path import re -import shutil import subprocess import urllib.parse import esphome.config_validation as cv from esphome.core import CORE, TimePeriodSeconds +from esphome.helpers import rmtree _LOGGER = logging.getLogger(__name__) @@ -115,24 +115,35 @@ def clone_or_update( if not repo_dir.is_dir(): _LOGGER.info("Cloning %s", key) _LOGGER.debug("Location: %s", repo_dir) - cmd = ["git", "clone", "--depth=1"] - cmd += ["--", url, str(repo_dir)] - run_git_command(cmd) + try: + cmd = ["git", "clone", "--depth=1"] + cmd += ["--", url, str(repo_dir)] + run_git_command(cmd) - if ref is not None: - # We need to fetch the PR branch first, otherwise git will complain - # about missing objects - _LOGGER.info("Fetching %s", ref) - run_git_command(["git", "fetch", "--", "origin", ref], git_dir=repo_dir) - run_git_command(["git", "reset", "--hard", "FETCH_HEAD"], git_dir=repo_dir) + if ref is not None: + # We need to fetch the PR branch first, otherwise git will complain + # about missing objects + _LOGGER.info("Fetching %s", ref) + run_git_command(["git", "fetch", "--", "origin", ref], git_dir=repo_dir) + run_git_command( + ["git", "reset", "--hard", "FETCH_HEAD"], git_dir=repo_dir + ) - if submodules is not None: - _LOGGER.info( - "Initializing submodules (%s) for %s", ", ".join(submodules), key - ) - run_git_command( - ["git", "submodule", "update", "--init"] + submodules, git_dir=repo_dir - ) + if submodules is not None: + _LOGGER.info( + "Initializing submodules (%s) for %s", ", ".join(submodules), key + ) + run_git_command( + ["git", "submodule", "update", "--init"] + submodules, + git_dir=repo_dir, + ) + except GitException: + # Remove incomplete clone to prevent stale state. Without this, + # a failed ref fetch leaves a clone on the default branch, and + # subsequent calls skip the update due to the refresh window. + if repo_dir.is_dir(): + rmtree(repo_dir) + raise else: # Check refresh needed @@ -193,7 +204,7 @@ def clone_or_update( err, ) _LOGGER.info("Removing broken repository at %s", repo_dir) - shutil.rmtree(repo_dir) + rmtree(repo_dir) _LOGGER.info("Successfully removed broken repository, re-cloning...") # Recursively call clone_or_update to re-clone diff --git a/esphome/helpers.py b/esphome/helpers.py index ae142b7f8b..145ebd4096 100644 --- a/esphome/helpers.py +++ b/esphome/helpers.py @@ -8,6 +8,7 @@ from pathlib import Path import platform import re import shutil +import stat import tempfile from typing import TYPE_CHECKING from urllib.parse import urlparse @@ -354,6 +355,23 @@ def is_ha_addon(): return get_bool_env("ESPHOME_IS_HA_ADDON") +def rmtree(path: Path | str) -> None: + """Remove a directory tree, handling read-only files on Windows. + + On Windows, git pack files and other files may be marked read-only, + causing shutil.rmtree to fail. This handles that by removing the + read-only flag and retrying. + """ + + def _onerror(func, path, exc_info): + if os.access(path, os.W_OK): + raise exc_info[1].with_traceback(exc_info[2]) + os.chmod(path, stat.S_IWUSR | stat.S_IRUSR) + func(path) + + shutil.rmtree(path, onerror=_onerror) + + def walk_files(path: Path): for root, _, files in os.walk(path): for name in files: @@ -481,8 +499,6 @@ def list_starts_with(list_, sub): def file_compare(path1: Path, path2: Path) -> bool: """Return True if the files path1 and path2 have the same contents.""" - import stat - try: stat1, stat2 = path1.stat(), path2.stat() except OSError: diff --git a/esphome/writer.py b/esphome/writer.py index 661118e518..fd4c811fb3 100644 --- a/esphome/writer.py +++ b/esphome/writer.py @@ -1,14 +1,10 @@ -from collections.abc import Callable import importlib import json import logging import os from pathlib import Path import re -import shutil -import stat import time -from types import TracebackType from esphome import loader from esphome.config import iter_component_configs, iter_components @@ -25,6 +21,7 @@ from esphome.helpers import ( get_str_env, is_ha_addon, read_file, + rmtree, walk_files, write_file, write_file_if_changed, @@ -404,28 +401,6 @@ def clean_cmake_cache(): pioenvs_cmake_path.unlink() -def _rmtree_error_handler( - func: Callable[[str], object], - path: str, - exc_info: tuple[type[BaseException], BaseException, TracebackType | None], -) -> None: - """Error handler for shutil.rmtree to handle read-only files on Windows. - - On Windows, git pack files and other files may be marked read-only, - causing shutil.rmtree to fail with "Access is denied". This handler - removes the read-only flag and retries the deletion. - """ - if os.access(path, os.W_OK): - raise exc_info[1].with_traceback(exc_info[2]) - os.chmod(path, stat.S_IWUSR | stat.S_IRUSR) - func(path) - - -def rmtree(path: Path | str) -> None: - """Remove a directory tree, handling read-only files on Windows.""" - shutil.rmtree(path, onerror=_rmtree_error_handler) - - def clean_build(clear_pio_cache: bool = True): # Allow skipping cache cleaning for integration tests if os.environ.get("ESPHOME_SKIP_CLEAN_BUILD"): diff --git a/tests/unit_tests/test_git.py b/tests/unit_tests/test_git.py index 0411fe5e43..745dfad487 100644 --- a/tests/unit_tests/test_git.py +++ b/tests/unit_tests/test_git.py @@ -656,7 +656,7 @@ def test_clone_or_update_recover_broken_flag_prevents_infinite_loop( # Should raise on the second attempt when _recover_broken=False # This hits the "if not _recover_broken: raise" path with ( - unittest.mock.patch("esphome.git.shutil.rmtree", side_effect=mock_rmtree), + unittest.mock.patch("esphome.git.rmtree", side_effect=mock_rmtree), pytest.raises(GitCommandError, match="fatal: unable to write new index file"), ): git.clone_or_update( @@ -671,3 +671,114 @@ def test_clone_or_update_recover_broken_flag_prevents_infinite_loop( stash_calls = [c for c in call_list if "stash" in c[0][0]] # Should have exactly two stash calls assert len(stash_calls) == 2 + + +def test_clone_or_update_cleans_up_on_failed_ref_fetch( + tmp_path: Path, mock_run_git_command: Mock +) -> None: + """Test that a failed ref fetch removes the incomplete clone directory. + + When cloning with a specific ref, if `git clone` succeeds but the + subsequent `git fetch ` fails, the clone directory should be + removed so the next attempt starts fresh instead of finding a stale + clone on the default branch. + """ + CORE.config_path = tmp_path / "test.yaml" + + url = "https://github.com/test/repo" + ref = "pull/123/head" + domain = "test" + repo_dir = _compute_repo_dir(url, ref, domain) + + def git_command_side_effect( + cmd: list[str], cwd: str | None = None, **kwargs: Any + ) -> str: + cmd_type = _get_git_command_type(cmd) + if cmd_type == "clone": + # Simulate successful clone by creating the directory + repo_dir.mkdir(parents=True, exist_ok=True) + (repo_dir / ".git").mkdir(exist_ok=True) + return "" + if cmd_type == "fetch": + raise GitCommandError("fatal: couldn't find remote ref pull/123/head") + return "" + + mock_run_git_command.side_effect = git_command_side_effect + + refresh = TimePeriodSeconds(days=1) + + with pytest.raises(GitCommandError, match="couldn't find remote ref"): + git.clone_or_update( + url=url, + ref=ref, + refresh=refresh, + domain=domain, + ) + + # The incomplete clone directory should have been removed + assert not repo_dir.exists() + + # Verify clone was attempted then fetch failed + call_list = mock_run_git_command.call_args_list + clone_calls = [c for c in call_list if "clone" in c[0][0]] + assert len(clone_calls) == 1 + fetch_calls = [c for c in call_list if "fetch" in c[0][0]] + assert len(fetch_calls) == 1 + + +def test_clone_or_update_stale_clone_is_retried_after_cleanup( + tmp_path: Path, mock_run_git_command: Mock +) -> None: + """Test that after cleanup, a subsequent call does a fresh clone. + + This is the full scenario: first call fails at fetch (directory cleaned up), + second call sees no directory and clones fresh. + """ + CORE.config_path = tmp_path / "test.yaml" + + url = "https://github.com/test/repo" + ref = "pull/123/head" + domain = "test" + repo_dir = _compute_repo_dir(url, ref, domain) + + call_count = {"clone": 0, "fetch": 0} + + def git_command_side_effect( + cmd: list[str], cwd: str | None = None, **kwargs: Any + ) -> str: + cmd_type = _get_git_command_type(cmd) + if cmd_type == "clone": + call_count["clone"] += 1 + repo_dir.mkdir(parents=True, exist_ok=True) + (repo_dir / ".git").mkdir(exist_ok=True) + return "" + if cmd_type == "fetch": + call_count["fetch"] += 1 + if call_count["fetch"] == 1: + # First fetch fails + raise GitCommandError("fatal: couldn't find remote ref pull/123/head") + # Second fetch succeeds + return "" + if cmd_type == "reset": + return "" + return "" + + mock_run_git_command.side_effect = git_command_side_effect + + refresh = TimePeriodSeconds(days=1) + + # First call: clone succeeds, fetch fails, directory cleaned up + with pytest.raises(GitCommandError, match="couldn't find remote ref"): + git.clone_or_update(url=url, ref=ref, refresh=refresh, domain=domain) + + assert not repo_dir.exists() + + # Second call: fresh clone + fetch succeeds + result_dir, _ = git.clone_or_update( + url=url, ref=ref, refresh=refresh, domain=domain + ) + + assert result_dir == repo_dir + assert repo_dir.exists() + assert call_count["clone"] == 2 + assert call_count["fetch"] == 2 From 6b8264fcaa56e7d9868733f9b3e39135c886f67f Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 18 Feb 2026 13:09:33 -0500 Subject: [PATCH 060/197] [external_components] Clean up incomplete clone on failed ref fetch (#14051) Co-authored-by: Claude Opus 4.6 --- esphome/components/esp32/__init__.py | 4 +- esphome/git.py | 47 ++++++----- esphome/helpers.py | 20 ++++- esphome/writer.py | 27 +------ tests/unit_tests/test_git.py | 113 ++++++++++++++++++++++++++- 5 files changed, 162 insertions(+), 49 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index b78b945a24..8b3e1afea6 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -44,9 +44,9 @@ from esphome.const import ( from esphome.core import CORE, HexInt, TimePeriod from esphome.coroutine import CoroPriority, coroutine_with_priority import esphome.final_validate as fv -from esphome.helpers import copy_file_if_changed, write_file_if_changed +from esphome.helpers import copy_file_if_changed, rmtree, write_file_if_changed from esphome.types import ConfigType -from esphome.writer import clean_cmake_cache, rmtree +from esphome.writer import clean_cmake_cache from .boards import BOARDS, STANDARD_BOARDS from .const import ( # noqa diff --git a/esphome/git.py b/esphome/git.py index 4ff07ffe75..a45768b5cd 100644 --- a/esphome/git.py +++ b/esphome/git.py @@ -5,12 +5,12 @@ import hashlib import logging from pathlib import Path import re -import shutil import subprocess import urllib.parse import esphome.config_validation as cv from esphome.core import CORE, TimePeriodSeconds +from esphome.helpers import rmtree _LOGGER = logging.getLogger(__name__) @@ -115,24 +115,35 @@ def clone_or_update( if not repo_dir.is_dir(): _LOGGER.info("Cloning %s", key) _LOGGER.debug("Location: %s", repo_dir) - cmd = ["git", "clone", "--depth=1"] - cmd += ["--", url, str(repo_dir)] - run_git_command(cmd) + try: + cmd = ["git", "clone", "--depth=1"] + cmd += ["--", url, str(repo_dir)] + run_git_command(cmd) - if ref is not None: - # We need to fetch the PR branch first, otherwise git will complain - # about missing objects - _LOGGER.info("Fetching %s", ref) - run_git_command(["git", "fetch", "--", "origin", ref], git_dir=repo_dir) - run_git_command(["git", "reset", "--hard", "FETCH_HEAD"], git_dir=repo_dir) + if ref is not None: + # We need to fetch the PR branch first, otherwise git will complain + # about missing objects + _LOGGER.info("Fetching %s", ref) + run_git_command(["git", "fetch", "--", "origin", ref], git_dir=repo_dir) + run_git_command( + ["git", "reset", "--hard", "FETCH_HEAD"], git_dir=repo_dir + ) - if submodules is not None: - _LOGGER.info( - "Initializing submodules (%s) for %s", ", ".join(submodules), key - ) - run_git_command( - ["git", "submodule", "update", "--init"] + submodules, git_dir=repo_dir - ) + if submodules is not None: + _LOGGER.info( + "Initializing submodules (%s) for %s", ", ".join(submodules), key + ) + run_git_command( + ["git", "submodule", "update", "--init"] + submodules, + git_dir=repo_dir, + ) + except GitException: + # Remove incomplete clone to prevent stale state. Without this, + # a failed ref fetch leaves a clone on the default branch, and + # subsequent calls skip the update due to the refresh window. + if repo_dir.is_dir(): + rmtree(repo_dir) + raise else: # Check refresh needed @@ -193,7 +204,7 @@ def clone_or_update( err, ) _LOGGER.info("Removing broken repository at %s", repo_dir) - shutil.rmtree(repo_dir) + rmtree(repo_dir) _LOGGER.info("Successfully removed broken repository, re-cloning...") # Recursively call clone_or_update to re-clone diff --git a/esphome/helpers.py b/esphome/helpers.py index ae142b7f8b..145ebd4096 100644 --- a/esphome/helpers.py +++ b/esphome/helpers.py @@ -8,6 +8,7 @@ from pathlib import Path import platform import re import shutil +import stat import tempfile from typing import TYPE_CHECKING from urllib.parse import urlparse @@ -354,6 +355,23 @@ def is_ha_addon(): return get_bool_env("ESPHOME_IS_HA_ADDON") +def rmtree(path: Path | str) -> None: + """Remove a directory tree, handling read-only files on Windows. + + On Windows, git pack files and other files may be marked read-only, + causing shutil.rmtree to fail. This handles that by removing the + read-only flag and retrying. + """ + + def _onerror(func, path, exc_info): + if os.access(path, os.W_OK): + raise exc_info[1].with_traceback(exc_info[2]) + os.chmod(path, stat.S_IWUSR | stat.S_IRUSR) + func(path) + + shutil.rmtree(path, onerror=_onerror) + + def walk_files(path: Path): for root, _, files in os.walk(path): for name in files: @@ -481,8 +499,6 @@ def list_starts_with(list_, sub): def file_compare(path1: Path, path2: Path) -> bool: """Return True if the files path1 and path2 have the same contents.""" - import stat - try: stat1, stat2 = path1.stat(), path2.stat() except OSError: diff --git a/esphome/writer.py b/esphome/writer.py index 661118e518..fd4c811fb3 100644 --- a/esphome/writer.py +++ b/esphome/writer.py @@ -1,14 +1,10 @@ -from collections.abc import Callable import importlib import json import logging import os from pathlib import Path import re -import shutil -import stat import time -from types import TracebackType from esphome import loader from esphome.config import iter_component_configs, iter_components @@ -25,6 +21,7 @@ from esphome.helpers import ( get_str_env, is_ha_addon, read_file, + rmtree, walk_files, write_file, write_file_if_changed, @@ -404,28 +401,6 @@ def clean_cmake_cache(): pioenvs_cmake_path.unlink() -def _rmtree_error_handler( - func: Callable[[str], object], - path: str, - exc_info: tuple[type[BaseException], BaseException, TracebackType | None], -) -> None: - """Error handler for shutil.rmtree to handle read-only files on Windows. - - On Windows, git pack files and other files may be marked read-only, - causing shutil.rmtree to fail with "Access is denied". This handler - removes the read-only flag and retries the deletion. - """ - if os.access(path, os.W_OK): - raise exc_info[1].with_traceback(exc_info[2]) - os.chmod(path, stat.S_IWUSR | stat.S_IRUSR) - func(path) - - -def rmtree(path: Path | str) -> None: - """Remove a directory tree, handling read-only files on Windows.""" - shutil.rmtree(path, onerror=_rmtree_error_handler) - - def clean_build(clear_pio_cache: bool = True): # Allow skipping cache cleaning for integration tests if os.environ.get("ESPHOME_SKIP_CLEAN_BUILD"): diff --git a/tests/unit_tests/test_git.py b/tests/unit_tests/test_git.py index 0411fe5e43..745dfad487 100644 --- a/tests/unit_tests/test_git.py +++ b/tests/unit_tests/test_git.py @@ -656,7 +656,7 @@ def test_clone_or_update_recover_broken_flag_prevents_infinite_loop( # Should raise on the second attempt when _recover_broken=False # This hits the "if not _recover_broken: raise" path with ( - unittest.mock.patch("esphome.git.shutil.rmtree", side_effect=mock_rmtree), + unittest.mock.patch("esphome.git.rmtree", side_effect=mock_rmtree), pytest.raises(GitCommandError, match="fatal: unable to write new index file"), ): git.clone_or_update( @@ -671,3 +671,114 @@ def test_clone_or_update_recover_broken_flag_prevents_infinite_loop( stash_calls = [c for c in call_list if "stash" in c[0][0]] # Should have exactly two stash calls assert len(stash_calls) == 2 + + +def test_clone_or_update_cleans_up_on_failed_ref_fetch( + tmp_path: Path, mock_run_git_command: Mock +) -> None: + """Test that a failed ref fetch removes the incomplete clone directory. + + When cloning with a specific ref, if `git clone` succeeds but the + subsequent `git fetch ` fails, the clone directory should be + removed so the next attempt starts fresh instead of finding a stale + clone on the default branch. + """ + CORE.config_path = tmp_path / "test.yaml" + + url = "https://github.com/test/repo" + ref = "pull/123/head" + domain = "test" + repo_dir = _compute_repo_dir(url, ref, domain) + + def git_command_side_effect( + cmd: list[str], cwd: str | None = None, **kwargs: Any + ) -> str: + cmd_type = _get_git_command_type(cmd) + if cmd_type == "clone": + # Simulate successful clone by creating the directory + repo_dir.mkdir(parents=True, exist_ok=True) + (repo_dir / ".git").mkdir(exist_ok=True) + return "" + if cmd_type == "fetch": + raise GitCommandError("fatal: couldn't find remote ref pull/123/head") + return "" + + mock_run_git_command.side_effect = git_command_side_effect + + refresh = TimePeriodSeconds(days=1) + + with pytest.raises(GitCommandError, match="couldn't find remote ref"): + git.clone_or_update( + url=url, + ref=ref, + refresh=refresh, + domain=domain, + ) + + # The incomplete clone directory should have been removed + assert not repo_dir.exists() + + # Verify clone was attempted then fetch failed + call_list = mock_run_git_command.call_args_list + clone_calls = [c for c in call_list if "clone" in c[0][0]] + assert len(clone_calls) == 1 + fetch_calls = [c for c in call_list if "fetch" in c[0][0]] + assert len(fetch_calls) == 1 + + +def test_clone_or_update_stale_clone_is_retried_after_cleanup( + tmp_path: Path, mock_run_git_command: Mock +) -> None: + """Test that after cleanup, a subsequent call does a fresh clone. + + This is the full scenario: first call fails at fetch (directory cleaned up), + second call sees no directory and clones fresh. + """ + CORE.config_path = tmp_path / "test.yaml" + + url = "https://github.com/test/repo" + ref = "pull/123/head" + domain = "test" + repo_dir = _compute_repo_dir(url, ref, domain) + + call_count = {"clone": 0, "fetch": 0} + + def git_command_side_effect( + cmd: list[str], cwd: str | None = None, **kwargs: Any + ) -> str: + cmd_type = _get_git_command_type(cmd) + if cmd_type == "clone": + call_count["clone"] += 1 + repo_dir.mkdir(parents=True, exist_ok=True) + (repo_dir / ".git").mkdir(exist_ok=True) + return "" + if cmd_type == "fetch": + call_count["fetch"] += 1 + if call_count["fetch"] == 1: + # First fetch fails + raise GitCommandError("fatal: couldn't find remote ref pull/123/head") + # Second fetch succeeds + return "" + if cmd_type == "reset": + return "" + return "" + + mock_run_git_command.side_effect = git_command_side_effect + + refresh = TimePeriodSeconds(days=1) + + # First call: clone succeeds, fetch fails, directory cleaned up + with pytest.raises(GitCommandError, match="couldn't find remote ref"): + git.clone_or_update(url=url, ref=ref, refresh=refresh, domain=domain) + + assert not repo_dir.exists() + + # Second call: fresh clone + fetch succeeds + result_dir, _ = git.clone_or_update( + url=url, ref=ref, refresh=refresh, domain=domain + ) + + assert result_dir == repo_dir + assert repo_dir.exists() + assert call_count["clone"] == 2 + assert call_count["fetch"] == 2 From ab572c2882c499f5e517f10c704ced98221543f9 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 19 Feb 2026 08:03:44 +1300 Subject: [PATCH 061/197] Bump version to 2026.2.0b5 --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index f9deb32899..88565b4a83 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 = 2026.2.0b4 +PROJECT_NUMBER = 2026.2.0b5 # 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 f494b5d41e..b746a55cd4 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2026.2.0b4" +__version__ = "2026.2.0b5" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 2c89cded4b50130cf3a570971705f488f0e27fed Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 19 Feb 2026 09:30:04 +1300 Subject: [PATCH 062/197] Bump version to 2026.2.0 --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index 88565b4a83..38135f9106 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 = 2026.2.0b5 +PROJECT_NUMBER = 2026.2.0 # 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 b746a55cd4..9115055e7b 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2026.2.0b5" +__version__ = "2026.2.0" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 4a038978d27b4ed193de8fb934ff32221ae9a31f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 15:04:14 -0600 Subject: [PATCH 063/197] [pca9685] Make mode constants inline constexpr (#14042) --- esphome/components/pca9685/pca9685_output.cpp | 6 +----- esphome/components/pca9685/pca9685_output.h | 10 +++++----- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/esphome/components/pca9685/pca9685_output.cpp b/esphome/components/pca9685/pca9685_output.cpp index 77e3d5a6c6..89a6bcdcc0 100644 --- a/esphome/components/pca9685/pca9685_output.cpp +++ b/esphome/components/pca9685/pca9685_output.cpp @@ -8,11 +8,7 @@ namespace pca9685 { static const char *const TAG = "pca9685"; -const uint8_t PCA9685_MODE_INVERTED = 0x10; -const uint8_t PCA9685_MODE_OUTPUT_ONACK = 0x08; -const uint8_t PCA9685_MODE_OUTPUT_TOTEM_POLE = 0x04; -const uint8_t PCA9685_MODE_OUTNE_HIGHZ = 0x02; -const uint8_t PCA9685_MODE_OUTNE_LOW = 0x01; +// PCA9685 mode constants are now inline constexpr in pca9685_output.h static const uint8_t PCA9685_REGISTER_SOFTWARE_RESET = 0x06; static const uint8_t PCA9685_REGISTER_MODE1 = 0x00; diff --git a/esphome/components/pca9685/pca9685_output.h b/esphome/components/pca9685/pca9685_output.h index 288c923d4c..785cc974da 100644 --- a/esphome/components/pca9685/pca9685_output.h +++ b/esphome/components/pca9685/pca9685_output.h @@ -13,15 +13,15 @@ enum class PhaseBalancer { }; /// Inverts polarity of channel output signal -extern const uint8_t PCA9685_MODE_INVERTED; +inline constexpr uint8_t PCA9685_MODE_INVERTED = 0x10; /// Channel update happens upon ACK (post-set) rather than on STOP (endTransmission) -extern const uint8_t PCA9685_MODE_OUTPUT_ONACK; +inline constexpr uint8_t PCA9685_MODE_OUTPUT_ONACK = 0x08; /// Use a totem-pole (push-pull) style output rather than an open-drain structure. -extern const uint8_t PCA9685_MODE_OUTPUT_TOTEM_POLE; +inline constexpr uint8_t PCA9685_MODE_OUTPUT_TOTEM_POLE = 0x04; /// For active low output enable, sets channel output to high-impedance state -extern const uint8_t PCA9685_MODE_OUTNE_HIGHZ; +inline constexpr uint8_t PCA9685_MODE_OUTNE_HIGHZ = 0x02; /// Similarly, sets channel output to high if in totem-pole mode, otherwise -extern const uint8_t PCA9685_MODE_OUTNE_LOW; +inline constexpr uint8_t PCA9685_MODE_OUTNE_LOW = 0x01; class PCA9685Output; From 82cfa00a97a8b6c9d0361c2bec66c3723a2be6e6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 15:04:30 -0600 Subject: [PATCH 064/197] [tlc59208f] Make mode constants inline constexpr (#14043) --- esphome/components/tlc59208f/tlc59208f_output.cpp | 12 +----------- esphome/components/tlc59208f/tlc59208f_output.h | 14 +++++++------- 2 files changed, 8 insertions(+), 18 deletions(-) diff --git a/esphome/components/tlc59208f/tlc59208f_output.cpp b/esphome/components/tlc59208f/tlc59208f_output.cpp index 85311a877c..d35585fe5f 100644 --- a/esphome/components/tlc59208f/tlc59208f_output.cpp +++ b/esphome/components/tlc59208f/tlc59208f_output.cpp @@ -26,17 +26,7 @@ const uint8_t TLC59208F_MODE1_SUB3 = (1 << 1); // 0: device doesn't respond to i2c all-call 3, 1*: responds to all-call const uint8_t TLC59208F_MODE1_ALLCALL = (1 << 0); -// 0*: Group dimming, 1: Group blinking -const uint8_t TLC59208F_MODE2_DMBLNK = (1 << 5); -// 0*: Output change on Stop command, 1: Output change on ACK -const uint8_t TLC59208F_MODE2_OCH = (1 << 3); -// 0*: WDT disabled, 1: WDT enabled -const uint8_t TLC59208F_MODE2_WDTEN = (1 << 2); -// WDT timeouts -const uint8_t TLC59208F_MODE2_WDT_5MS = (0 << 0); -const uint8_t TLC59208F_MODE2_WDT_15MS = (1 << 0); -const uint8_t TLC59208F_MODE2_WDT_25MS = (2 << 0); -const uint8_t TLC59208F_MODE2_WDT_35MS = (3 << 0); +// TLC59208F MODE2 constants are now inline constexpr in tlc59208f_output.h // --- Special function --- // Call address to perform software reset, no devices will ACK diff --git a/esphome/components/tlc59208f/tlc59208f_output.h b/esphome/components/tlc59208f/tlc59208f_output.h index 68ca8061d7..34663cd364 100644 --- a/esphome/components/tlc59208f/tlc59208f_output.h +++ b/esphome/components/tlc59208f/tlc59208f_output.h @@ -9,16 +9,16 @@ namespace esphome { namespace tlc59208f { // 0*: Group dimming, 1: Group blinking -extern const uint8_t TLC59208F_MODE2_DMBLNK; +inline constexpr uint8_t TLC59208F_MODE2_DMBLNK = (1 << 5); // 0*: Output change on Stop command, 1: Output change on ACK -extern const uint8_t TLC59208F_MODE2_OCH; +inline constexpr uint8_t TLC59208F_MODE2_OCH = (1 << 3); // 0*: WDT disabled, 1: WDT enabled -extern const uint8_t TLC59208F_MODE2_WDTEN; +inline constexpr uint8_t TLC59208F_MODE2_WDTEN = (1 << 2); // WDT timeouts -extern const uint8_t TLC59208F_MODE2_WDT_5MS; -extern const uint8_t TLC59208F_MODE2_WDT_15MS; -extern const uint8_t TLC59208F_MODE2_WDT_25MS; -extern const uint8_t TLC59208F_MODE2_WDT_35MS; +inline constexpr uint8_t TLC59208F_MODE2_WDT_5MS = (0 << 0); +inline constexpr uint8_t TLC59208F_MODE2_WDT_15MS = (1 << 0); +inline constexpr uint8_t TLC59208F_MODE2_WDT_25MS = (2 << 0); +inline constexpr uint8_t TLC59208F_MODE2_WDT_35MS = (3 << 0); class TLC59208FOutput; From 09fc0288953d13a09bafef3e9038b36d3ff4c1ee Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 15:16:26 -0600 Subject: [PATCH 065/197] [core] Remove dead global_state variable (#14060) --- esphome/core/component.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index a452f4d400..47c4a70c0f 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -82,8 +82,6 @@ void store_component_error_message(const Component *component, const char *messa const uint16_t WARN_IF_BLOCKING_OVER_MS = 50U; ///< Initial blocking time allowed without warning const uint16_t WARN_IF_BLOCKING_INCREMENT_MS = 10U; ///< How long the blocking time must be larger to warn again -uint32_t global_state = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) - float Component::get_loop_priority() const { return 0.0f; } float Component::get_setup_priority() const { return setup_priority::DATA; } From 02e310f2c9549349ae12615f3fec627d9189479a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 16:48:13 -0600 Subject: [PATCH 066/197] [core] Remove unnecessary IRAM_ATTR from yield(), delay(), feed_wdt(), and arch_feed_wdt() (#14063) --- esphome/components/esp32/core.cpp | 6 +++--- esphome/components/esp8266/core.cpp | 6 +++--- esphome/components/host/core.cpp | 6 +++--- esphome/components/libretiny/core.cpp | 6 +++--- esphome/components/rp2040/core.cpp | 6 +++--- esphome/core/application.cpp | 2 +- 6 files changed, 16 insertions(+), 16 deletions(-) diff --git a/esphome/components/esp32/core.cpp b/esphome/components/esp32/core.cpp index 09a45c14a6..202d929ab9 100644 --- a/esphome/components/esp32/core.cpp +++ b/esphome/components/esp32/core.cpp @@ -21,9 +21,9 @@ extern "C" __attribute__((weak)) void initArduino() {} namespace esphome { -void IRAM_ATTR HOT yield() { vPortYield(); } +void HOT yield() { vPortYield(); } uint32_t IRAM_ATTR HOT millis() { return (uint32_t) (esp_timer_get_time() / 1000ULL); } -void IRAM_ATTR HOT delay(uint32_t ms) { vTaskDelay(ms / portTICK_PERIOD_MS); } +void HOT delay(uint32_t ms) { vTaskDelay(ms / portTICK_PERIOD_MS); } uint32_t IRAM_ATTR HOT micros() { return (uint32_t) esp_timer_get_time(); } void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); } void arch_restart() { @@ -44,7 +44,7 @@ void arch_init() { esp_ota_mark_app_valid_cancel_rollback(); #endif } -void IRAM_ATTR HOT arch_feed_wdt() { esp_task_wdt_reset(); } +void HOT arch_feed_wdt() { esp_task_wdt_reset(); } uint8_t progmem_read_byte(const uint8_t *addr) { return *addr; } uint32_t arch_get_cpu_cycle_count() { return esp_cpu_get_cycle_count(); } diff --git a/esphome/components/esp8266/core.cpp b/esphome/components/esp8266/core.cpp index 784b87916b..236d3022be 100644 --- a/esphome/components/esp8266/core.cpp +++ b/esphome/components/esp8266/core.cpp @@ -14,9 +14,9 @@ extern "C" { namespace esphome { -void IRAM_ATTR HOT yield() { ::yield(); } +void HOT yield() { ::yield(); } uint32_t IRAM_ATTR HOT millis() { return ::millis(); } -void IRAM_ATTR HOT delay(uint32_t ms) { ::delay(ms); } +void HOT delay(uint32_t ms) { ::delay(ms); } uint32_t IRAM_ATTR HOT micros() { return ::micros(); } void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); } void arch_restart() { @@ -27,7 +27,7 @@ void arch_restart() { } } void arch_init() {} -void IRAM_ATTR HOT arch_feed_wdt() { system_soft_wdt_feed(); } +void HOT arch_feed_wdt() { system_soft_wdt_feed(); } uint8_t progmem_read_byte(const uint8_t *addr) { return pgm_read_byte(addr); // NOLINT diff --git a/esphome/components/host/core.cpp b/esphome/components/host/core.cpp index 164d622dd4..c20a33fa37 100644 --- a/esphome/components/host/core.cpp +++ b/esphome/components/host/core.cpp @@ -11,7 +11,7 @@ namespace esphome { -void IRAM_ATTR HOT yield() { ::sched_yield(); } +void HOT yield() { ::sched_yield(); } uint32_t IRAM_ATTR HOT millis() { struct timespec spec; clock_gettime(CLOCK_MONOTONIC, &spec); @@ -19,7 +19,7 @@ uint32_t IRAM_ATTR HOT millis() { uint32_t ms = round(spec.tv_nsec / 1e6); return ((uint32_t) seconds) * 1000U + ms; } -void IRAM_ATTR HOT delay(uint32_t ms) { +void HOT delay(uint32_t ms) { struct timespec ts; ts.tv_sec = ms / 1000; ts.tv_nsec = (ms % 1000) * 1000000; @@ -48,7 +48,7 @@ void arch_restart() { exit(0); } void arch_init() { // pass } -void IRAM_ATTR HOT arch_feed_wdt() { +void HOT arch_feed_wdt() { // pass } diff --git a/esphome/components/libretiny/core.cpp b/esphome/components/libretiny/core.cpp index b22740f02a..4dda7c3856 100644 --- a/esphome/components/libretiny/core.cpp +++ b/esphome/components/libretiny/core.cpp @@ -11,10 +11,10 @@ void loop(); namespace esphome { -void IRAM_ATTR HOT yield() { ::yield(); } +void HOT yield() { ::yield(); } uint32_t IRAM_ATTR HOT millis() { return ::millis(); } uint32_t IRAM_ATTR HOT micros() { return ::micros(); } -void IRAM_ATTR HOT delay(uint32_t ms) { ::delay(ms); } +void HOT delay(uint32_t ms) { ::delay(ms); } void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { ::delayMicroseconds(us); } void arch_init() { @@ -30,7 +30,7 @@ void arch_restart() { while (1) { } } -void IRAM_ATTR HOT arch_feed_wdt() { lt_wdt_feed(); } +void HOT arch_feed_wdt() { lt_wdt_feed(); } uint32_t arch_get_cpu_cycle_count() { return lt_cpu_get_cycle_count(); } uint32_t arch_get_cpu_freq_hz() { return lt_cpu_get_freq(); } uint8_t progmem_read_byte(const uint8_t *addr) { return *addr; } diff --git a/esphome/components/rp2040/core.cpp b/esphome/components/rp2040/core.cpp index d88e9f54b7..37378d88bb 100644 --- a/esphome/components/rp2040/core.cpp +++ b/esphome/components/rp2040/core.cpp @@ -9,9 +9,9 @@ namespace esphome { -void IRAM_ATTR HOT yield() { ::yield(); } +void HOT yield() { ::yield(); } uint32_t IRAM_ATTR HOT millis() { return ::millis(); } -void IRAM_ATTR HOT delay(uint32_t ms) { ::delay(ms); } +void HOT delay(uint32_t ms) { ::delay(ms); } uint32_t IRAM_ATTR HOT micros() { return ::micros(); } void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); } void arch_restart() { @@ -27,7 +27,7 @@ void arch_init() { #endif } -void IRAM_ATTR HOT arch_feed_wdt() { watchdog_update(); } +void HOT arch_feed_wdt() { watchdog_update(); } uint8_t progmem_read_byte(const uint8_t *addr) { return pgm_read_byte(addr); // NOLINT diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index 449acc64cf..406885fd81 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -240,7 +240,7 @@ void Application::process_dump_config_() { this->dump_config_at_++; } -void IRAM_ATTR HOT Application::feed_wdt(uint32_t time) { +void HOT Application::feed_wdt(uint32_t time) { static uint32_t last_feed = 0; // Use provided time if available, otherwise get current time uint32_t now = time ? time : millis(); From 387f615dae037ad8a19dd6f0fad7e04741eee1d7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 16:48:30 -0600 Subject: [PATCH 067/197] [api] Add handshake timeout to prevent connection slot exhaustion (#14050) --- esphome/components/api/api_connection.cpp | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 4d564af9e2..5a7994a322 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -60,6 +60,11 @@ static constexpr uint8_t MAX_MESSAGES_PER_LOOP = 5; static constexpr uint8_t MAX_PING_RETRIES = 60; static constexpr uint16_t PING_RETRY_INTERVAL = 1000; static constexpr uint32_t KEEPALIVE_DISCONNECT_TIMEOUT = (KEEPALIVE_TIMEOUT_MS * 5) / 2; +// Timeout for completing the handshake (Noise transport + HelloRequest). +// A stalled handshake from a buggy client or network glitch holds a connection +// slot, which can prevent legitimate clients from reconnecting. Also hardens +// against the less likely case of intentional connection slot exhaustion. +static constexpr uint32_t HANDSHAKE_TIMEOUT_MS = 15000; static constexpr auto ESPHOME_VERSION_REF = StringRef::from_lit(ESPHOME_VERSION); @@ -205,7 +210,12 @@ void APIConnection::loop() { this->fatal_error_with_log_(LOG_STR("Reading failed"), err); return; } else { - this->last_traffic_ = now; + // Only update last_traffic_ after authentication to ensure the + // handshake timeout is an absolute deadline from connection start. + // Pre-auth messages (e.g. PingRequest) must not reset the timer. + if (this->is_authenticated()) { + this->last_traffic_ = now; + } // read a packet this->read_message(buffer.data_len, buffer.type, buffer.data); if (this->flags_.remove) @@ -223,6 +233,15 @@ void APIConnection::loop() { this->process_active_iterator_(); } + // Disconnect clients that haven't completed the handshake in time. + // Stale half-open connections from buggy clients or network issues can + // accumulate and block legitimate clients from reconnecting. + if (!this->is_authenticated() && now - this->last_traffic_ > HANDSHAKE_TIMEOUT_MS) { + this->on_fatal_error(); + this->log_client_(ESPHOME_LOG_LEVEL_WARN, LOG_STR("handshake timeout; disconnecting")); + return; + } + if (this->flags_.sent_ping) { // Disconnect if not responded within 2.5*keepalive if (now - this->last_traffic_ > KEEPALIVE_DISCONNECT_TIMEOUT) { @@ -1484,6 +1503,8 @@ void APIConnection::complete_authentication_() { } this->flags_.connection_state = static_cast(ConnectionState::AUTHENTICATED); + // Reset traffic timer so keepalive starts from authentication, not connection start + this->last_traffic_ = App.get_loop_component_start_time(); this->log_client_(ESPHOME_LOG_LEVEL_DEBUG, LOG_STR("connected")); #ifdef USE_API_CLIENT_CONNECTED_TRIGGER { From d90754dc0a182785575f2232f3faa29c4d787c84 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 16:49:19 -0600 Subject: [PATCH 068/197] [http_request] Replace heavy STL containers with std::vector for headers (#14024) --- esphome/components/http_request/__init__.py | 2 +- .../components/http_request/http_request.cpp | 22 +++---- .../components/http_request/http_request.h | 58 ++++++++++++------- .../http_request/http_request_arduino.cpp | 12 ++-- .../http_request/http_request_arduino.h | 2 +- .../http_request/http_request_host.cpp | 6 +- .../http_request/http_request_host.h | 2 +- .../http_request/http_request_idf.cpp | 16 ++--- .../http_request/http_request_idf.h | 7 +-- 9 files changed, 65 insertions(+), 62 deletions(-) diff --git a/esphome/components/http_request/__init__.py b/esphome/components/http_request/__init__.py index 64d74323d6..5faffccbe4 100644 --- a/esphome/components/http_request/__init__.py +++ b/esphome/components/http_request/__init__.py @@ -310,7 +310,7 @@ async def http_request_action_to_code(config, action_id, template_arg, args): cg.add(var.add_request_header(key, template_)) for value in config.get(CONF_COLLECT_HEADERS, []): - cg.add(var.add_collect_header(value)) + cg.add(var.add_collect_header(value.lower())) if response_conf := config.get(CONF_ON_RESPONSE): if capture_response: diff --git a/esphome/components/http_request/http_request.cpp b/esphome/components/http_request/http_request.cpp index 11dde4715a..6590d2018e 100644 --- a/esphome/components/http_request/http_request.cpp +++ b/esphome/components/http_request/http_request.cpp @@ -22,23 +22,15 @@ void HttpRequestComponent::dump_config() { } std::string HttpContainer::get_response_header(const std::string &header_name) { - auto response_headers = this->get_response_headers(); - auto header_name_lower_case = str_lower_case(header_name); - if (response_headers.count(header_name_lower_case) == 0) { - ESP_LOGW(TAG, "No header with name %s found", header_name_lower_case.c_str()); - return ""; - } else { - auto values = response_headers[header_name_lower_case]; - if (values.empty()) { - ESP_LOGE(TAG, "header with name %s returned an empty list, this shouldn't happen", - header_name_lower_case.c_str()); - return ""; - } else { - auto header_value = values.front(); - ESP_LOGD(TAG, "Header with name %s found with value %s", header_name_lower_case.c_str(), header_value.c_str()); - return header_value; + auto lower = str_lower_case(header_name); + for (const auto &entry : this->response_headers_) { + if (entry.name == lower) { + ESP_LOGD(TAG, "Header with name %s found with value %s", lower.c_str(), entry.value.c_str()); + return entry.value; } } + ESP_LOGW(TAG, "No header with name %s found", lower.c_str()); + return ""; } } // namespace esphome::http_request diff --git a/esphome/components/http_request/http_request.h b/esphome/components/http_request/http_request.h index a427cc4a05..458ffe94a8 100644 --- a/esphome/components/http_request/http_request.h +++ b/esphome/components/http_request/http_request.h @@ -80,6 +80,16 @@ inline bool is_redirect(int const status) { */ inline bool is_success(int const status) { return status >= HTTP_STATUS_OK && status < HTTP_STATUS_MULTIPLE_CHOICES; } +/// Check if a header name should be collected (linear scan, fine for small lists) +inline bool should_collect_header(const std::vector &lower_case_collect_headers, + const std::string &lower_header_name) { + for (const auto &h : lower_case_collect_headers) { + if (h == lower_header_name) + return true; + } + return false; +} + /* * HTTP Container Read Semantics * ============================= @@ -258,20 +268,13 @@ class HttpContainer : public Parented { return !this->is_chunked_ && this->bytes_read_ >= this->content_length; } - /** - * @brief Get response headers. - * - * @return The key is the lower case response header name, the value is the header value. - */ - std::map> get_response_headers() { return this->response_headers_; } - std::string get_response_header(const std::string &header_name); protected: size_t bytes_read_{0}; bool secure_{false}; bool is_chunked_{false}; ///< True if response uses chunked transfer encoding - std::map> response_headers_{}; + std::vector
response_headers_{}; }; /// Read data from HTTP container into buffer with timeout handling @@ -333,8 +336,8 @@ class HttpRequestComponent : public Component { return this->start(url, "GET", "", request_headers); } std::shared_ptr get(const std::string &url, const std::list
&request_headers, - const std::set &collect_headers) { - return this->start(url, "GET", "", request_headers, collect_headers); + const std::vector &lower_case_collect_headers) { + return this->start(url, "GET", "", request_headers, lower_case_collect_headers); } std::shared_ptr post(const std::string &url, const std::string &body) { return this->start(url, "POST", body, {}); @@ -345,29 +348,40 @@ class HttpRequestComponent : public Component { } std::shared_ptr post(const std::string &url, const std::string &body, const std::list
&request_headers, - const std::set &collect_headers) { - return this->start(url, "POST", body, request_headers, collect_headers); + const std::vector &lower_case_collect_headers) { + return this->start(url, "POST", body, request_headers, lower_case_collect_headers); } std::shared_ptr start(const std::string &url, const std::string &method, const std::string &body, const std::list
&request_headers) { - return this->start(url, method, body, request_headers, {}); + // Call perform() directly to avoid ambiguity with the std::set overload + return this->perform(url, method, body, request_headers, {}); + } + + // Remove before 2027.1.0 + ESPDEPRECATED("Pass collect_headers as std::vector instead of std::set. Removed in 2027.1.0.", + "2026.7.0") + std::shared_ptr start(const std::string &url, const std::string &method, const std::string &body, + const std::list
&request_headers, + const std::set &collect_headers) { + std::vector lower; + lower.reserve(collect_headers.size()); + for (const auto &h : collect_headers) { + lower.push_back(str_lower_case(h)); + } + return this->perform(url, method, body, request_headers, lower); } std::shared_ptr start(const std::string &url, const std::string &method, const std::string &body, const std::list
&request_headers, - const std::set &collect_headers) { - std::set lower_case_collect_headers; - for (const std::string &collect_header : collect_headers) { - lower_case_collect_headers.insert(str_lower_case(collect_header)); - } + const std::vector &lower_case_collect_headers) { return this->perform(url, method, body, request_headers, lower_case_collect_headers); } protected: virtual std::shared_ptr perform(const std::string &url, const std::string &method, const std::string &body, const std::list
&request_headers, - const std::set &collect_headers) = 0; + const std::vector &lower_case_collect_headers) = 0; const char *useragent_{nullptr}; bool follow_redirects_{}; uint16_t redirect_limit_{}; @@ -389,7 +403,7 @@ template class HttpRequestSendAction : public Action { this->request_headers_.insert({key, value}); } - void add_collect_header(const char *value) { this->collect_headers_.insert(value); } + void add_collect_header(const char *value) { this->lower_case_collect_headers_.push_back(value); } void add_json(const char *key, TemplatableValue value) { this->json_.insert({key, value}); } @@ -431,7 +445,7 @@ template class HttpRequestSendAction : public Action { } auto container = this->parent_->start(this->url_.value(x...), this->method_.value(x...), body, request_headers, - this->collect_headers_); + this->lower_case_collect_headers_); auto captured_args = std::make_tuple(x...); @@ -494,7 +508,7 @@ template class HttpRequestSendAction : public Action { void encode_json_func_(Ts... x, JsonObject root) { this->json_func_(x..., root); } HttpRequestComponent *parent_; std::map> request_headers_{}; - std::set collect_headers_{"content-type", "content-length"}; + std::vector lower_case_collect_headers_{"content-type", "content-length"}; std::map> json_{}; std::function json_func_{nullptr}; #ifdef USE_HTTP_REQUEST_RESPONSE diff --git a/esphome/components/http_request/http_request_arduino.cpp b/esphome/components/http_request/http_request_arduino.cpp index e5b919e380..3f60b76b58 100644 --- a/esphome/components/http_request/http_request_arduino.cpp +++ b/esphome/components/http_request/http_request_arduino.cpp @@ -27,7 +27,7 @@ static constexpr int ESP8266_SSL_ERR_OOM = -1000; std::shared_ptr HttpRequestArduino::perform(const std::string &url, const std::string &method, const std::string &body, const std::list
&request_headers, - const std::set &collect_headers) { + const std::vector &lower_case_collect_headers) { if (!network::is_connected()) { this->status_momentary_error("failed", 1000); ESP_LOGW(TAG, "HTTP Request failed; Not connected to network"); @@ -107,9 +107,9 @@ std::shared_ptr HttpRequestArduino::perform(const std::string &ur } // returned needed headers must be collected before the requests - const char *header_keys[collect_headers.size()]; + const char *header_keys[lower_case_collect_headers.size()]; int index = 0; - for (auto const &header_name : collect_headers) { + for (auto const &header_name : lower_case_collect_headers) { header_keys[index++] = header_name.c_str(); } container->client_.collectHeaders(header_keys, index); @@ -160,14 +160,14 @@ std::shared_ptr HttpRequestArduino::perform(const std::string &ur // Still return the container, so it can be used to get the status code and error message } - container->response_headers_ = {}; + container->response_headers_.clear(); auto header_count = container->client_.headers(); for (int i = 0; i < header_count; i++) { const std::string header_name = str_lower_case(container->client_.headerName(i).c_str()); - if (collect_headers.count(header_name) > 0) { + if (should_collect_header(lower_case_collect_headers, header_name)) { std::string header_value = container->client_.header(i).c_str(); ESP_LOGD(TAG, "Received response header, name: %s, value: %s", header_name.c_str(), header_value.c_str()); - container->response_headers_[header_name].push_back(header_value); + container->response_headers_.push_back({header_name, header_value}); } } diff --git a/esphome/components/http_request/http_request_arduino.h b/esphome/components/http_request/http_request_arduino.h index a1084b12d5..dbd61de364 100644 --- a/esphome/components/http_request/http_request_arduino.h +++ b/esphome/components/http_request/http_request_arduino.h @@ -50,7 +50,7 @@ class HttpRequestArduino : public HttpRequestComponent { protected: std::shared_ptr perform(const std::string &url, const std::string &method, const std::string &body, const std::list
&request_headers, - const std::set &collect_headers) override; + const std::vector &lower_case_collect_headers) override; }; } // namespace esphome::http_request diff --git a/esphome/components/http_request/http_request_host.cpp b/esphome/components/http_request/http_request_host.cpp index b94570be12..714a73fc31 100644 --- a/esphome/components/http_request/http_request_host.cpp +++ b/esphome/components/http_request/http_request_host.cpp @@ -19,7 +19,7 @@ static const char *const TAG = "http_request.host"; std::shared_ptr HttpRequestHost::perform(const std::string &url, const std::string &method, const std::string &body, const std::list
&request_headers, - const std::set &response_headers) { + const std::vector &lower_case_collect_headers) { if (!network::is_connected()) { this->status_momentary_error("failed", 1000); ESP_LOGW(TAG, "HTTP Request failed; Not connected to network"); @@ -116,8 +116,8 @@ std::shared_ptr HttpRequestHost::perform(const std::string &url, for (auto header : response.headers) { ESP_LOGD(TAG, "Header: %s: %s", header.first.c_str(), header.second.c_str()); auto lower_name = str_lower_case(header.first); - if (response_headers.find(lower_name) != response_headers.end()) { - container->response_headers_[lower_name].emplace_back(header.second); + if (should_collect_header(lower_case_collect_headers, lower_name)) { + container->response_headers_.push_back({lower_name, header.second}); } } container->duration_ms = millis() - start; diff --git a/esphome/components/http_request/http_request_host.h b/esphome/components/http_request/http_request_host.h index 32e149e6a3..79f5b7e817 100644 --- a/esphome/components/http_request/http_request_host.h +++ b/esphome/components/http_request/http_request_host.h @@ -20,7 +20,7 @@ class HttpRequestHost : public HttpRequestComponent { public: std::shared_ptr perform(const std::string &url, const std::string &method, const std::string &body, const std::list
&request_headers, - const std::set &response_headers) override; + const std::vector &lower_case_collect_headers) override; void set_ca_path(const char *ca_path) { this->ca_path_ = ca_path; } protected: diff --git a/esphome/components/http_request/http_request_idf.cpp b/esphome/components/http_request/http_request_idf.cpp index 486984a694..0921c50b9f 100644 --- a/esphome/components/http_request/http_request_idf.cpp +++ b/esphome/components/http_request/http_request_idf.cpp @@ -19,8 +19,8 @@ namespace esphome::http_request { static const char *const TAG = "http_request.idf"; struct UserData { - const std::set &collect_headers; - std::map> response_headers; + const std::vector &lower_case_collect_headers; + std::vector
&response_headers; }; void HttpRequestIDF::dump_config() { @@ -38,10 +38,10 @@ esp_err_t HttpRequestIDF::http_event_handler(esp_http_client_event_t *evt) { switch (evt->event_id) { case HTTP_EVENT_ON_HEADER: { const std::string header_name = str_lower_case(evt->header_key); - if (user_data->collect_headers.count(header_name)) { + if (should_collect_header(user_data->lower_case_collect_headers, header_name)) { const std::string header_value = evt->header_value; ESP_LOGD(TAG, "Received response header, name: %s, value: %s", header_name.c_str(), header_value.c_str()); - user_data->response_headers[header_name].push_back(header_value); + user_data->response_headers.push_back({header_name, header_value}); } break; } @@ -55,7 +55,7 @@ esp_err_t HttpRequestIDF::http_event_handler(esp_http_client_event_t *evt) { std::shared_ptr HttpRequestIDF::perform(const std::string &url, const std::string &method, const std::string &body, const std::list
&request_headers, - const std::set &collect_headers) { + const std::vector &lower_case_collect_headers) { if (!network::is_connected()) { this->status_momentary_error("failed", 1000); ESP_LOGE(TAG, "HTTP Request failed; Not connected to network"); @@ -110,8 +110,6 @@ std::shared_ptr HttpRequestIDF::perform(const std::string &url, c watchdog::WatchdogManager wdm(this->get_watchdog_timeout()); config.event_handler = http_event_handler; - auto user_data = UserData{collect_headers, {}}; - config.user_data = static_cast(&user_data); esp_http_client_handle_t client = esp_http_client_init(&config); @@ -120,6 +118,9 @@ std::shared_ptr HttpRequestIDF::perform(const std::string &url, c container->set_secure(secure); + auto user_data = UserData{lower_case_collect_headers, container->response_headers_}; + esp_http_client_set_user_data(client, static_cast(&user_data)); + for (const auto &header : request_headers) { esp_http_client_set_header(client, header.name.c_str(), header.value.c_str()); } @@ -164,7 +165,6 @@ std::shared_ptr HttpRequestIDF::perform(const std::string &url, c container->feed_wdt(); container->status_code = esp_http_client_get_status_code(client); container->feed_wdt(); - container->set_response_headers(user_data.response_headers); container->duration_ms = millis() - start; if (is_success(container->status_code)) { return container; diff --git a/esphome/components/http_request/http_request_idf.h b/esphome/components/http_request/http_request_idf.h index 2a130eae58..9206ba6f5d 100644 --- a/esphome/components/http_request/http_request_idf.h +++ b/esphome/components/http_request/http_request_idf.h @@ -21,11 +21,8 @@ class HttpContainerIDF : public HttpContainer { /// @brief Feeds the watchdog timer if the executing task has one attached void feed_wdt(); - void set_response_headers(std::map> &response_headers) { - this->response_headers_ = std::move(response_headers); - } - protected: + friend class HttpRequestIDF; esp_http_client_handle_t client_; }; @@ -41,7 +38,7 @@ class HttpRequestIDF : public HttpRequestComponent { protected: std::shared_ptr perform(const std::string &url, const std::string &method, const std::string &body, const std::list
&request_headers, - const std::set &collect_headers) override; + const std::vector &lower_case_collect_headers) override; // if zero ESP-IDF will use DEFAULT_HTTP_BUF_SIZE uint16_t buffer_size_rx_{}; uint16_t buffer_size_tx_{}; From bd055e75b9c2def6d9e12170e771920115c9751f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 16:49:37 -0600 Subject: [PATCH 069/197] [core] Shrink Application::dump_config_at_ from size_t to uint16_t (#14053) Co-authored-by: Claude Opus 4.6 --- esphome/core/application.h | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/esphome/core/application.h b/esphome/core/application.h index 30611227a2..e0299f3db3 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -582,9 +582,6 @@ class Application { std::string name_; std::string friendly_name_; - // size_t members - size_t dump_config_at_{SIZE_MAX}; - // 4-byte members uint32_t last_loop_{0}; uint32_t loop_component_start_time_{0}; @@ -594,7 +591,8 @@ class Application { #endif // 2-byte members (grouped together for alignment) - uint16_t loop_interval_{16}; // Loop interval in ms (max 65535ms = 65.5 seconds) + uint16_t dump_config_at_{std::numeric_limits::max()}; // Index into components_ for dump_config progress + uint16_t loop_interval_{16}; // Loop interval in ms (max 65535ms = 65.5 seconds) uint16_t looping_components_active_end_{0}; // Index marking end of active components in looping_components_ uint16_t current_loop_index_{0}; // For safe reentrant modifications during iteration From 5f82017a310bd60123eadee17a5f2f3d36106694 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 19:01:00 -0600 Subject: [PATCH 070/197] [udp] Register socket consumption for CONFIG_LWIP_MAX_SOCKETS (#14068) --- esphome/components/udp/__init__.py | 63 ++++++++++++++++++------------ 1 file changed, 39 insertions(+), 24 deletions(-) diff --git a/esphome/components/udp/__init__.py b/esphome/components/udp/__init__.py index bfaa5f2516..c9586d0b95 100644 --- a/esphome/components/udp/__init__.py +++ b/esphome/components/udp/__init__.py @@ -14,6 +14,7 @@ import esphome.config_validation as cv from esphome.const import CONF_DATA, CONF_ID, CONF_PORT, CONF_TRIGGER_ID from esphome.core import ID from esphome.cpp_generator import MockObj +from esphome.types import ConfigType CODEOWNERS = ["@clydebarrow"] DEPENDENCIES = ["network"] @@ -65,33 +66,47 @@ RELOCATED = { ) } -CONFIG_SCHEMA = cv.COMPONENT_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(UDPComponent), - cv.Optional(CONF_PORT, default=18511): cv.Any( - cv.port, - cv.Schema( + +def _consume_udp_sockets(config: ConfigType) -> ConfigType: + """Register socket needs for UDP component.""" + from esphome.components import socket + + # UDP uses up to 2 sockets: 1 broadcast + 1 listen + # Whether each is used depends on code generation, so register worst case + socket.consume_sockets(2, "udp")(config) + return config + + +CONFIG_SCHEMA = cv.All( + cv.COMPONENT_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(UDPComponent), + cv.Optional(CONF_PORT, default=18511): cv.Any( + cv.port, + cv.Schema( + { + cv.Required(CONF_LISTEN_PORT): cv.port, + cv.Required(CONF_BROADCAST_PORT): cv.port, + } + ), + ), + cv.Optional( + CONF_LISTEN_ADDRESS, default="255.255.255.255" + ): cv.ipv4address_multi_broadcast, + cv.Optional(CONF_ADDRESSES, default=["255.255.255.255"]): cv.ensure_list( + cv.ipv4address, + ), + cv.Optional(CONF_ON_RECEIVE): automation.validate_automation( { - cv.Required(CONF_LISTEN_PORT): cv.port, - cv.Required(CONF_BROADCAST_PORT): cv.port, + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + Trigger.template(trigger_args) + ), } ), - ), - cv.Optional( - CONF_LISTEN_ADDRESS, default="255.255.255.255" - ): cv.ipv4address_multi_broadcast, - cv.Optional(CONF_ADDRESSES, default=["255.255.255.255"]): cv.ensure_list( - cv.ipv4address, - ), - cv.Optional(CONF_ON_RECEIVE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - Trigger.template(trigger_args) - ), - } - ), - } -).extend(RELOCATED) + } + ).extend(RELOCATED), + _consume_udp_sockets, +) async def register_udp_client(var, config): From 3b869f172064dc994f2ff001e4c73c8da858b01e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 19:01:37 -0600 Subject: [PATCH 071/197] [web_server] Double socket allocation to prevent connection exhaustion (#14067) --- esphome/components/web_server/__init__.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/esphome/components/web_server/__init__.py b/esphome/components/web_server/__init__.py index 8b02a6baee..294a5e0a15 100644 --- a/esphome/components/web_server/__init__.py +++ b/esphome/components/web_server/__init__.py @@ -144,9 +144,10 @@ def _consume_web_server_sockets(config: ConfigType) -> ConfigType: """Register socket needs for web_server component.""" from esphome.components import socket - # Web server needs 1 listening socket + typically 2 concurrent client connections - # (browser makes 2 connections for page + event stream) - sockets_needed = 3 + # Web server needs 1 listening socket + typically 5 concurrent client connections + # (browser opens connections for page resources, SSE event stream, and POST + # requests for entity control which may linger before closing) + sockets_needed = 6 socket.consume_sockets(sockets_needed, "web_server")(config) return config From 565443b710eb8b56bd7bc2d53fef765d195e4303 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 19:08:53 -0600 Subject: [PATCH 072/197] [pulse_counter] Fix compilation on ESP32-C6/C5/H2/P4 (#14070) --- esphome/components/pulse_counter/pulse_counter_sensor.cpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/esphome/components/pulse_counter/pulse_counter_sensor.cpp b/esphome/components/pulse_counter/pulse_counter_sensor.cpp index 8ac5a28d8f..ef4cc980f6 100644 --- a/esphome/components/pulse_counter/pulse_counter_sensor.cpp +++ b/esphome/components/pulse_counter/pulse_counter_sensor.cpp @@ -2,7 +2,7 @@ #include "esphome/core/log.h" #ifdef HAS_PCNT -#include +#include #include #endif @@ -117,9 +117,7 @@ bool HwPulseCounterStorage::pulse_counter_setup(InternalGPIOPin *pin) { } if (this->filter_us != 0) { - uint32_t apb_freq; - esp_clk_tree_src_get_freq_hz(SOC_MOD_CLK_APB, ESP_CLK_TREE_SRC_FREQ_PRECISION_CACHED, &apb_freq); - uint32_t max_glitch_ns = PCNT_LL_MAX_GLITCH_WIDTH * 1000000u / apb_freq; + uint32_t max_glitch_ns = PCNT_LL_MAX_GLITCH_WIDTH * 1000000u / (uint32_t) esp_clk_apb_freq(); pcnt_glitch_filter_config_t filter_config = { .max_glitch_ns = std::min(this->filter_us * 1000u, max_glitch_ns), }; From be853afc2465a291d3a4806122884c52d0bd905e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 20:57:56 -0600 Subject: [PATCH 073/197] [core] Conditionally compile setup_priority override infrastructure (#14057) --- esphome/core/application.cpp | 2 ++ esphome/core/component.cpp | 19 +++++++++++++------ esphome/core/component.h | 1 + esphome/core/defines.h | 1 + esphome/cpp_helpers.py | 3 ++- 5 files changed, 19 insertions(+), 7 deletions(-) diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index 406885fd81..b216233f9b 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -137,8 +137,10 @@ void Application::setup() { ESP_LOGI(TAG, "setup() finished successfully!"); +#ifdef USE_SETUP_PRIORITY_OVERRIDE // Clear setup priority overrides to free memory clear_setup_priority_overrides(); +#endif #if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) // Set up wake socket for waking main loop from tasks diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index 47c4a70c0f..ba0f1663b9 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -41,20 +41,23 @@ struct ComponentErrorMessage { bool is_flash_ptr; }; +#ifdef USE_SETUP_PRIORITY_OVERRIDE struct ComponentPriorityOverride { const Component *component; float priority; }; +// Setup priority overrides - freed after setup completes +// Using raw pointer instead of unique_ptr to avoid global constructor/destructor overhead +// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) +std::vector *setup_priority_overrides = nullptr; +#endif + // Error messages for failed components // Using raw pointer instead of unique_ptr to avoid global constructor/destructor overhead // This is never freed as error messages persist for the lifetime of the device // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) std::vector *component_error_messages = nullptr; -// Setup priority overrides - freed after setup completes -// Using raw pointer instead of unique_ptr to avoid global constructor/destructor overhead -// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) -std::vector *setup_priority_overrides = nullptr; // Helper to store error messages - reduces duplication between deprecated and new API // Remove before 2026.6.0 when deprecated const char* API is removed @@ -459,6 +462,7 @@ void log_update_interval(const char *tag, PollingComponent *component) { } } float Component::get_actual_setup_priority() const { +#ifdef USE_SETUP_PRIORITY_OVERRIDE // Check if there's an override in the global vector if (setup_priority_overrides) { // Linear search is fine for small n (typically < 5 overrides) @@ -468,14 +472,14 @@ float Component::get_actual_setup_priority() const { } } } +#endif return this->get_setup_priority(); } +#ifdef USE_SETUP_PRIORITY_OVERRIDE void Component::set_setup_priority(float priority) { // Lazy allocate the vector if needed if (!setup_priority_overrides) { setup_priority_overrides = new std::vector(); - // Reserve some space to avoid reallocations (most configs have < 10 overrides) - setup_priority_overrides->reserve(10); } // Check if this component already has an override @@ -489,6 +493,7 @@ void Component::set_setup_priority(float priority) { // Add new override setup_priority_overrides->emplace_back(ComponentPriorityOverride{this, priority}); } +#endif bool Component::has_overridden_loop() const { #if defined(USE_HOST) || defined(CLANG_TIDY) @@ -557,10 +562,12 @@ uint32_t WarnIfComponentBlockingGuard::finish() { WarnIfComponentBlockingGuard::~WarnIfComponentBlockingGuard() {} +#ifdef USE_SETUP_PRIORITY_OVERRIDE void clear_setup_priority_overrides() { // Free the setup priority map completely delete setup_priority_overrides; setup_priority_overrides = nullptr; } +#endif } // namespace esphome diff --git a/esphome/core/component.h b/esphome/core/component.h index 848bc0ba35..6f7f77dbc1 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -563,6 +563,7 @@ class WarnIfComponentBlockingGuard { }; // Function to clear setup priority overrides after all components are set up +// Only has an implementation when USE_SETUP_PRIORITY_OVERRIDE is defined void clear_setup_priority_overrides(); } // namespace esphome diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 0d6c1a42e8..bcafcb4c60 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -109,6 +109,7 @@ #define USE_SAFE_MODE_CALLBACK #define USE_SELECT #define USE_SENSOR +#define USE_SETUP_PRIORITY_OVERRIDE #define USE_STATUS_LED #define USE_STATUS_SENSOR #define USE_SWITCH diff --git a/esphome/cpp_helpers.py b/esphome/cpp_helpers.py index 2698b9b3d5..954a28d3d1 100644 --- a/esphome/cpp_helpers.py +++ b/esphome/cpp_helpers.py @@ -9,7 +9,7 @@ from esphome.const import ( ) from esphome.core import CORE, ID, coroutine from esphome.coroutine import FakeAwaitable -from esphome.cpp_generator import LogStringLiteral, add, get_variable +from esphome.cpp_generator import LogStringLiteral, add, add_define, get_variable from esphome.cpp_types import App from esphome.types import ConfigFragmentType, ConfigType from esphome.util import Registry, RegistryEntry @@ -49,6 +49,7 @@ async def register_component(var, config): ) CORE.component_ids.remove(id_) if CONF_SETUP_PRIORITY in config: + add_define("USE_SETUP_PRIORITY_OVERRIDE") add(var.set_setup_priority(config[CONF_SETUP_PRIORITY])) if CONF_UPDATE_INTERVAL in config: add(var.set_update_interval(config[CONF_UPDATE_INTERVAL])) From e4c233b6ceef1d1a306e5879752ffc9815d1190b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 20:59:31 -0600 Subject: [PATCH 074/197] [mqtt] Use constexpr for compile-time constants (#14075) --- esphome/components/mqtt/mqtt_backend_esp32.h | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/esphome/components/mqtt/mqtt_backend_esp32.h b/esphome/components/mqtt/mqtt_backend_esp32.h index adba0cf004..ccc4c4026c 100644 --- a/esphome/components/mqtt/mqtt_backend_esp32.h +++ b/esphome/components/mqtt/mqtt_backend_esp32.h @@ -114,11 +114,11 @@ struct QueueElement { class MQTTBackendESP32 final : public MQTTBackend { public: - static const size_t MQTT_BUFFER_SIZE = 4096; - static const size_t TASK_STACK_SIZE = 3072; - static const size_t TASK_STACK_SIZE_TLS = 4096; // Larger stack for TLS operations - static const ssize_t TASK_PRIORITY = 5; - static const uint8_t MQTT_QUEUE_LENGTH = 30; // 30*12 bytes = 360 + static constexpr size_t MQTT_BUFFER_SIZE = 4096; + static constexpr size_t TASK_STACK_SIZE = 3072; + static constexpr size_t TASK_STACK_SIZE_TLS = 4096; // Larger stack for TLS operations + static constexpr ssize_t TASK_PRIORITY = 5; + static constexpr uint8_t MQTT_QUEUE_LENGTH = 30; // 30*12 bytes = 360 void set_keep_alive(uint16_t keep_alive) final { this->keep_alive_ = keep_alive; } void set_client_id(const char *client_id) final { this->client_id_ = client_id; } From 66d2ac8cb93a5b7380210ee7933d28620dabd3ab Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 21:00:09 -0600 Subject: [PATCH 075/197] [web_server] Move climate static traits to DETAIL_ALL only (#14066) --- esphome/components/web_server/web_server.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 3acd2d2119..a796c1426b 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -1546,16 +1546,16 @@ std::string WebServer::climate_json_(climate::Climate *obj, JsonDetail start_con for (auto const &custom_preset : traits.get_supported_custom_presets()) opt.add(custom_preset); } + 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(); this->add_sorting_info_(root, obj); } bool has_state = false; root[ESPHOME_F("mode")] = PSTR_LOCAL(climate_mode_to_string(obj->mode)); - 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)); root[ESPHOME_F("state")] = root[ESPHOME_F("action")]; From 7e118178b3d5e208eab87e0f882b38dcb18e5ed6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 21:00:24 -0600 Subject: [PATCH 076/197] [web_server] Fix water_heater JSON key names and move traits to DETAIL_ALL (#14064) --- esphome/components/web_server/web_server.cpp | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index a796c1426b..e3d131f58e 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -1922,6 +1922,9 @@ std::string WebServer::water_heater_json_(water_heater::WaterHeater *obj, JsonDe JsonArray modes = root[ESPHOME_F("modes")].to(); for (auto m : traits.get_supported_modes()) modes.add(PSTR_LOCAL(water_heater::water_heater_mode_to_string(m))); + root[ESPHOME_F("min_temp")] = traits.get_min_temperature(); + root[ESPHOME_F("max_temp")] = traits.get_max_temperature(); + root[ESPHOME_F("step")] = traits.get_target_temperature_step(); this->add_sorting_info_(root, obj); } @@ -1944,10 +1947,6 @@ std::string WebServer::water_heater_json_(water_heater::WaterHeater *obj, JsonDe root[ESPHOME_F("target_temperature")] = target; } - root[ESPHOME_F("min_temperature")] = traits.get_min_temperature(); - root[ESPHOME_F("max_temperature")] = traits.get_max_temperature(); - root[ESPHOME_F("step")] = traits.get_target_temperature_step(); - if (traits.get_supports_away_mode()) { root[ESPHOME_F("away")] = obj->is_away(); } From 9c9365c146064177dbc371d425307f8e2cb728d6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 21:07:06 -0600 Subject: [PATCH 077/197] [bluetooth_proxy][esp32_ble_client][esp32_ble_server] Use constexpr for compile-time constants (#14073) --- esphome/components/bluetooth_proxy/bluetooth_proxy.h | 4 ++-- .../components/esp32_ble_client/ble_client_base.cpp | 12 ++++++------ .../components/esp32_ble_server/ble_characteristic.h | 12 ++++++------ 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.h b/esphome/components/bluetooth_proxy/bluetooth_proxy.h index ab9aee2d81..62a035e79f 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.h +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.h @@ -35,8 +35,8 @@ using namespace esp32_ble_client; // Version 3: New connection API // Version 4: Pairing support // Version 5: Cache clear support -static const uint32_t LEGACY_ACTIVE_CONNECTIONS_VERSION = 5; -static const uint32_t LEGACY_PASSIVE_ONLY_VERSION = 1; +static constexpr uint32_t LEGACY_ACTIVE_CONNECTIONS_VERSION = 5; +static constexpr uint32_t LEGACY_PASSIVE_ONLY_VERSION = 1; enum BluetoothProxyFeature : uint32_t { FEATURE_PASSIVE_SCAN = 1 << 0, diff --git a/esphome/components/esp32_ble_client/ble_client_base.cpp b/esphome/components/esp32_ble_client/ble_client_base.cpp index c464c89390..3f0eeeab4a 100644 --- a/esphome/components/esp32_ble_client/ble_client_base.cpp +++ b/esphome/components/esp32_ble_client/ble_client_base.cpp @@ -16,17 +16,17 @@ static const char *const TAG = "esp32_ble_client"; // Intermediate connection parameters for standard operation // ESP-IDF defaults (12.5-15ms) are too slow for stable connections through WiFi-based BLE proxies, // causing disconnections. These medium parameters balance responsiveness with bandwidth usage. -static const uint16_t MEDIUM_MIN_CONN_INTERVAL = 0x07; // 7 * 1.25ms = 8.75ms -static const uint16_t MEDIUM_MAX_CONN_INTERVAL = 0x09; // 9 * 1.25ms = 11.25ms +static constexpr uint16_t MEDIUM_MIN_CONN_INTERVAL = 0x07; // 7 * 1.25ms = 8.75ms +static constexpr uint16_t MEDIUM_MAX_CONN_INTERVAL = 0x09; // 9 * 1.25ms = 11.25ms // The timeout value was increased from 6s to 8s to address stability issues observed // in certain BLE devices when operating through WiFi-based BLE proxies. The longer // timeout reduces the likelihood of disconnections during periods of high latency. -static const uint16_t MEDIUM_CONN_TIMEOUT = 800; // 800 * 10ms = 8s +static constexpr uint16_t MEDIUM_CONN_TIMEOUT = 800; // 800 * 10ms = 8s // Fastest connection parameters for devices with short discovery timeouts -static const uint16_t FAST_MIN_CONN_INTERVAL = 0x06; // 6 * 1.25ms = 7.5ms (BLE minimum) -static const uint16_t FAST_MAX_CONN_INTERVAL = 0x06; // 6 * 1.25ms = 7.5ms -static const uint16_t FAST_CONN_TIMEOUT = 1000; // 1000 * 10ms = 10s +static constexpr uint16_t FAST_MIN_CONN_INTERVAL = 0x06; // 6 * 1.25ms = 7.5ms (BLE minimum) +static constexpr uint16_t FAST_MAX_CONN_INTERVAL = 0x06; // 6 * 1.25ms = 7.5ms +static constexpr uint16_t FAST_CONN_TIMEOUT = 1000; // 1000 * 10ms = 10s static const esp_bt_uuid_t NOTIFY_DESC_UUID = { .len = ESP_UUID_LEN_16, .uuid = diff --git a/esphome/components/esp32_ble_server/ble_characteristic.h b/esphome/components/esp32_ble_server/ble_characteristic.h index c2cdb1660c..72897d1dfb 100644 --- a/esphome/components/esp32_ble_server/ble_characteristic.h +++ b/esphome/components/esp32_ble_server/ble_characteristic.h @@ -57,12 +57,12 @@ class BLECharacteristic { ESPBTUUID get_uuid() { return this->uuid_; } std::vector &get_value() { return this->value_; } - static const uint32_t PROPERTY_READ = 1 << 0; - static const uint32_t PROPERTY_WRITE = 1 << 1; - static const uint32_t PROPERTY_NOTIFY = 1 << 2; - static const uint32_t PROPERTY_BROADCAST = 1 << 3; - static const uint32_t PROPERTY_INDICATE = 1 << 4; - static const uint32_t PROPERTY_WRITE_NR = 1 << 5; + static constexpr uint32_t PROPERTY_READ = 1 << 0; + static constexpr uint32_t PROPERTY_WRITE = 1 << 1; + static constexpr uint32_t PROPERTY_NOTIFY = 1 << 2; + static constexpr uint32_t PROPERTY_BROADCAST = 1 << 3; + static constexpr uint32_t PROPERTY_INDICATE = 1 << 4; + static constexpr uint32_t PROPERTY_WRITE_NR = 1 << 5; bool is_created(); bool is_failed(); From 76c151c6e6948fbe2f5c62356ef299797cc83480 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 21:07:38 -0600 Subject: [PATCH 078/197] [api] Use constexpr for compile-time constant (#14072) --- esphome/components/api/api_frame_helper_noise.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/api/api_frame_helper_noise.cpp b/esphome/components/api/api_frame_helper_noise.cpp index 1ae848dead..492988128a 100644 --- a/esphome/components/api/api_frame_helper_noise.cpp +++ b/esphome/components/api/api_frame_helper_noise.cpp @@ -474,7 +474,7 @@ APIError APINoiseFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffer, s // buf_start[1], buf_start[2] to be set after encryption // Write message header (to be encrypted) - const uint8_t msg_offset = 3; + constexpr uint8_t msg_offset = 3; buf_start[msg_offset] = static_cast(msg.message_type >> 8); // type high byte buf_start[msg_offset + 1] = static_cast(msg.message_type); // type low byte buf_start[msg_offset + 2] = static_cast(msg.payload_size >> 8); // data_len high byte From ee7d63f73a3587cc86c6d6fe71e5fcd3df4e31b5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 21:09:49 -0600 Subject: [PATCH 079/197] [packet_transport] Use constexpr for compile-time constants (#14074) --- esphome/components/packet_transport/packet_transport.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/packet_transport/packet_transport.cpp b/esphome/components/packet_transport/packet_transport.cpp index 365a5f2ec7..7b7a852398 100644 --- a/esphome/components/packet_transport/packet_transport.cpp +++ b/esphome/components/packet_transport/packet_transport.cpp @@ -58,9 +58,9 @@ union FuData { float f32; }; -static const uint16_t MAGIC_NUMBER = 0x4553; -static const uint16_t MAGIC_PING = 0x5048; -static const uint32_t PREF_HASH = 0x45535043; +static constexpr uint16_t MAGIC_NUMBER = 0x4553; +static constexpr uint16_t MAGIC_PING = 0x5048; +static constexpr uint32_t PREF_HASH = 0x45535043; enum DataKey { ZERO_FILL_KEY, DATA_KEY, From 20239d1bb304ceea6f7056c0466071178ad8af50 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 21:16:09 -0600 Subject: [PATCH 080/197] [remote_base] Use constexpr for compile-time constants (#14076) --- .../remote_base/abbwelcome_protocol.cpp | 8 +++---- .../remote_base/abbwelcome_protocol.h | 4 ++-- .../components/remote_base/aeha_protocol.cpp | 14 +++++------ .../remote_base/byronsx_protocol.cpp | 10 ++++---- .../remote_base/canalsat_protocol.cpp | 8 +++---- .../components/remote_base/dish_protocol.cpp | 10 ++++---- .../components/remote_base/dooya_protocol.cpp | 12 +++++----- .../remote_base/drayton_protocol.cpp | 22 ++++++++--------- .../components/remote_base/jvc_protocol.cpp | 12 +++++----- .../remote_base/keeloq_protocol.cpp | 22 ++++++++--------- .../components/remote_base/lg_protocol.cpp | 10 ++++---- .../remote_base/magiquest_protocol.cpp | 10 ++++---- .../components/remote_base/midea_protocol.h | 2 +- .../components/remote_base/nec_protocol.cpp | 10 ++++---- .../components/remote_base/nexa_protocol.cpp | 22 ++++++++--------- .../remote_base/panasonic_protocol.cpp | 10 ++++---- .../remote_base/pioneer_protocol.cpp | 12 +++++----- .../remote_base/pronto_protocol.cpp | 24 +++++++++---------- .../components/remote_base/rc5_protocol.cpp | 4 ++-- .../components/remote_base/rc6_protocol.cpp | 10 ++++---- .../remote_base/roomba_protocol.cpp | 10 ++++---- .../remote_base/samsung36_protocol.cpp | 20 ++++++++-------- .../remote_base/samsung_protocol.cpp | 14 +++++------ .../components/remote_base/sony_protocol.cpp | 10 ++++---- .../remote_base/symphony_protocol.cpp | 14 +++++------ .../remote_base/toshiba_ac_protocol.cpp | 16 ++++++------- .../components/remote_base/toto_protocol.cpp | 12 +++++----- 27 files changed, 166 insertions(+), 166 deletions(-) diff --git a/esphome/components/remote_base/abbwelcome_protocol.cpp b/esphome/components/remote_base/abbwelcome_protocol.cpp index 352ae10ed7..a67ca48dbe 100644 --- a/esphome/components/remote_base/abbwelcome_protocol.cpp +++ b/esphome/components/remote_base/abbwelcome_protocol.cpp @@ -6,10 +6,10 @@ namespace remote_base { static const char *const TAG = "remote.abbwelcome"; -static const uint32_t BIT_ONE_SPACE_US = 102; -static const uint32_t BIT_ZERO_MARK_US = 32; // 18-44 -static const uint32_t BIT_ZERO_SPACE_US = BIT_ONE_SPACE_US - BIT_ZERO_MARK_US; -static const uint16_t BYTE_SPACE_US = 210; +static constexpr uint32_t BIT_ONE_SPACE_US = 102; +static constexpr uint32_t BIT_ZERO_MARK_US = 32; // 18-44 +static constexpr uint32_t BIT_ZERO_SPACE_US = BIT_ONE_SPACE_US - BIT_ZERO_MARK_US; +static constexpr uint16_t BYTE_SPACE_US = 210; uint8_t ABBWelcomeData::calc_cs_() const { uint8_t checksum = 0; diff --git a/esphome/components/remote_base/abbwelcome_protocol.h b/esphome/components/remote_base/abbwelcome_protocol.h index 1dddedf8ce..66664a89f3 100644 --- a/esphome/components/remote_base/abbwelcome_protocol.h +++ b/esphome/components/remote_base/abbwelcome_protocol.h @@ -11,8 +11,8 @@ namespace esphome { namespace remote_base { -static const uint8_t MAX_DATA_LENGTH = 15; -static const uint8_t DATA_LENGTH_MASK = 0x3f; +static constexpr uint8_t MAX_DATA_LENGTH = 15; +static constexpr uint8_t DATA_LENGTH_MASK = 0x3f; /* Message Format: diff --git a/esphome/components/remote_base/aeha_protocol.cpp b/esphome/components/remote_base/aeha_protocol.cpp index 3b926e7981..f40cff7623 100644 --- a/esphome/components/remote_base/aeha_protocol.cpp +++ b/esphome/components/remote_base/aeha_protocol.cpp @@ -7,13 +7,13 @@ namespace remote_base { static const char *const TAG = "remote.aeha"; -static const uint16_t BITWISE = 425; -static const uint16_t HEADER_HIGH_US = BITWISE * 8; -static const uint16_t HEADER_LOW_US = BITWISE * 4; -static const uint16_t BIT_HIGH_US = BITWISE; -static const uint16_t BIT_ONE_LOW_US = BITWISE * 3; -static const uint16_t BIT_ZERO_LOW_US = BITWISE; -static const uint16_t TRAILER = BITWISE; +static constexpr uint16_t BITWISE = 425; +static constexpr uint16_t HEADER_HIGH_US = BITWISE * 8; +static constexpr uint16_t HEADER_LOW_US = BITWISE * 4; +static constexpr uint16_t BIT_HIGH_US = BITWISE; +static constexpr uint16_t BIT_ONE_LOW_US = BITWISE * 3; +static constexpr uint16_t BIT_ZERO_LOW_US = BITWISE; +static constexpr uint16_t TRAILER = BITWISE; void AEHAProtocol::encode(RemoteTransmitData *dst, const AEHAData &data) { dst->reserve(2 + 32 + (data.data.size() * 2) + 1); diff --git a/esphome/components/remote_base/byronsx_protocol.cpp b/esphome/components/remote_base/byronsx_protocol.cpp index 6bfa4b7ff9..f243b5bdfd 100644 --- a/esphome/components/remote_base/byronsx_protocol.cpp +++ b/esphome/components/remote_base/byronsx_protocol.cpp @@ -8,11 +8,11 @@ namespace remote_base { static const char *const TAG = "remote.byronsx"; -static const uint32_t BIT_TIME_US = 333; -static const uint8_t NBITS_ADDRESS = 8; -static const uint8_t NBITS_COMMAND = 4; -static const uint8_t NBITS_START_BIT = 1; -static const uint8_t NBITS_DATA = NBITS_ADDRESS + NBITS_COMMAND /*+ NBITS_COMMAND*/; +static constexpr uint32_t BIT_TIME_US = 333; +static constexpr uint8_t NBITS_ADDRESS = 8; +static constexpr uint8_t NBITS_COMMAND = 4; +static constexpr uint8_t NBITS_START_BIT = 1; +static constexpr uint8_t NBITS_DATA = NBITS_ADDRESS + NBITS_COMMAND /*+ NBITS_COMMAND*/; /* ByronSX Protocol diff --git a/esphome/components/remote_base/canalsat_protocol.cpp b/esphome/components/remote_base/canalsat_protocol.cpp index bee3d57fd8..1468b66939 100644 --- a/esphome/components/remote_base/canalsat_protocol.cpp +++ b/esphome/components/remote_base/canalsat_protocol.cpp @@ -7,10 +7,10 @@ namespace remote_base { static const char *const CANALSAT_TAG = "remote.canalsat"; static const char *const CANALSATLD_TAG = "remote.canalsatld"; -static const uint16_t CANALSAT_FREQ = 55500; -static const uint16_t CANALSATLD_FREQ = 56000; -static const uint16_t CANALSAT_UNIT = 250; -static const uint16_t CANALSATLD_UNIT = 320; +static constexpr uint16_t CANALSAT_FREQ = 55500; +static constexpr uint16_t CANALSATLD_FREQ = 56000; +static constexpr uint16_t CANALSAT_UNIT = 250; +static constexpr uint16_t CANALSATLD_UNIT = 320; CanalSatProtocol::CanalSatProtocol() { this->frequency_ = CANALSAT_FREQ; diff --git a/esphome/components/remote_base/dish_protocol.cpp b/esphome/components/remote_base/dish_protocol.cpp index 754b6c3b12..69226101bf 100644 --- a/esphome/components/remote_base/dish_protocol.cpp +++ b/esphome/components/remote_base/dish_protocol.cpp @@ -6,11 +6,11 @@ namespace remote_base { static const char *const TAG = "remote.dish"; -static const uint32_t HEADER_HIGH_US = 400; -static const uint32_t HEADER_LOW_US = 6100; -static const uint32_t BIT_HIGH_US = 400; -static const uint32_t BIT_ONE_LOW_US = 1700; -static const uint32_t BIT_ZERO_LOW_US = 2800; +static constexpr uint32_t HEADER_HIGH_US = 400; +static constexpr uint32_t HEADER_LOW_US = 6100; +static constexpr uint32_t BIT_HIGH_US = 400; +static constexpr uint32_t BIT_ONE_LOW_US = 1700; +static constexpr uint32_t BIT_ZERO_LOW_US = 2800; void DishProtocol::encode(RemoteTransmitData *dst, const DishData &data) { dst->reserve(138); diff --git a/esphome/components/remote_base/dooya_protocol.cpp b/esphome/components/remote_base/dooya_protocol.cpp index 04c5fef8f3..84bdbf3e08 100644 --- a/esphome/components/remote_base/dooya_protocol.cpp +++ b/esphome/components/remote_base/dooya_protocol.cpp @@ -6,12 +6,12 @@ namespace remote_base { static const char *const TAG = "remote.dooya"; -static const uint32_t HEADER_HIGH_US = 5000; -static const uint32_t HEADER_LOW_US = 1500; -static const uint32_t BIT_ZERO_HIGH_US = 350; -static const uint32_t BIT_ZERO_LOW_US = 750; -static const uint32_t BIT_ONE_HIGH_US = 750; -static const uint32_t BIT_ONE_LOW_US = 350; +static constexpr uint32_t HEADER_HIGH_US = 5000; +static constexpr uint32_t HEADER_LOW_US = 1500; +static constexpr uint32_t BIT_ZERO_HIGH_US = 350; +static constexpr uint32_t BIT_ZERO_LOW_US = 750; +static constexpr uint32_t BIT_ONE_HIGH_US = 750; +static constexpr uint32_t BIT_ONE_LOW_US = 350; void DooyaProtocol::encode(RemoteTransmitData *dst, const DooyaData &data) { dst->set_carrier_frequency(0); diff --git a/esphome/components/remote_base/drayton_protocol.cpp b/esphome/components/remote_base/drayton_protocol.cpp index da2e985af0..946bd9cacb 100644 --- a/esphome/components/remote_base/drayton_protocol.cpp +++ b/esphome/components/remote_base/drayton_protocol.cpp @@ -8,18 +8,18 @@ namespace remote_base { static const char *const TAG = "remote.drayton"; -static const uint32_t BIT_TIME_US = 500; -static const uint8_t CARRIER_KHZ = 2; -static const uint8_t NBITS_PREAMBLE = 12; -static const uint8_t NBITS_SYNC = 4; -static const uint8_t NBITS_ADDRESS = 16; -static const uint8_t NBITS_CHANNEL = 5; -static const uint8_t NBITS_COMMAND = 7; -static const uint8_t NDATABITS = NBITS_ADDRESS + NBITS_CHANNEL + NBITS_COMMAND; -static const uint8_t MIN_RX_SRC = (NDATABITS + NBITS_SYNC / 2); +static constexpr uint32_t BIT_TIME_US = 500; +static constexpr uint8_t CARRIER_KHZ = 2; +static constexpr uint8_t NBITS_PREAMBLE = 12; +static constexpr uint8_t NBITS_SYNC = 4; +static constexpr uint8_t NBITS_ADDRESS = 16; +static constexpr uint8_t NBITS_CHANNEL = 5; +static constexpr uint8_t NBITS_COMMAND = 7; +static constexpr uint8_t NDATABITS = NBITS_ADDRESS + NBITS_CHANNEL + NBITS_COMMAND; +static constexpr uint8_t MIN_RX_SRC = (NDATABITS + NBITS_SYNC / 2); -static const uint8_t CMD_ON = 0x41; -static const uint8_t CMD_OFF = 0x02; +static constexpr uint8_t CMD_ON = 0x41; +static constexpr uint8_t CMD_OFF = 0x02; /* Drayton Protocol diff --git a/esphome/components/remote_base/jvc_protocol.cpp b/esphome/components/remote_base/jvc_protocol.cpp index ca423b61e6..c33cae7a48 100644 --- a/esphome/components/remote_base/jvc_protocol.cpp +++ b/esphome/components/remote_base/jvc_protocol.cpp @@ -6,12 +6,12 @@ namespace remote_base { static const char *const TAG = "remote.jvc"; -static const uint8_t NBITS = 16; -static const uint32_t HEADER_HIGH_US = 8400; -static const uint32_t HEADER_LOW_US = 4200; -static const uint32_t BIT_ONE_LOW_US = 1725; -static const uint32_t BIT_ZERO_LOW_US = 525; -static const uint32_t BIT_HIGH_US = 525; +static constexpr uint8_t NBITS = 16; +static constexpr uint32_t HEADER_HIGH_US = 8400; +static constexpr uint32_t HEADER_LOW_US = 4200; +static constexpr uint32_t BIT_ONE_LOW_US = 1725; +static constexpr uint32_t BIT_ZERO_LOW_US = 525; +static constexpr uint32_t BIT_HIGH_US = 525; void JVCProtocol::encode(RemoteTransmitData *dst, const JVCData &data) { dst->set_carrier_frequency(38000); diff --git a/esphome/components/remote_base/keeloq_protocol.cpp b/esphome/components/remote_base/keeloq_protocol.cpp index 72540c37f1..e95c79ef25 100644 --- a/esphome/components/remote_base/keeloq_protocol.cpp +++ b/esphome/components/remote_base/keeloq_protocol.cpp @@ -8,18 +8,18 @@ namespace remote_base { static const char *const TAG = "remote.keeloq"; -static const uint32_t BIT_TIME_US = 380; -static const uint8_t NBITS_PREAMBLE = 12; -static const uint8_t NBITS_REPEAT = 1; -static const uint8_t NBITS_VLOW = 1; -static const uint8_t NBITS_SERIAL = 28; -static const uint8_t NBITS_BUTTONS = 4; -static const uint8_t NBITS_DISC = 12; -static const uint8_t NBITS_SYNC_CNT = 16; +static constexpr uint32_t BIT_TIME_US = 380; +static constexpr uint8_t NBITS_PREAMBLE = 12; +static constexpr uint8_t NBITS_REPEAT = 1; +static constexpr uint8_t NBITS_VLOW = 1; +static constexpr uint8_t NBITS_SERIAL = 28; +static constexpr uint8_t NBITS_BUTTONS = 4; +static constexpr uint8_t NBITS_DISC = 12; +static constexpr uint8_t NBITS_SYNC_CNT = 16; -static const uint8_t NBITS_FIXED_DATA = NBITS_REPEAT + NBITS_VLOW + NBITS_BUTTONS + NBITS_SERIAL; -static const uint8_t NBITS_ENCRYPTED_DATA = NBITS_BUTTONS + NBITS_DISC + NBITS_SYNC_CNT; -static const uint8_t NBITS_DATA = NBITS_FIXED_DATA + NBITS_ENCRYPTED_DATA; +static constexpr uint8_t NBITS_FIXED_DATA = NBITS_REPEAT + NBITS_VLOW + NBITS_BUTTONS + NBITS_SERIAL; +static constexpr uint8_t NBITS_ENCRYPTED_DATA = NBITS_BUTTONS + NBITS_DISC + NBITS_SYNC_CNT; +static constexpr uint8_t NBITS_DATA = NBITS_FIXED_DATA + NBITS_ENCRYPTED_DATA; /* KeeLoq Protocol diff --git a/esphome/components/remote_base/lg_protocol.cpp b/esphome/components/remote_base/lg_protocol.cpp index d25c59d2b1..4c54ff00bd 100644 --- a/esphome/components/remote_base/lg_protocol.cpp +++ b/esphome/components/remote_base/lg_protocol.cpp @@ -6,11 +6,11 @@ namespace remote_base { static const char *const TAG = "remote.lg"; -static const uint32_t HEADER_HIGH_US = 8000; -static const uint32_t HEADER_LOW_US = 4000; -static const uint32_t BIT_HIGH_US = 600; -static const uint32_t BIT_ONE_LOW_US = 1600; -static const uint32_t BIT_ZERO_LOW_US = 550; +static constexpr uint32_t HEADER_HIGH_US = 8000; +static constexpr uint32_t HEADER_LOW_US = 4000; +static constexpr uint32_t BIT_HIGH_US = 600; +static constexpr uint32_t BIT_ONE_LOW_US = 1600; +static constexpr uint32_t BIT_ZERO_LOW_US = 550; void LGProtocol::encode(RemoteTransmitData *dst, const LGData &data) { dst->set_carrier_frequency(38000); diff --git a/esphome/components/remote_base/magiquest_protocol.cpp b/esphome/components/remote_base/magiquest_protocol.cpp index 1cec58a55f..f25a982fdc 100644 --- a/esphome/components/remote_base/magiquest_protocol.cpp +++ b/esphome/components/remote_base/magiquest_protocol.cpp @@ -10,11 +10,11 @@ namespace remote_base { static const char *const TAG = "remote.magiquest"; -static const uint32_t MAGIQUEST_UNIT = 288; // us -static const uint32_t MAGIQUEST_ONE_MARK = 2 * MAGIQUEST_UNIT; -static const uint32_t MAGIQUEST_ONE_SPACE = 2 * MAGIQUEST_UNIT; -static const uint32_t MAGIQUEST_ZERO_MARK = MAGIQUEST_UNIT; -static const uint32_t MAGIQUEST_ZERO_SPACE = 3 * MAGIQUEST_UNIT; +static constexpr uint32_t MAGIQUEST_UNIT = 288; // us +static constexpr uint32_t MAGIQUEST_ONE_MARK = 2 * MAGIQUEST_UNIT; +static constexpr uint32_t MAGIQUEST_ONE_SPACE = 2 * MAGIQUEST_UNIT; +static constexpr uint32_t MAGIQUEST_ZERO_MARK = MAGIQUEST_UNIT; +static constexpr uint32_t MAGIQUEST_ZERO_SPACE = 3 * MAGIQUEST_UNIT; void MagiQuestProtocol::encode(RemoteTransmitData *dst, const MagiQuestData &data) { dst->reserve(101); // 2 start bits, 48 data bits, 1 stop bit diff --git a/esphome/components/remote_base/midea_protocol.h b/esphome/components/remote_base/midea_protocol.h index ddefff867a..334e8a7cb3 100644 --- a/esphome/components/remote_base/midea_protocol.h +++ b/esphome/components/remote_base/midea_protocol.h @@ -62,7 +62,7 @@ class MideaData { this->data_[idx] |= (value << shift); } void set_mask_(uint8_t idx, bool state, uint8_t mask = 255) { this->set_value_(idx, state ? mask : 0, mask); } - static const uint8_t OFFSET_CS = 5; + static constexpr uint8_t OFFSET_CS = 5; // 48-bits data std::array data_; // Calculate checksum diff --git a/esphome/components/remote_base/nec_protocol.cpp b/esphome/components/remote_base/nec_protocol.cpp index 6ea9a8583c..062f81b4d6 100644 --- a/esphome/components/remote_base/nec_protocol.cpp +++ b/esphome/components/remote_base/nec_protocol.cpp @@ -6,11 +6,11 @@ namespace remote_base { static const char *const TAG = "remote.nec"; -static const uint32_t HEADER_HIGH_US = 9000; -static const uint32_t HEADER_LOW_US = 4500; -static const uint32_t BIT_HIGH_US = 560; -static const uint32_t BIT_ONE_LOW_US = 1690; -static const uint32_t BIT_ZERO_LOW_US = 560; +static constexpr uint32_t HEADER_HIGH_US = 9000; +static constexpr uint32_t HEADER_LOW_US = 4500; +static constexpr uint32_t BIT_HIGH_US = 560; +static constexpr uint32_t BIT_ONE_LOW_US = 1690; +static constexpr uint32_t BIT_ZERO_LOW_US = 560; void NECProtocol::encode(RemoteTransmitData *dst, const NECData &data) { ESP_LOGD(TAG, "Sending NEC: address=0x%04X, command=0x%04X command_repeats=%d", data.address, data.command, diff --git a/esphome/components/remote_base/nexa_protocol.cpp b/esphome/components/remote_base/nexa_protocol.cpp index 862165714e..28b415d4d6 100644 --- a/esphome/components/remote_base/nexa_protocol.cpp +++ b/esphome/components/remote_base/nexa_protocol.cpp @@ -6,18 +6,18 @@ namespace remote_base { static const char *const TAG = "remote.nexa"; -static const uint8_t NBITS = 32; -static const uint32_t HEADER_HIGH_US = 319; -static const uint32_t HEADER_LOW_US = 2610; -static const uint32_t BIT_HIGH_US = 319; -static const uint32_t BIT_ONE_LOW_US = 1000; -static const uint32_t BIT_ZERO_LOW_US = 140; +static constexpr uint8_t NBITS = 32; +static constexpr uint32_t HEADER_HIGH_US = 319; +static constexpr uint32_t HEADER_LOW_US = 2610; +static constexpr uint32_t BIT_HIGH_US = 319; +static constexpr uint32_t BIT_ONE_LOW_US = 1000; +static constexpr uint32_t BIT_ZERO_LOW_US = 140; -static const uint32_t TX_HEADER_HIGH_US = 250; -static const uint32_t TX_HEADER_LOW_US = TX_HEADER_HIGH_US * 10; -static const uint32_t TX_BIT_HIGH_US = 250; -static const uint32_t TX_BIT_ONE_LOW_US = TX_BIT_HIGH_US * 5; -static const uint32_t TX_BIT_ZERO_LOW_US = TX_BIT_HIGH_US * 1; +static constexpr uint32_t TX_HEADER_HIGH_US = 250; +static constexpr uint32_t TX_HEADER_LOW_US = TX_HEADER_HIGH_US * 10; +static constexpr uint32_t TX_BIT_HIGH_US = 250; +static constexpr uint32_t TX_BIT_ONE_LOW_US = TX_BIT_HIGH_US * 5; +static constexpr uint32_t TX_BIT_ZERO_LOW_US = TX_BIT_HIGH_US * 1; void NexaProtocol::one(RemoteTransmitData *dst) const { // '1' => '10' diff --git a/esphome/components/remote_base/panasonic_protocol.cpp b/esphome/components/remote_base/panasonic_protocol.cpp index d6cf1a160d..e0acc42692 100644 --- a/esphome/components/remote_base/panasonic_protocol.cpp +++ b/esphome/components/remote_base/panasonic_protocol.cpp @@ -6,11 +6,11 @@ namespace remote_base { static const char *const TAG = "remote.panasonic"; -static const uint32_t HEADER_HIGH_US = 3502; -static const uint32_t HEADER_LOW_US = 1750; -static const uint32_t BIT_HIGH_US = 502; -static const uint32_t BIT_ZERO_LOW_US = 400; -static const uint32_t BIT_ONE_LOW_US = 1244; +static constexpr uint32_t HEADER_HIGH_US = 3502; +static constexpr uint32_t HEADER_LOW_US = 1750; +static constexpr uint32_t BIT_HIGH_US = 502; +static constexpr uint32_t BIT_ZERO_LOW_US = 400; +static constexpr uint32_t BIT_ONE_LOW_US = 1244; void PanasonicProtocol::encode(RemoteTransmitData *dst, const PanasonicData &data) { dst->reserve(100); diff --git a/esphome/components/remote_base/pioneer_protocol.cpp b/esphome/components/remote_base/pioneer_protocol.cpp index 043565282d..f350ef66ae 100644 --- a/esphome/components/remote_base/pioneer_protocol.cpp +++ b/esphome/components/remote_base/pioneer_protocol.cpp @@ -6,12 +6,12 @@ namespace remote_base { static const char *const TAG = "remote.pioneer"; -static const uint32_t HEADER_HIGH_US = 9000; -static const uint32_t HEADER_LOW_US = 4500; -static const uint32_t BIT_HIGH_US = 560; -static const uint32_t BIT_ONE_LOW_US = 1690; -static const uint32_t BIT_ZERO_LOW_US = 560; -static const uint32_t TRAILER_SPACE_US = 25500; +static constexpr uint32_t HEADER_HIGH_US = 9000; +static constexpr uint32_t HEADER_LOW_US = 4500; +static constexpr uint32_t BIT_HIGH_US = 560; +static constexpr uint32_t BIT_ONE_LOW_US = 1690; +static constexpr uint32_t BIT_ZERO_LOW_US = 560; +static constexpr uint32_t TRAILER_SPACE_US = 25500; void PioneerProtocol::encode(RemoteTransmitData *dst, const PioneerData &data) { uint32_t address1 = ((data.rc_code_1 & 0xff00) | (~(data.rc_code_1 >> 8) & 0xff)); diff --git a/esphome/components/remote_base/pronto_protocol.cpp b/esphome/components/remote_base/pronto_protocol.cpp index 401a0976b2..cff3145199 100644 --- a/esphome/components/remote_base/pronto_protocol.cpp +++ b/esphome/components/remote_base/pronto_protocol.cpp @@ -59,18 +59,18 @@ bool ProntoData::operator==(const ProntoData &rhs) const { } // DO NOT EXPORT from this file -static const uint16_t MICROSECONDS_T_MAX = 0xFFFFU; -static const uint16_t LEARNED_TOKEN = 0x0000U; -static const uint16_t LEARNED_NON_MODULATED_TOKEN = 0x0100U; -static const uint16_t BITS_IN_HEXADECIMAL = 4U; -static const uint16_t DIGITS_IN_PRONTO_NUMBER = 4U; -static const uint16_t NUMBERS_IN_PREAMBLE = 4U; -static const uint16_t HEX_MASK = 0xFU; -static const uint32_t REFERENCE_FREQUENCY = 4145146UL; -static const uint16_t FALLBACK_FREQUENCY = 64767U; // To use with frequency = 0; -static const uint32_t MICROSECONDS_IN_SECONDS = 1000000UL; -static const uint16_t PRONTO_DEFAULT_GAP = 45000; -static const uint16_t MARK_EXCESS_MICROS = 20; +static constexpr uint16_t MICROSECONDS_T_MAX = 0xFFFFU; +static constexpr uint16_t LEARNED_TOKEN = 0x0000U; +static constexpr uint16_t LEARNED_NON_MODULATED_TOKEN = 0x0100U; +static constexpr uint16_t BITS_IN_HEXADECIMAL = 4U; +static constexpr uint16_t DIGITS_IN_PRONTO_NUMBER = 4U; +static constexpr uint16_t NUMBERS_IN_PREAMBLE = 4U; +static constexpr uint16_t HEX_MASK = 0xFU; +static constexpr uint32_t REFERENCE_FREQUENCY = 4145146UL; +static constexpr uint16_t FALLBACK_FREQUENCY = 64767U; // To use with frequency = 0; +static constexpr uint32_t MICROSECONDS_IN_SECONDS = 1000000UL; +static constexpr uint16_t PRONTO_DEFAULT_GAP = 45000; +static constexpr uint16_t MARK_EXCESS_MICROS = 20; static constexpr size_t PRONTO_LOG_CHUNK_SIZE = 230; static uint16_t to_frequency_k_hz(uint16_t code) { diff --git a/esphome/components/remote_base/rc5_protocol.cpp b/esphome/components/remote_base/rc5_protocol.cpp index 08f2f2eaa3..bb6d382d80 100644 --- a/esphome/components/remote_base/rc5_protocol.cpp +++ b/esphome/components/remote_base/rc5_protocol.cpp @@ -6,8 +6,8 @@ namespace remote_base { static const char *const TAG = "remote.rc5"; -static const uint32_t BIT_TIME_US = 889; -static const uint8_t NBITS = 14; +static constexpr uint32_t BIT_TIME_US = 889; +static constexpr uint8_t NBITS = 14; void RC5Protocol::encode(RemoteTransmitData *dst, const RC5Data &data) { static bool toggle = false; diff --git a/esphome/components/remote_base/rc6_protocol.cpp b/esphome/components/remote_base/rc6_protocol.cpp index fcb4da11a4..b442bb4c27 100644 --- a/esphome/components/remote_base/rc6_protocol.cpp +++ b/esphome/components/remote_base/rc6_protocol.cpp @@ -6,11 +6,11 @@ namespace remote_base { static const char *const RC6_TAG = "remote.rc6"; -static const uint16_t RC6_FREQ = 36000; -static const uint16_t RC6_UNIT = 444; -static const uint16_t RC6_HEADER_MARK = (6 * RC6_UNIT); -static const uint16_t RC6_HEADER_SPACE = (2 * RC6_UNIT); -static const uint16_t RC6_MODE_MASK = 0x07; +static constexpr uint16_t RC6_FREQ = 36000; +static constexpr uint16_t RC6_UNIT = 444; +static constexpr uint16_t RC6_HEADER_MARK = (6 * RC6_UNIT); +static constexpr uint16_t RC6_HEADER_SPACE = (2 * RC6_UNIT); +static constexpr uint16_t RC6_MODE_MASK = 0x07; void RC6Protocol::encode(RemoteTransmitData *dst, const RC6Data &data) { dst->reserve(44); diff --git a/esphome/components/remote_base/roomba_protocol.cpp b/esphome/components/remote_base/roomba_protocol.cpp index 2d2dde114a..6b7d216374 100644 --- a/esphome/components/remote_base/roomba_protocol.cpp +++ b/esphome/components/remote_base/roomba_protocol.cpp @@ -6,11 +6,11 @@ namespace remote_base { static const char *const TAG = "remote.roomba"; -static const uint8_t NBITS = 8; -static const uint32_t BIT_ONE_HIGH_US = 3000; -static const uint32_t BIT_ONE_LOW_US = 1000; -static const uint32_t BIT_ZERO_HIGH_US = BIT_ONE_LOW_US; -static const uint32_t BIT_ZERO_LOW_US = BIT_ONE_HIGH_US; +static constexpr uint8_t NBITS = 8; +static constexpr uint32_t BIT_ONE_HIGH_US = 3000; +static constexpr uint32_t BIT_ONE_LOW_US = 1000; +static constexpr uint32_t BIT_ZERO_HIGH_US = BIT_ONE_LOW_US; +static constexpr uint32_t BIT_ZERO_LOW_US = BIT_ONE_HIGH_US; void RoombaProtocol::encode(RemoteTransmitData *dst, const RoombaData &data) { dst->set_carrier_frequency(38000); diff --git a/esphome/components/remote_base/samsung36_protocol.cpp b/esphome/components/remote_base/samsung36_protocol.cpp index bd3eee5849..10e8bd2d01 100644 --- a/esphome/components/remote_base/samsung36_protocol.cpp +++ b/esphome/components/remote_base/samsung36_protocol.cpp @@ -6,17 +6,17 @@ namespace remote_base { static const char *const TAG = "remote.samsung36"; -static const uint8_t NBITS = 78; +static constexpr uint8_t NBITS = 78; -static const uint32_t HEADER_HIGH_US = 4500; -static const uint32_t HEADER_LOW_US = 4500; -static const uint32_t BIT_HIGH_US = 500; -static const uint32_t BIT_ONE_LOW_US = 1500; -static const uint32_t BIT_ZERO_LOW_US = 500; -static const uint32_t MIDDLE_HIGH_US = 500; -static const uint32_t MIDDLE_LOW_US = 4500; -static const uint32_t FOOTER_HIGH_US = 500; -static const uint32_t FOOTER_LOW_US = 59000; +static constexpr uint32_t HEADER_HIGH_US = 4500; +static constexpr uint32_t HEADER_LOW_US = 4500; +static constexpr uint32_t BIT_HIGH_US = 500; +static constexpr uint32_t BIT_ONE_LOW_US = 1500; +static constexpr uint32_t BIT_ZERO_LOW_US = 500; +static constexpr uint32_t MIDDLE_HIGH_US = 500; +static constexpr uint32_t MIDDLE_LOW_US = 4500; +static constexpr uint32_t FOOTER_HIGH_US = 500; +static constexpr uint32_t FOOTER_LOW_US = 59000; void Samsung36Protocol::encode(RemoteTransmitData *dst, const Samsung36Data &data) { dst->set_carrier_frequency(38000); diff --git a/esphome/components/remote_base/samsung_protocol.cpp b/esphome/components/remote_base/samsung_protocol.cpp index 2d6d5531e5..2a48cbb918 100644 --- a/esphome/components/remote_base/samsung_protocol.cpp +++ b/esphome/components/remote_base/samsung_protocol.cpp @@ -7,13 +7,13 @@ namespace remote_base { static const char *const TAG = "remote.samsung"; -static const uint32_t HEADER_HIGH_US = 4500; -static const uint32_t HEADER_LOW_US = 4500; -static const uint32_t BIT_HIGH_US = 560; -static const uint32_t BIT_ONE_LOW_US = 1690; -static const uint32_t BIT_ZERO_LOW_US = 560; -static const uint32_t FOOTER_HIGH_US = 560; -static const uint32_t FOOTER_LOW_US = 560; +static constexpr uint32_t HEADER_HIGH_US = 4500; +static constexpr uint32_t HEADER_LOW_US = 4500; +static constexpr uint32_t BIT_HIGH_US = 560; +static constexpr uint32_t BIT_ONE_LOW_US = 1690; +static constexpr uint32_t BIT_ZERO_LOW_US = 560; +static constexpr uint32_t FOOTER_HIGH_US = 560; +static constexpr uint32_t FOOTER_LOW_US = 560; void SamsungProtocol::encode(RemoteTransmitData *dst, const SamsungData &data) { dst->set_carrier_frequency(38000); diff --git a/esphome/components/remote_base/sony_protocol.cpp b/esphome/components/remote_base/sony_protocol.cpp index 69f2b49c42..504b346925 100644 --- a/esphome/components/remote_base/sony_protocol.cpp +++ b/esphome/components/remote_base/sony_protocol.cpp @@ -6,11 +6,11 @@ namespace remote_base { static const char *const TAG = "remote.sony"; -static const uint32_t HEADER_HIGH_US = 2400; -static const uint32_t HEADER_LOW_US = 600; -static const uint32_t BIT_ONE_HIGH_US = 1200; -static const uint32_t BIT_ZERO_HIGH_US = 600; -static const uint32_t BIT_LOW_US = 600; +static constexpr uint32_t HEADER_HIGH_US = 2400; +static constexpr uint32_t HEADER_LOW_US = 600; +static constexpr uint32_t BIT_ONE_HIGH_US = 1200; +static constexpr uint32_t BIT_ZERO_HIGH_US = 600; +static constexpr uint32_t BIT_LOW_US = 600; void SonyProtocol::encode(RemoteTransmitData *dst, const SonyData &data) { dst->set_carrier_frequency(40000); diff --git a/esphome/components/remote_base/symphony_protocol.cpp b/esphome/components/remote_base/symphony_protocol.cpp index 34b5dba07f..f30a980d91 100644 --- a/esphome/components/remote_base/symphony_protocol.cpp +++ b/esphome/components/remote_base/symphony_protocol.cpp @@ -13,16 +13,16 @@ static const char *const TAG = "remote.symphony"; // footer-gap handling used there. // Symphony protocol timing specifications (tuned to handset captures) -static const uint32_t BIT_ZERO_HIGH_US = 460; // short -static const uint32_t BIT_ZERO_LOW_US = 1260; // long -static const uint32_t BIT_ONE_HIGH_US = 1260; // long -static const uint32_t BIT_ONE_LOW_US = 460; // short -static const uint32_t CARRIER_FREQUENCY = 38000; +static constexpr uint32_t BIT_ZERO_HIGH_US = 460; // short +static constexpr uint32_t BIT_ZERO_LOW_US = 1260; // long +static constexpr uint32_t BIT_ONE_HIGH_US = 1260; // long +static constexpr uint32_t BIT_ONE_LOW_US = 460; // short +static constexpr uint32_t CARRIER_FREQUENCY = 38000; // IRremoteESP8266 reference: kSymphonyFooterGap = 4 * (mark + space) -static const uint32_t FOOTER_GAP_US = 4 * (BIT_ZERO_HIGH_US + BIT_ZERO_LOW_US); +static constexpr uint32_t FOOTER_GAP_US = 4 * (BIT_ZERO_HIGH_US + BIT_ZERO_LOW_US); // Typical inter-frame gap (~34.8 ms observed) -static const uint32_t INTER_FRAME_GAP_US = 34760; +static constexpr uint32_t INTER_FRAME_GAP_US = 34760; void SymphonyProtocol::encode(RemoteTransmitData *dst, const SymphonyData &data) { dst->set_carrier_frequency(CARRIER_FREQUENCY); diff --git a/esphome/components/remote_base/toshiba_ac_protocol.cpp b/esphome/components/remote_base/toshiba_ac_protocol.cpp index 42241eea8c..a20a29b84a 100644 --- a/esphome/components/remote_base/toshiba_ac_protocol.cpp +++ b/esphome/components/remote_base/toshiba_ac_protocol.cpp @@ -7,14 +7,14 @@ namespace remote_base { static const char *const TAG = "remote.toshibaac"; -static const uint32_t HEADER_HIGH_US = 4500; -static const uint32_t HEADER_LOW_US = 4500; -static const uint32_t BIT_HIGH_US = 560; -static const uint32_t BIT_ONE_LOW_US = 1690; -static const uint32_t BIT_ZERO_LOW_US = 560; -static const uint32_t FOOTER_HIGH_US = 560; -static const uint32_t FOOTER_LOW_US = 4500; -static const uint16_t PACKET_SPACE = 5500; +static constexpr uint32_t HEADER_HIGH_US = 4500; +static constexpr uint32_t HEADER_LOW_US = 4500; +static constexpr uint32_t BIT_HIGH_US = 560; +static constexpr uint32_t BIT_ONE_LOW_US = 1690; +static constexpr uint32_t BIT_ZERO_LOW_US = 560; +static constexpr uint32_t FOOTER_HIGH_US = 560; +static constexpr uint32_t FOOTER_LOW_US = 4500; +static constexpr uint16_t PACKET_SPACE = 5500; void ToshibaAcProtocol::encode(RemoteTransmitData *dst, const ToshibaAcData &data) { dst->set_carrier_frequency(38000); diff --git a/esphome/components/remote_base/toto_protocol.cpp b/esphome/components/remote_base/toto_protocol.cpp index ba21263bc8..f08258c4a3 100644 --- a/esphome/components/remote_base/toto_protocol.cpp +++ b/esphome/components/remote_base/toto_protocol.cpp @@ -6,12 +6,12 @@ namespace remote_base { static const char *const TAG = "remote.toto"; -static const uint32_t PREAMBLE_HIGH_US = 6200; -static const uint32_t PREAMBLE_LOW_US = 2800; -static const uint32_t BIT_HIGH_US = 550; -static const uint32_t BIT_ONE_LOW_US = 1700; -static const uint32_t BIT_ZERO_LOW_US = 550; -static const uint32_t TOTO_HEADER = 0x2008; +static constexpr uint32_t PREAMBLE_HIGH_US = 6200; +static constexpr uint32_t PREAMBLE_LOW_US = 2800; +static constexpr uint32_t BIT_HIGH_US = 550; +static constexpr uint32_t BIT_ONE_LOW_US = 1700; +static constexpr uint32_t BIT_ZERO_LOW_US = 550; +static constexpr uint32_t TOTO_HEADER = 0x2008; void TotoProtocol::encode(RemoteTransmitData *dst, const TotoData &data) { uint32_t payload = 0; From dff9780d3af9f6ce41bf9b3db8e6138dc197c08f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 21:19:48 -0600 Subject: [PATCH 081/197] [core] Use constexpr for compile-time constants (#14071) --- esphome/core/application.h | 2 +- esphome/core/helpers.cpp | 6 +++--- esphome/core/scheduler.cpp | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/esphome/core/application.h b/esphome/core/application.h index e0299f3db3..5b3e3dfed6 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -111,7 +111,7 @@ namespace esphome { // For reboots, it's more important to shut down quickly than disconnect cleanly // since we're not entering deep sleep. The only consequence of not shutting down // cleanly is a warning in the log. -static const uint32_t TEARDOWN_TIMEOUT_REBOOT_MS = 1000; // 1 second for quick reboot +static constexpr uint32_t TEARDOWN_TIMEOUT_REBOOT_MS = 1000; // 1 second for quick reboot class Application { public: diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index c2f7f67d9a..09e755ca71 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -846,9 +846,9 @@ void IRAM_ATTR HOT delay_microseconds_safe(uint32_t us) { // avoids CPU locks that could trigger WDT or affect WiFi/BT stability uint32_t start = micros(); - const uint32_t lag = 5000; // microseconds, specifies the maximum time for a CPU busy-loop. - // it must be larger than the worst-case duration of a delay(1) call (hardware tasks) - // 5ms is conservative, it could be reduced when exact BT/WiFi stack delays are known + constexpr uint32_t lag = 5000; // microseconds, specifies the maximum time for a CPU busy-loop. + // it must be larger than the worst-case duration of a delay(1) call (hardware tasks) + // 5ms is conservative, it could be reduced when exact BT/WiFi stack delays are known if (us > lag) { delay((us - lag) / 1000UL); // note: in disabled-interrupt contexts delay() won't actually sleep while (micros() - start < us - lag) diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index 4194c3aa9e..3294f689e8 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -728,7 +728,7 @@ uint64_t Scheduler::millis_64_(uint32_t now) { // Define a safe window around the rollover point (10 seconds) // This covers any reasonable scheduler delays or thread preemption - static const uint32_t ROLLOVER_WINDOW = 10000; // 10 seconds in milliseconds + static constexpr uint32_t ROLLOVER_WINDOW = 10000; // 10 seconds in milliseconds // Check if we're near the rollover boundary (close to std::numeric_limits::max() or just past 0) bool near_rollover = (last > (std::numeric_limits::max() - ROLLOVER_WINDOW)) || (now < ROLLOVER_WINDOW); From e7f202186450ad2f865ecbc865ca87b10b243dc3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 21:32:24 -0600 Subject: [PATCH 082/197] [http_request] Replace std::map with std::vector in action template (#14026) --- esphome/components/http_request/__init__.py | 6 +++++- esphome/components/http_request/http_request.h | 11 ++++++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/esphome/components/http_request/__init__.py b/esphome/components/http_request/__init__.py index 5faffccbe4..2d6ecae0bc 100644 --- a/esphome/components/http_request/__init__.py +++ b/esphome/components/http_request/__init__.py @@ -302,10 +302,14 @@ async def http_request_action_to_code(config, action_id, template_arg, args): lambda_ = await cg.process_lambda(json_, args_, return_type=cg.void) cg.add(var.set_json(lambda_)) else: + cg.add(var.init_json(len(json_))) for key in json_: template_ = await cg.templatable(json_[key], args, cg.std_string) cg.add(var.add_json(key, template_)) - for key, value in config.get(CONF_REQUEST_HEADERS, {}).items(): + request_headers = config.get(CONF_REQUEST_HEADERS, {}) + if request_headers: + cg.add(var.init_request_headers(len(request_headers))) + for key, value in request_headers.items(): template_ = await cg.templatable(value, args, cg.const_char_ptr) cg.add(var.add_request_header(key, template_)) diff --git a/esphome/components/http_request/http_request.h b/esphome/components/http_request/http_request.h index 458ffe94a8..2b2d05c63f 100644 --- a/esphome/components/http_request/http_request.h +++ b/esphome/components/http_request/http_request.h @@ -1,7 +1,6 @@ #pragma once #include -#include #include #include #include @@ -399,13 +398,15 @@ template class HttpRequestSendAction : public Action { TEMPLATABLE_VALUE(bool, capture_response) #endif + void init_request_headers(size_t count) { this->request_headers_.init(count); } void add_request_header(const char *key, TemplatableValue value) { - this->request_headers_.insert({key, value}); + this->request_headers_.push_back({key, value}); } void add_collect_header(const char *value) { this->lower_case_collect_headers_.push_back(value); } - void add_json(const char *key, TemplatableValue value) { this->json_.insert({key, value}); } + void init_json(size_t count) { this->json_.init(count); } + void add_json(const char *key, TemplatableValue value) { this->json_.push_back({key, value}); } void set_json(std::function json_func) { this->json_func_ = json_func; } @@ -507,9 +508,9 @@ template class HttpRequestSendAction : public Action { } void encode_json_func_(Ts... x, JsonObject root) { this->json_func_(x..., root); } HttpRequestComponent *parent_; - std::map> request_headers_{}; + FixedVector>> request_headers_{}; std::vector lower_case_collect_headers_{"content-type", "content-length"}; - std::map> json_{}; + FixedVector>> json_{}; std::function json_func_{nullptr}; #ifdef USE_HTTP_REQUEST_RESPONSE Trigger, std::string &, Ts...> success_trigger_with_response_; From eaf0d03a37a85ead5f33c5b3512d80944f2bfa09 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 21:32:37 -0600 Subject: [PATCH 083/197] [ld2420] Use constexpr for compile-time constants (#14079) --- esphome/components/ld2420/ld2420.cpp | 118 +++++++++++++-------------- esphome/components/ld2420/ld2420.h | 6 +- 2 files changed, 62 insertions(+), 62 deletions(-) diff --git a/esphome/components/ld2420/ld2420.cpp b/esphome/components/ld2420/ld2420.cpp index 69b69f4a61..cf78a1a460 100644 --- a/esphome/components/ld2420/ld2420.cpp +++ b/esphome/components/ld2420/ld2420.cpp @@ -63,73 +63,73 @@ namespace esphome::ld2420 { static const char *const TAG = "ld2420"; // Local const's -static const uint16_t REFRESH_RATE_MS = 1000; +static constexpr uint16_t REFRESH_RATE_MS = 1000; // Command sets -static const uint16_t CMD_DISABLE_CONF = 0x00FE; -static const uint16_t CMD_ENABLE_CONF = 0x00FF; -static const uint16_t CMD_PARM_HIGH_TRESH = 0x0012; -static const uint16_t CMD_PARM_LOW_TRESH = 0x0021; -static const uint16_t CMD_PROTOCOL_VER = 0x0002; -static const uint16_t CMD_READ_ABD_PARAM = 0x0008; -static const uint16_t CMD_READ_REG_ADDR = 0x0020; -static const uint16_t CMD_READ_REGISTER = 0x0002; -static const uint16_t CMD_READ_SERIAL_NUM = 0x0011; -static const uint16_t CMD_READ_SYS_PARAM = 0x0013; -static const uint16_t CMD_READ_VERSION = 0x0000; -static const uint16_t CMD_RESTART = 0x0068; -static const uint16_t CMD_SYSTEM_MODE = 0x0000; -static const uint16_t CMD_SYSTEM_MODE_GR = 0x0003; -static const uint16_t CMD_SYSTEM_MODE_MTT = 0x0001; -static const uint16_t CMD_SYSTEM_MODE_SIMPLE = 0x0064; -static const uint16_t CMD_SYSTEM_MODE_DEBUG = 0x0000; -static const uint16_t CMD_SYSTEM_MODE_ENERGY = 0x0004; -static const uint16_t CMD_SYSTEM_MODE_VS = 0x0002; -static const uint16_t CMD_WRITE_ABD_PARAM = 0x0007; -static const uint16_t CMD_WRITE_REGISTER = 0x0001; -static const uint16_t CMD_WRITE_SYS_PARAM = 0x0012; +static constexpr uint16_t CMD_DISABLE_CONF = 0x00FE; +static constexpr uint16_t CMD_ENABLE_CONF = 0x00FF; +static constexpr uint16_t CMD_PARM_HIGH_TRESH = 0x0012; +static constexpr uint16_t CMD_PARM_LOW_TRESH = 0x0021; +static constexpr uint16_t CMD_PROTOCOL_VER = 0x0002; +static constexpr uint16_t CMD_READ_ABD_PARAM = 0x0008; +static constexpr uint16_t CMD_READ_REG_ADDR = 0x0020; +static constexpr uint16_t CMD_READ_REGISTER = 0x0002; +static constexpr uint16_t CMD_READ_SERIAL_NUM = 0x0011; +static constexpr uint16_t CMD_READ_SYS_PARAM = 0x0013; +static constexpr uint16_t CMD_READ_VERSION = 0x0000; +static constexpr uint16_t CMD_RESTART = 0x0068; +static constexpr uint16_t CMD_SYSTEM_MODE = 0x0000; +static constexpr uint16_t CMD_SYSTEM_MODE_GR = 0x0003; +static constexpr uint16_t CMD_SYSTEM_MODE_MTT = 0x0001; +static constexpr uint16_t CMD_SYSTEM_MODE_SIMPLE = 0x0064; +static constexpr uint16_t CMD_SYSTEM_MODE_DEBUG = 0x0000; +static constexpr uint16_t CMD_SYSTEM_MODE_ENERGY = 0x0004; +static constexpr uint16_t CMD_SYSTEM_MODE_VS = 0x0002; +static constexpr uint16_t CMD_WRITE_ABD_PARAM = 0x0007; +static constexpr uint16_t CMD_WRITE_REGISTER = 0x0001; +static constexpr uint16_t CMD_WRITE_SYS_PARAM = 0x0012; -static const uint8_t CMD_ABD_DATA_REPLY_SIZE = 0x04; -static const uint8_t CMD_ABD_DATA_REPLY_START = 0x0A; -static const uint8_t CMD_MAX_BYTES = 0x64; -static const uint8_t CMD_REG_DATA_REPLY_SIZE = 0x02; +static constexpr uint8_t CMD_ABD_DATA_REPLY_SIZE = 0x04; +static constexpr uint8_t CMD_ABD_DATA_REPLY_START = 0x0A; +static constexpr uint8_t CMD_MAX_BYTES = 0x64; +static constexpr uint8_t CMD_REG_DATA_REPLY_SIZE = 0x02; -static const uint8_t LD2420_ERROR_NONE = 0x00; -static const uint8_t LD2420_ERROR_TIMEOUT = 0x02; -static const uint8_t LD2420_ERROR_UNKNOWN = 0x01; +static constexpr uint8_t LD2420_ERROR_NONE = 0x00; +static constexpr uint8_t LD2420_ERROR_TIMEOUT = 0x02; +static constexpr uint8_t LD2420_ERROR_UNKNOWN = 0x01; // Register address values -static const uint16_t CMD_MIN_GATE_REG = 0x0000; -static const uint16_t CMD_MAX_GATE_REG = 0x0001; -static const uint16_t CMD_TIMEOUT_REG = 0x0004; -static const uint16_t CMD_GATE_MOVE_THRESH[TOTAL_GATES] = {0x0010, 0x0011, 0x0012, 0x0013, 0x0014, 0x0015, - 0x0016, 0x0017, 0x0018, 0x0019, 0x001A, 0x001B, - 0x001C, 0x001D, 0x001E, 0x001F}; -static const uint16_t CMD_GATE_STILL_THRESH[TOTAL_GATES] = {0x0020, 0x0021, 0x0022, 0x0023, 0x0024, 0x0025, - 0x0026, 0x0027, 0x0028, 0x0029, 0x002A, 0x002B, - 0x002C, 0x002D, 0x002E, 0x002F}; -static const uint32_t FACTORY_MOVE_THRESH[TOTAL_GATES] = {60000, 30000, 400, 250, 250, 250, 250, 250, - 250, 250, 250, 250, 250, 250, 250, 250}; -static const uint32_t FACTORY_STILL_THRESH[TOTAL_GATES] = {40000, 20000, 200, 200, 200, 200, 200, 150, - 150, 100, 100, 100, 100, 100, 100, 100}; -static const uint16_t FACTORY_TIMEOUT = 120; -static const uint16_t FACTORY_MIN_GATE = 1; -static const uint16_t FACTORY_MAX_GATE = 12; +static constexpr uint16_t CMD_MIN_GATE_REG = 0x0000; +static constexpr uint16_t CMD_MAX_GATE_REG = 0x0001; +static constexpr uint16_t CMD_TIMEOUT_REG = 0x0004; +static constexpr uint16_t CMD_GATE_MOVE_THRESH[TOTAL_GATES] = {0x0010, 0x0011, 0x0012, 0x0013, 0x0014, 0x0015, + 0x0016, 0x0017, 0x0018, 0x0019, 0x001A, 0x001B, + 0x001C, 0x001D, 0x001E, 0x001F}; +static constexpr uint16_t CMD_GATE_STILL_THRESH[TOTAL_GATES] = {0x0020, 0x0021, 0x0022, 0x0023, 0x0024, 0x0025, + 0x0026, 0x0027, 0x0028, 0x0029, 0x002A, 0x002B, + 0x002C, 0x002D, 0x002E, 0x002F}; +static constexpr uint32_t FACTORY_MOVE_THRESH[TOTAL_GATES] = {60000, 30000, 400, 250, 250, 250, 250, 250, + 250, 250, 250, 250, 250, 250, 250, 250}; +static constexpr uint32_t FACTORY_STILL_THRESH[TOTAL_GATES] = {40000, 20000, 200, 200, 200, 200, 200, 150, + 150, 100, 100, 100, 100, 100, 100, 100}; +static constexpr uint16_t FACTORY_TIMEOUT = 120; +static constexpr uint16_t FACTORY_MIN_GATE = 1; +static constexpr uint16_t FACTORY_MAX_GATE = 12; // COMMAND_BYTE Header & Footer -static const uint32_t CMD_FRAME_FOOTER = 0x01020304; -static const uint32_t CMD_FRAME_HEADER = 0xFAFBFCFD; -static const uint32_t DEBUG_FRAME_FOOTER = 0xFAFBFCFD; -static const uint32_t DEBUG_FRAME_HEADER = 0x1410BFAA; -static const uint32_t ENERGY_FRAME_FOOTER = 0xF5F6F7F8; -static const uint32_t ENERGY_FRAME_HEADER = 0xF1F2F3F4; -static const int CALIBRATE_VERSION_MIN = 154; -static const uint8_t CMD_FRAME_COMMAND = 6; -static const uint8_t CMD_FRAME_DATA_LENGTH = 4; -static const uint8_t CMD_FRAME_STATUS = 7; -static const uint8_t CMD_ERROR_WORD = 8; -static const uint8_t ENERGY_SENSOR_START = 9; -static const uint8_t CALIBRATE_REPORT_INTERVAL = 4; +static constexpr uint32_t CMD_FRAME_FOOTER = 0x01020304; +static constexpr uint32_t CMD_FRAME_HEADER = 0xFAFBFCFD; +static constexpr uint32_t DEBUG_FRAME_FOOTER = 0xFAFBFCFD; +static constexpr uint32_t DEBUG_FRAME_HEADER = 0x1410BFAA; +static constexpr uint32_t ENERGY_FRAME_FOOTER = 0xF5F6F7F8; +static constexpr uint32_t ENERGY_FRAME_HEADER = 0xF1F2F3F4; +static constexpr int CALIBRATE_VERSION_MIN = 154; +static constexpr uint8_t CMD_FRAME_COMMAND = 6; +static constexpr uint8_t CMD_FRAME_DATA_LENGTH = 4; +static constexpr uint8_t CMD_FRAME_STATUS = 7; +static constexpr uint8_t CMD_ERROR_WORD = 8; +static constexpr uint8_t ENERGY_SENSOR_START = 9; +static constexpr uint8_t CALIBRATE_REPORT_INTERVAL = 4; static const char *const OP_NORMAL_MODE_STRING = "Normal"; static const char *const OP_SIMPLE_MODE_STRING = "Simple"; diff --git a/esphome/components/ld2420/ld2420.h b/esphome/components/ld2420/ld2420.h index 6d81f86497..02250c5911 100644 --- a/esphome/components/ld2420/ld2420.h +++ b/esphome/components/ld2420/ld2420.h @@ -20,9 +20,9 @@ namespace esphome::ld2420 { -static const uint8_t CALIBRATE_SAMPLES = 64; -static const uint8_t MAX_LINE_LENGTH = 46; // Max characters for serial buffer -static const uint8_t TOTAL_GATES = 16; +static constexpr uint8_t CALIBRATE_SAMPLES = 64; +static constexpr uint8_t MAX_LINE_LENGTH = 46; // Max characters for serial buffer +static constexpr uint8_t TOTAL_GATES = 16; enum OpMode : uint8_t { OP_NORMAL_MODE = 1, From 9a8b00a42835b20db98081f1801c5cbafafc723a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 21:33:23 -0600 Subject: [PATCH 084/197] [nfc] Use constexpr for compile-time constants (#14077) --- esphome/components/nfc/nci_core.h | 228 +++++++++++------------ esphome/components/nfc/ndef_message.h | 2 +- esphome/components/nfc/ndef_record.h | 16 +- esphome/components/nfc/ndef_record_uri.h | 2 +- esphome/components/nfc/nfc.h | 60 +++--- 5 files changed, 154 insertions(+), 154 deletions(-) diff --git a/esphome/components/nfc/nci_core.h b/esphome/components/nfc/nci_core.h index fdaf6d0cc5..6b42070ed0 100644 --- a/esphome/components/nfc/nci_core.h +++ b/esphome/components/nfc/nci_core.h @@ -8,137 +8,137 @@ namespace esphome { namespace nfc { // Header info -static const uint8_t NCI_PKT_HEADER_SIZE = 3; // NCI packet (pkt) headers are always three bytes -static const uint8_t NCI_PKT_MT_GID_OFFSET = 0; // NCI packet (pkt) MT and GID offsets -static const uint8_t NCI_PKT_OID_OFFSET = 1; // NCI packet (pkt) OID offset -static const uint8_t NCI_PKT_LENGTH_OFFSET = 2; // NCI packet (pkt) message length (size) offset -static const uint8_t NCI_PKT_PAYLOAD_OFFSET = 3; // NCI packet (pkt) payload offset +static constexpr uint8_t NCI_PKT_HEADER_SIZE = 3; // NCI packet (pkt) headers are always three bytes +static constexpr uint8_t NCI_PKT_MT_GID_OFFSET = 0; // NCI packet (pkt) MT and GID offsets +static constexpr uint8_t NCI_PKT_OID_OFFSET = 1; // NCI packet (pkt) OID offset +static constexpr uint8_t NCI_PKT_LENGTH_OFFSET = 2; // NCI packet (pkt) message length (size) offset +static constexpr uint8_t NCI_PKT_PAYLOAD_OFFSET = 3; // NCI packet (pkt) payload offset // Important masks -static const uint8_t NCI_PKT_MT_MASK = 0xE0; // NCI packet (pkt) message type mask -static const uint8_t NCI_PKT_PBF_MASK = 0x10; // packet boundary flag bit -static const uint8_t NCI_PKT_GID_MASK = 0x0F; -static const uint8_t NCI_PKT_OID_MASK = 0x3F; +static constexpr uint8_t NCI_PKT_MT_MASK = 0xE0; // NCI packet (pkt) message type mask +static constexpr uint8_t NCI_PKT_PBF_MASK = 0x10; // packet boundary flag bit +static constexpr uint8_t NCI_PKT_GID_MASK = 0x0F; +static constexpr uint8_t NCI_PKT_OID_MASK = 0x3F; // Message types -static const uint8_t NCI_PKT_MT_DATA = 0x00; // For sending commands to NFC endpoint (card/tag) -static const uint8_t NCI_PKT_MT_CTRL_COMMAND = 0x20; // For sending commands to NFCC -static const uint8_t NCI_PKT_MT_CTRL_RESPONSE = 0x40; // Response from NFCC to commands -static const uint8_t NCI_PKT_MT_CTRL_NOTIFICATION = 0x60; // Notification from NFCC +static constexpr uint8_t NCI_PKT_MT_DATA = 0x00; // For sending commands to NFC endpoint (card/tag) +static constexpr uint8_t NCI_PKT_MT_CTRL_COMMAND = 0x20; // For sending commands to NFCC +static constexpr uint8_t NCI_PKT_MT_CTRL_RESPONSE = 0x40; // Response from NFCC to commands +static constexpr uint8_t NCI_PKT_MT_CTRL_NOTIFICATION = 0x60; // Notification from NFCC // GIDs -static const uint8_t NCI_CORE_GID = 0x0; -static const uint8_t RF_GID = 0x1; -static const uint8_t NFCEE_GID = 0x1; -static const uint8_t NCI_PROPRIETARY_GID = 0xF; +static constexpr uint8_t NCI_CORE_GID = 0x0; +static constexpr uint8_t RF_GID = 0x1; +static constexpr uint8_t NFCEE_GID = 0x1; +static constexpr uint8_t NCI_PROPRIETARY_GID = 0xF; // OIDs -static const uint8_t NCI_CORE_RESET_OID = 0x00; -static const uint8_t NCI_CORE_INIT_OID = 0x01; -static const uint8_t NCI_CORE_SET_CONFIG_OID = 0x02; -static const uint8_t NCI_CORE_GET_CONFIG_OID = 0x03; -static const uint8_t NCI_CORE_CONN_CREATE_OID = 0x04; -static const uint8_t NCI_CORE_CONN_CLOSE_OID = 0x05; -static const uint8_t NCI_CORE_CONN_CREDITS_OID = 0x06; -static const uint8_t NCI_CORE_GENERIC_ERROR_OID = 0x07; -static const uint8_t NCI_CORE_INTERFACE_ERROR_OID = 0x08; +static constexpr uint8_t NCI_CORE_RESET_OID = 0x00; +static constexpr uint8_t NCI_CORE_INIT_OID = 0x01; +static constexpr uint8_t NCI_CORE_SET_CONFIG_OID = 0x02; +static constexpr uint8_t NCI_CORE_GET_CONFIG_OID = 0x03; +static constexpr uint8_t NCI_CORE_CONN_CREATE_OID = 0x04; +static constexpr uint8_t NCI_CORE_CONN_CLOSE_OID = 0x05; +static constexpr uint8_t NCI_CORE_CONN_CREDITS_OID = 0x06; +static constexpr uint8_t NCI_CORE_GENERIC_ERROR_OID = 0x07; +static constexpr uint8_t NCI_CORE_INTERFACE_ERROR_OID = 0x08; -static const uint8_t RF_DISCOVER_MAP_OID = 0x00; -static const uint8_t RF_SET_LISTEN_MODE_ROUTING_OID = 0x01; -static const uint8_t RF_GET_LISTEN_MODE_ROUTING_OID = 0x02; -static const uint8_t RF_DISCOVER_OID = 0x03; -static const uint8_t RF_DISCOVER_SELECT_OID = 0x04; -static const uint8_t RF_INTF_ACTIVATED_OID = 0x05; -static const uint8_t RF_DEACTIVATE_OID = 0x06; -static const uint8_t RF_FIELD_INFO_OID = 0x07; -static const uint8_t RF_T3T_POLLING_OID = 0x08; -static const uint8_t RF_NFCEE_ACTION_OID = 0x09; -static const uint8_t RF_NFCEE_DISCOVERY_REQ_OID = 0x0A; -static const uint8_t RF_PARAMETER_UPDATE_OID = 0x0B; +static constexpr uint8_t RF_DISCOVER_MAP_OID = 0x00; +static constexpr uint8_t RF_SET_LISTEN_MODE_ROUTING_OID = 0x01; +static constexpr uint8_t RF_GET_LISTEN_MODE_ROUTING_OID = 0x02; +static constexpr uint8_t RF_DISCOVER_OID = 0x03; +static constexpr uint8_t RF_DISCOVER_SELECT_OID = 0x04; +static constexpr uint8_t RF_INTF_ACTIVATED_OID = 0x05; +static constexpr uint8_t RF_DEACTIVATE_OID = 0x06; +static constexpr uint8_t RF_FIELD_INFO_OID = 0x07; +static constexpr uint8_t RF_T3T_POLLING_OID = 0x08; +static constexpr uint8_t RF_NFCEE_ACTION_OID = 0x09; +static constexpr uint8_t RF_NFCEE_DISCOVERY_REQ_OID = 0x0A; +static constexpr uint8_t RF_PARAMETER_UPDATE_OID = 0x0B; -static const uint8_t NFCEE_DISCOVER_OID = 0x00; -static const uint8_t NFCEE_MODE_SET_OID = 0x01; +static constexpr uint8_t NFCEE_DISCOVER_OID = 0x00; +static constexpr uint8_t NFCEE_MODE_SET_OID = 0x01; // Interfaces -static const uint8_t INTF_NFCEE_DIRECT = 0x00; -static const uint8_t INTF_FRAME = 0x01; -static const uint8_t INTF_ISODEP = 0x02; -static const uint8_t INTF_NFCDEP = 0x03; -static const uint8_t INTF_TAGCMD = 0x80; // NXP proprietary +static constexpr uint8_t INTF_NFCEE_DIRECT = 0x00; +static constexpr uint8_t INTF_FRAME = 0x01; +static constexpr uint8_t INTF_ISODEP = 0x02; +static constexpr uint8_t INTF_NFCDEP = 0x03; +static constexpr uint8_t INTF_TAGCMD = 0x80; // NXP proprietary // Bit rates -static const uint8_t NFC_BIT_RATE_106 = 0x00; -static const uint8_t NFC_BIT_RATE_212 = 0x01; -static const uint8_t NFC_BIT_RATE_424 = 0x02; -static const uint8_t NFC_BIT_RATE_848 = 0x03; -static const uint8_t NFC_BIT_RATE_1695 = 0x04; -static const uint8_t NFC_BIT_RATE_3390 = 0x05; -static const uint8_t NFC_BIT_RATE_6780 = 0x06; +static constexpr uint8_t NFC_BIT_RATE_106 = 0x00; +static constexpr uint8_t NFC_BIT_RATE_212 = 0x01; +static constexpr uint8_t NFC_BIT_RATE_424 = 0x02; +static constexpr uint8_t NFC_BIT_RATE_848 = 0x03; +static constexpr uint8_t NFC_BIT_RATE_1695 = 0x04; +static constexpr uint8_t NFC_BIT_RATE_3390 = 0x05; +static constexpr uint8_t NFC_BIT_RATE_6780 = 0x06; // Protocols -static const uint8_t PROT_UNDETERMINED = 0x00; -static const uint8_t PROT_T1T = 0x01; -static const uint8_t PROT_T2T = 0x02; -static const uint8_t PROT_T3T = 0x03; -static const uint8_t PROT_ISODEP = 0x04; -static const uint8_t PROT_NFCDEP = 0x05; -static const uint8_t PROT_T5T = 0x06; -static const uint8_t PROT_MIFARE = 0x80; +static constexpr uint8_t PROT_UNDETERMINED = 0x00; +static constexpr uint8_t PROT_T1T = 0x01; +static constexpr uint8_t PROT_T2T = 0x02; +static constexpr uint8_t PROT_T3T = 0x03; +static constexpr uint8_t PROT_ISODEP = 0x04; +static constexpr uint8_t PROT_NFCDEP = 0x05; +static constexpr uint8_t PROT_T5T = 0x06; +static constexpr uint8_t PROT_MIFARE = 0x80; // RF Technologies -static const uint8_t NFC_RF_TECH_A = 0x00; -static const uint8_t NFC_RF_TECH_B = 0x01; -static const uint8_t NFC_RF_TECH_F = 0x02; -static const uint8_t NFC_RF_TECH_15693 = 0x03; +static constexpr uint8_t NFC_RF_TECH_A = 0x00; +static constexpr uint8_t NFC_RF_TECH_B = 0x01; +static constexpr uint8_t NFC_RF_TECH_F = 0x02; +static constexpr uint8_t NFC_RF_TECH_15693 = 0x03; // RF Technology & Modes -static const uint8_t MODE_MASK = 0xF0; -static const uint8_t MODE_LISTEN_MASK = 0x80; -static const uint8_t MODE_POLL = 0x00; +static constexpr uint8_t MODE_MASK = 0xF0; +static constexpr uint8_t MODE_LISTEN_MASK = 0x80; +static constexpr uint8_t MODE_POLL = 0x00; -static const uint8_t TECH_PASSIVE_NFCA = 0x00; -static const uint8_t TECH_PASSIVE_NFCB = 0x01; -static const uint8_t TECH_PASSIVE_NFCF = 0x02; -static const uint8_t TECH_ACTIVE_NFCA = 0x03; -static const uint8_t TECH_ACTIVE_NFCF = 0x05; -static const uint8_t TECH_PASSIVE_15693 = 0x06; +static constexpr uint8_t TECH_PASSIVE_NFCA = 0x00; +static constexpr uint8_t TECH_PASSIVE_NFCB = 0x01; +static constexpr uint8_t TECH_PASSIVE_NFCF = 0x02; +static constexpr uint8_t TECH_ACTIVE_NFCA = 0x03; +static constexpr uint8_t TECH_ACTIVE_NFCF = 0x05; +static constexpr uint8_t TECH_PASSIVE_15693 = 0x06; // Status codes -static const uint8_t STATUS_OK = 0x00; -static const uint8_t STATUS_REJECTED = 0x01; -static const uint8_t STATUS_RF_FRAME_CORRUPTED = 0x02; -static const uint8_t STATUS_FAILED = 0x03; -static const uint8_t STATUS_NOT_INITIALIZED = 0x04; -static const uint8_t STATUS_SYNTAX_ERROR = 0x05; -static const uint8_t STATUS_SEMANTIC_ERROR = 0x06; -static const uint8_t STATUS_INVALID_PARAM = 0x09; -static const uint8_t STATUS_MESSAGE_SIZE_EXCEEDED = 0x0A; -static const uint8_t DISCOVERY_ALREADY_STARTED = 0xA0; -static const uint8_t DISCOVERY_TARGET_ACTIVATION_FAILED = 0xA1; -static const uint8_t DISCOVERY_TEAR_DOWN = 0xA2; -static const uint8_t RF_TRANSMISSION_ERROR = 0xB0; -static const uint8_t RF_PROTOCOL_ERROR = 0xB1; -static const uint8_t RF_TIMEOUT_ERROR = 0xB2; -static const uint8_t NFCEE_INTERFACE_ACTIVATION_FAILED = 0xC0; -static const uint8_t NFCEE_TRANSMISSION_ERROR = 0xC1; -static const uint8_t NFCEE_PROTOCOL_ERROR = 0xC2; -static const uint8_t NFCEE_TIMEOUT_ERROR = 0xC3; +static constexpr uint8_t STATUS_OK = 0x00; +static constexpr uint8_t STATUS_REJECTED = 0x01; +static constexpr uint8_t STATUS_RF_FRAME_CORRUPTED = 0x02; +static constexpr uint8_t STATUS_FAILED = 0x03; +static constexpr uint8_t STATUS_NOT_INITIALIZED = 0x04; +static constexpr uint8_t STATUS_SYNTAX_ERROR = 0x05; +static constexpr uint8_t STATUS_SEMANTIC_ERROR = 0x06; +static constexpr uint8_t STATUS_INVALID_PARAM = 0x09; +static constexpr uint8_t STATUS_MESSAGE_SIZE_EXCEEDED = 0x0A; +static constexpr uint8_t DISCOVERY_ALREADY_STARTED = 0xA0; +static constexpr uint8_t DISCOVERY_TARGET_ACTIVATION_FAILED = 0xA1; +static constexpr uint8_t DISCOVERY_TEAR_DOWN = 0xA2; +static constexpr uint8_t RF_TRANSMISSION_ERROR = 0xB0; +static constexpr uint8_t RF_PROTOCOL_ERROR = 0xB1; +static constexpr uint8_t RF_TIMEOUT_ERROR = 0xB2; +static constexpr uint8_t NFCEE_INTERFACE_ACTIVATION_FAILED = 0xC0; +static constexpr uint8_t NFCEE_TRANSMISSION_ERROR = 0xC1; +static constexpr uint8_t NFCEE_PROTOCOL_ERROR = 0xC2; +static constexpr uint8_t NFCEE_TIMEOUT_ERROR = 0xC3; // Deactivation types/reasons -static const uint8_t DEACTIVATION_TYPE_IDLE = 0x00; -static const uint8_t DEACTIVATION_TYPE_SLEEP = 0x01; -static const uint8_t DEACTIVATION_TYPE_SLEEP_AF = 0x02; -static const uint8_t DEACTIVATION_TYPE_DISCOVERY = 0x03; +static constexpr uint8_t DEACTIVATION_TYPE_IDLE = 0x00; +static constexpr uint8_t DEACTIVATION_TYPE_SLEEP = 0x01; +static constexpr uint8_t DEACTIVATION_TYPE_SLEEP_AF = 0x02; +static constexpr uint8_t DEACTIVATION_TYPE_DISCOVERY = 0x03; // RF discover map modes -static const uint8_t RF_DISCOVER_MAP_MODE_POLL = 0x1; -static const uint8_t RF_DISCOVER_MAP_MODE_LISTEN = 0x2; +static constexpr uint8_t RF_DISCOVER_MAP_MODE_POLL = 0x1; +static constexpr uint8_t RF_DISCOVER_MAP_MODE_LISTEN = 0x2; // RF discover notification types -static const uint8_t RF_DISCOVER_NTF_NT_LAST = 0x00; -static const uint8_t RF_DISCOVER_NTF_NT_LAST_RL = 0x01; -static const uint8_t RF_DISCOVER_NTF_NT_MORE = 0x02; +static constexpr uint8_t RF_DISCOVER_NTF_NT_LAST = 0x00; +static constexpr uint8_t RF_DISCOVER_NTF_NT_LAST_RL = 0x01; +static constexpr uint8_t RF_DISCOVER_NTF_NT_MORE = 0x02; // Important message offsets -static const uint8_t RF_DISCOVER_NTF_DISCOVERY_ID = 0 + NCI_PKT_HEADER_SIZE; -static const uint8_t RF_DISCOVER_NTF_PROTOCOL = 1 + NCI_PKT_HEADER_SIZE; -static const uint8_t RF_DISCOVER_NTF_MODE_TECH = 2 + NCI_PKT_HEADER_SIZE; -static const uint8_t RF_DISCOVER_NTF_RF_TECH_LENGTH = 3 + NCI_PKT_HEADER_SIZE; -static const uint8_t RF_DISCOVER_NTF_RF_TECH_PARAMS = 4 + NCI_PKT_HEADER_SIZE; -static const uint8_t RF_INTF_ACTIVATED_NTF_DISCOVERY_ID = 0 + NCI_PKT_HEADER_SIZE; -static const uint8_t RF_INTF_ACTIVATED_NTF_INTERFACE = 1 + NCI_PKT_HEADER_SIZE; -static const uint8_t RF_INTF_ACTIVATED_NTF_PROTOCOL = 2 + NCI_PKT_HEADER_SIZE; -static const uint8_t RF_INTF_ACTIVATED_NTF_MODE_TECH = 3 + NCI_PKT_HEADER_SIZE; -static const uint8_t RF_INTF_ACTIVATED_NTF_MAX_SIZE = 4 + NCI_PKT_HEADER_SIZE; -static const uint8_t RF_INTF_ACTIVATED_NTF_INIT_CRED = 5 + NCI_PKT_HEADER_SIZE; -static const uint8_t RF_INTF_ACTIVATED_NTF_RF_TECH_LENGTH = 6 + NCI_PKT_HEADER_SIZE; -static const uint8_t RF_INTF_ACTIVATED_NTF_RF_TECH_PARAMS = 7 + NCI_PKT_HEADER_SIZE; +static constexpr uint8_t RF_DISCOVER_NTF_DISCOVERY_ID = 0 + NCI_PKT_HEADER_SIZE; +static constexpr uint8_t RF_DISCOVER_NTF_PROTOCOL = 1 + NCI_PKT_HEADER_SIZE; +static constexpr uint8_t RF_DISCOVER_NTF_MODE_TECH = 2 + NCI_PKT_HEADER_SIZE; +static constexpr uint8_t RF_DISCOVER_NTF_RF_TECH_LENGTH = 3 + NCI_PKT_HEADER_SIZE; +static constexpr uint8_t RF_DISCOVER_NTF_RF_TECH_PARAMS = 4 + NCI_PKT_HEADER_SIZE; +static constexpr uint8_t RF_INTF_ACTIVATED_NTF_DISCOVERY_ID = 0 + NCI_PKT_HEADER_SIZE; +static constexpr uint8_t RF_INTF_ACTIVATED_NTF_INTERFACE = 1 + NCI_PKT_HEADER_SIZE; +static constexpr uint8_t RF_INTF_ACTIVATED_NTF_PROTOCOL = 2 + NCI_PKT_HEADER_SIZE; +static constexpr uint8_t RF_INTF_ACTIVATED_NTF_MODE_TECH = 3 + NCI_PKT_HEADER_SIZE; +static constexpr uint8_t RF_INTF_ACTIVATED_NTF_MAX_SIZE = 4 + NCI_PKT_HEADER_SIZE; +static constexpr uint8_t RF_INTF_ACTIVATED_NTF_INIT_CRED = 5 + NCI_PKT_HEADER_SIZE; +static constexpr uint8_t RF_INTF_ACTIVATED_NTF_RF_TECH_LENGTH = 6 + NCI_PKT_HEADER_SIZE; +static constexpr uint8_t RF_INTF_ACTIVATED_NTF_RF_TECH_PARAMS = 7 + NCI_PKT_HEADER_SIZE; } // namespace nfc } // namespace esphome diff --git a/esphome/components/nfc/ndef_message.h b/esphome/components/nfc/ndef_message.h index 2e81fb216c..48f79b8854 100644 --- a/esphome/components/nfc/ndef_message.h +++ b/esphome/components/nfc/ndef_message.h @@ -12,7 +12,7 @@ namespace esphome { namespace nfc { -static const uint8_t MAX_NDEF_RECORDS = 4; +static constexpr uint8_t MAX_NDEF_RECORDS = 4; class NdefMessage { public: diff --git a/esphome/components/nfc/ndef_record.h b/esphome/components/nfc/ndef_record.h index 76d8b6a50a..1a7c24aee9 100644 --- a/esphome/components/nfc/ndef_record.h +++ b/esphome/components/nfc/ndef_record.h @@ -8,14 +8,14 @@ namespace esphome { namespace nfc { -static const uint8_t TNF_EMPTY = 0x00; -static const uint8_t TNF_WELL_KNOWN = 0x01; -static const uint8_t TNF_MIME_MEDIA = 0x02; -static const uint8_t TNF_ABSOLUTE_URI = 0x03; -static const uint8_t TNF_EXTERNAL_TYPE = 0x04; -static const uint8_t TNF_UNKNOWN = 0x05; -static const uint8_t TNF_UNCHANGED = 0x06; -static const uint8_t TNF_RESERVED = 0x07; +static constexpr uint8_t TNF_EMPTY = 0x00; +static constexpr uint8_t TNF_WELL_KNOWN = 0x01; +static constexpr uint8_t TNF_MIME_MEDIA = 0x02; +static constexpr uint8_t TNF_ABSOLUTE_URI = 0x03; +static constexpr uint8_t TNF_EXTERNAL_TYPE = 0x04; +static constexpr uint8_t TNF_UNKNOWN = 0x05; +static constexpr uint8_t TNF_UNCHANGED = 0x06; +static constexpr uint8_t TNF_RESERVED = 0x07; class NdefRecord { public: diff --git a/esphome/components/nfc/ndef_record_uri.h b/esphome/components/nfc/ndef_record_uri.h index 1eadda1b4f..2f7790a9a9 100644 --- a/esphome/components/nfc/ndef_record_uri.h +++ b/esphome/components/nfc/ndef_record_uri.h @@ -9,7 +9,7 @@ namespace esphome { namespace nfc { -static const uint8_t PAYLOAD_IDENTIFIERS_COUNT = 0x23; +static constexpr uint8_t PAYLOAD_IDENTIFIERS_COUNT = 0x23; static const char *const PAYLOAD_IDENTIFIERS[] = {"", "http://www.", "https://www.", diff --git a/esphome/components/nfc/nfc.h b/esphome/components/nfc/nfc.h index 5191904833..cdaea82af6 100644 --- a/esphome/components/nfc/nfc.h +++ b/esphome/components/nfc/nfc.h @@ -12,47 +12,47 @@ namespace esphome { namespace nfc { -static const uint8_t MIFARE_CLASSIC_BLOCK_SIZE = 16; -static const uint8_t MIFARE_CLASSIC_LONG_TLV_SIZE = 4; -static const uint8_t MIFARE_CLASSIC_SHORT_TLV_SIZE = 2; -static const uint8_t MIFARE_CLASSIC_BLOCKS_PER_SECT_LOW = 4; -static const uint8_t MIFARE_CLASSIC_BLOCKS_PER_SECT_HIGH = 16; -static const uint8_t MIFARE_CLASSIC_16BLOCK_SECT_START = 32; +static constexpr uint8_t MIFARE_CLASSIC_BLOCK_SIZE = 16; +static constexpr uint8_t MIFARE_CLASSIC_LONG_TLV_SIZE = 4; +static constexpr uint8_t MIFARE_CLASSIC_SHORT_TLV_SIZE = 2; +static constexpr uint8_t MIFARE_CLASSIC_BLOCKS_PER_SECT_LOW = 4; +static constexpr uint8_t MIFARE_CLASSIC_BLOCKS_PER_SECT_HIGH = 16; +static constexpr uint8_t MIFARE_CLASSIC_16BLOCK_SECT_START = 32; -static const uint8_t MIFARE_ULTRALIGHT_PAGE_SIZE = 4; -static const uint8_t MIFARE_ULTRALIGHT_READ_SIZE = 4; -static const uint8_t MIFARE_ULTRALIGHT_DATA_START_PAGE = 4; -static const uint8_t MIFARE_ULTRALIGHT_MAX_PAGE = 63; +static constexpr uint8_t MIFARE_ULTRALIGHT_PAGE_SIZE = 4; +static constexpr uint8_t MIFARE_ULTRALIGHT_READ_SIZE = 4; +static constexpr uint8_t MIFARE_ULTRALIGHT_DATA_START_PAGE = 4; +static constexpr uint8_t MIFARE_ULTRALIGHT_MAX_PAGE = 63; -static const uint8_t TAG_TYPE_MIFARE_CLASSIC = 0; -static const uint8_t TAG_TYPE_1 = 1; -static const uint8_t TAG_TYPE_2 = 2; -static const uint8_t TAG_TYPE_3 = 3; -static const uint8_t TAG_TYPE_4 = 4; -static const uint8_t TAG_TYPE_UNKNOWN = 99; +static constexpr uint8_t TAG_TYPE_MIFARE_CLASSIC = 0; +static constexpr uint8_t TAG_TYPE_1 = 1; +static constexpr uint8_t TAG_TYPE_2 = 2; +static constexpr uint8_t TAG_TYPE_3 = 3; +static constexpr uint8_t TAG_TYPE_4 = 4; +static constexpr uint8_t TAG_TYPE_UNKNOWN = 99; // Mifare Commands -static const uint8_t MIFARE_CMD_AUTH_A = 0x60; -static const uint8_t MIFARE_CMD_AUTH_B = 0x61; -static const uint8_t MIFARE_CMD_HALT = 0x50; -static const uint8_t MIFARE_CMD_READ = 0x30; -static const uint8_t MIFARE_CMD_WRITE = 0xA0; -static const uint8_t MIFARE_CMD_WRITE_ULTRALIGHT = 0xA2; +static constexpr uint8_t MIFARE_CMD_AUTH_A = 0x60; +static constexpr uint8_t MIFARE_CMD_AUTH_B = 0x61; +static constexpr uint8_t MIFARE_CMD_HALT = 0x50; +static constexpr uint8_t MIFARE_CMD_READ = 0x30; +static constexpr uint8_t MIFARE_CMD_WRITE = 0xA0; +static constexpr uint8_t MIFARE_CMD_WRITE_ULTRALIGHT = 0xA2; // Mifare Ack/Nak -static const uint8_t MIFARE_CMD_ACK = 0x0A; -static const uint8_t MIFARE_CMD_NAK_INVALID_XFER_BUFF_VALID = 0x00; -static const uint8_t MIFARE_CMD_NAK_CRC_ERROR_XFER_BUFF_VALID = 0x01; -static const uint8_t MIFARE_CMD_NAK_INVALID_XFER_BUFF_INVALID = 0x04; -static const uint8_t MIFARE_CMD_NAK_CRC_ERROR_XFER_BUFF_INVALID = 0x05; +static constexpr uint8_t MIFARE_CMD_ACK = 0x0A; +static constexpr uint8_t MIFARE_CMD_NAK_INVALID_XFER_BUFF_VALID = 0x00; +static constexpr uint8_t MIFARE_CMD_NAK_CRC_ERROR_XFER_BUFF_VALID = 0x01; +static constexpr uint8_t MIFARE_CMD_NAK_INVALID_XFER_BUFF_INVALID = 0x04; +static constexpr uint8_t MIFARE_CMD_NAK_CRC_ERROR_XFER_BUFF_INVALID = 0x05; static const char *const MIFARE_CLASSIC = "Mifare Classic"; static const char *const NFC_FORUM_TYPE_2 = "NFC Forum Type 2"; static const char *const ERROR = "Error"; -static const uint8_t DEFAULT_KEY[6] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; -static const uint8_t NDEF_KEY[6] = {0xD3, 0xF7, 0xD3, 0xF7, 0xD3, 0xF7}; -static const uint8_t MAD_KEY[6] = {0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5}; +static constexpr uint8_t DEFAULT_KEY[6] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; +static constexpr uint8_t NDEF_KEY[6] = {0xD3, 0xF7, 0xD3, 0xF7, 0xD3, 0xF7}; +static constexpr uint8_t MAD_KEY[6] = {0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5}; /// Max UID size is 10 bytes, formatted as "XX-XX-XX-XX-XX-XX-XX-XX-XX-XX\0" = 30 chars static constexpr size_t FORMAT_UID_BUFFER_SIZE = 30; From 2f9b76f129c2b278b0c45ba5c66430d638631ee2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 21:33:39 -0600 Subject: [PATCH 085/197] [pn7160] Use constexpr for compile-time constants (#14078) --- esphome/components/pn7150/pn7150.h | 111 +++++++++---------- esphome/components/pn7160/pn7160.h | 121 +++++++++++---------- esphome/components/pn7160_spi/pn7160_spi.h | 4 +- 3 files changed, 119 insertions(+), 117 deletions(-) diff --git a/esphome/components/pn7150/pn7150.h b/esphome/components/pn7150/pn7150.h index 5feba17d21..c5dd283832 100644 --- a/esphome/components/pn7150/pn7150.h +++ b/esphome/components/pn7150/pn7150.h @@ -14,48 +14,48 @@ namespace esphome { namespace pn7150 { -static const uint16_t NFCC_DEFAULT_TIMEOUT = 10; -static const uint16_t NFCC_INIT_TIMEOUT = 50; -static const uint16_t NFCC_TAG_WRITE_TIMEOUT = 15; +static constexpr uint16_t NFCC_DEFAULT_TIMEOUT = 10; +static constexpr uint16_t NFCC_INIT_TIMEOUT = 50; +static constexpr uint16_t NFCC_TAG_WRITE_TIMEOUT = 15; -static const uint8_t NFCC_MAX_COMM_FAILS = 3; -static const uint8_t NFCC_MAX_ERROR_COUNT = 10; +static constexpr uint8_t NFCC_MAX_COMM_FAILS = 3; +static constexpr uint8_t NFCC_MAX_ERROR_COUNT = 10; -static const uint8_t XCHG_DATA_OID = 0x10; -static const uint8_t MF_SECTORSEL_OID = 0x32; -static const uint8_t MFC_AUTHENTICATE_OID = 0x40; -static const uint8_t TEST_PRBS_OID = 0x30; -static const uint8_t TEST_ANTENNA_OID = 0x3D; -static const uint8_t TEST_GET_REGISTER_OID = 0x33; +static constexpr uint8_t XCHG_DATA_OID = 0x10; +static constexpr uint8_t MF_SECTORSEL_OID = 0x32; +static constexpr uint8_t MFC_AUTHENTICATE_OID = 0x40; +static constexpr uint8_t TEST_PRBS_OID = 0x30; +static constexpr uint8_t TEST_ANTENNA_OID = 0x3D; +static constexpr uint8_t TEST_GET_REGISTER_OID = 0x33; -static const uint8_t MFC_AUTHENTICATE_PARAM_KS_A = 0x00; // key select A -static const uint8_t MFC_AUTHENTICATE_PARAM_KS_B = 0x80; // key select B -static const uint8_t MFC_AUTHENTICATE_PARAM_EMBED_KEY = 0x10; +static constexpr uint8_t MFC_AUTHENTICATE_PARAM_KS_A = 0x00; // key select A +static constexpr uint8_t MFC_AUTHENTICATE_PARAM_KS_B = 0x80; // key select B +static constexpr uint8_t MFC_AUTHENTICATE_PARAM_EMBED_KEY = 0x10; -static const uint8_t CARD_EMU_T4T_APP_SELECT[] = {0x00, 0xA4, 0x04, 0x00, 0x07, 0xD2, 0x76, - 0x00, 0x00, 0x85, 0x01, 0x01, 0x00}; -static const uint8_t CARD_EMU_T4T_CC[] = {0x00, 0x0F, 0x20, 0x00, 0xFF, 0x00, 0xFF, 0x04, - 0x06, 0xE1, 0x04, 0x00, 0xFF, 0x00, 0x00}; -static const uint8_t CARD_EMU_T4T_CC_SELECT[] = {0x00, 0xA4, 0x00, 0x0C, 0x02, 0xE1, 0x03}; -static const uint8_t CARD_EMU_T4T_NDEF_SELECT[] = {0x00, 0xA4, 0x00, 0x0C, 0x02, 0xE1, 0x04}; -static const uint8_t CARD_EMU_T4T_READ[] = {0x00, 0xB0}; -static const uint8_t CARD_EMU_T4T_WRITE[] = {0x00, 0xD6}; -static const uint8_t CARD_EMU_T4T_OK[] = {0x90, 0x00}; -static const uint8_t CARD_EMU_T4T_NOK[] = {0x6A, 0x82}; +static constexpr uint8_t CARD_EMU_T4T_APP_SELECT[] = {0x00, 0xA4, 0x04, 0x00, 0x07, 0xD2, 0x76, + 0x00, 0x00, 0x85, 0x01, 0x01, 0x00}; +static constexpr uint8_t CARD_EMU_T4T_CC[] = {0x00, 0x0F, 0x20, 0x00, 0xFF, 0x00, 0xFF, 0x04, + 0x06, 0xE1, 0x04, 0x00, 0xFF, 0x00, 0x00}; +static constexpr uint8_t CARD_EMU_T4T_CC_SELECT[] = {0x00, 0xA4, 0x00, 0x0C, 0x02, 0xE1, 0x03}; +static constexpr uint8_t CARD_EMU_T4T_NDEF_SELECT[] = {0x00, 0xA4, 0x00, 0x0C, 0x02, 0xE1, 0x04}; +static constexpr uint8_t CARD_EMU_T4T_READ[] = {0x00, 0xB0}; +static constexpr uint8_t CARD_EMU_T4T_WRITE[] = {0x00, 0xD6}; +static constexpr uint8_t CARD_EMU_T4T_OK[] = {0x90, 0x00}; +static constexpr uint8_t CARD_EMU_T4T_NOK[] = {0x6A, 0x82}; -static const uint8_t CORE_CONFIG_SOLO[] = {0x01, // Number of parameter fields - 0x00, // config param identifier (TOTAL_DURATION) - 0x02, // length of value - 0x01, // TOTAL_DURATION (low)... - 0x00}; // TOTAL_DURATION (high): 1 ms +static constexpr uint8_t CORE_CONFIG_SOLO[] = {0x01, // Number of parameter fields + 0x00, // config param identifier (TOTAL_DURATION) + 0x02, // length of value + 0x01, // TOTAL_DURATION (low)... + 0x00}; // TOTAL_DURATION (high): 1 ms -static const uint8_t CORE_CONFIG_RW_CE[] = {0x01, // Number of parameter fields - 0x00, // config param identifier (TOTAL_DURATION) - 0x02, // length of value - 0xF8, // TOTAL_DURATION (low)... - 0x02}; // TOTAL_DURATION (high): 760 ms +static constexpr uint8_t CORE_CONFIG_RW_CE[] = {0x01, // Number of parameter fields + 0x00, // config param identifier (TOTAL_DURATION) + 0x02, // length of value + 0xF8, // TOTAL_DURATION (low)... + 0x02}; // TOTAL_DURATION (high): 760 ms -static const uint8_t PMU_CFG[] = { +static constexpr uint8_t PMU_CFG[] = { 0x01, // Number of parameters 0xA0, 0x0E, // ext. tag 3, // length @@ -64,7 +64,7 @@ static const uint8_t PMU_CFG[] = { 0x01, // RFU; must be 0x00 for CFG1 and 0x01 for CFG2 }; -static const uint8_t RF_DISCOVER_MAP_CONFIG[] = { // poll modes +static constexpr uint8_t RF_DISCOVER_MAP_CONFIG[] = { // poll modes nfc::PROT_T1T, nfc::RF_DISCOVER_MAP_MODE_POLL, nfc::INTF_FRAME, // poll mode nfc::PROT_T2T, nfc::RF_DISCOVER_MAP_MODE_POLL, @@ -76,28 +76,29 @@ static const uint8_t RF_DISCOVER_MAP_CONFIG[] = { // poll modes nfc::PROT_MIFARE, nfc::RF_DISCOVER_MAP_MODE_POLL, nfc::INTF_TAGCMD}; // poll mode -static const uint8_t RF_DISCOVERY_LISTEN_CONFIG[] = {nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCA, // listen mode - nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCB, // listen mode - nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCF}; // listen mode +static constexpr uint8_t RF_DISCOVERY_LISTEN_CONFIG[] = { + nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCA, // listen mode + nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCB, // listen mode + nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCF}; // listen mode -static const uint8_t RF_DISCOVERY_POLL_CONFIG[] = {nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCA, // poll mode - nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCB, // poll mode - nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCF}; // poll mode +static constexpr uint8_t RF_DISCOVERY_POLL_CONFIG[] = {nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCA, // poll mode + nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCB, // poll mode + nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCF}; // poll mode -static const uint8_t RF_DISCOVERY_CONFIG[] = {nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCA, // poll mode - nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCB, // poll mode - nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCF, // poll mode - nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCA, // listen mode - nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCB, // listen mode - nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCF}; // listen mode +static constexpr uint8_t RF_DISCOVERY_CONFIG[] = {nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCA, // poll mode + nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCB, // poll mode + nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCF, // poll mode + nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCA, // listen mode + nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCB, // listen mode + nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCF}; // listen mode -static const uint8_t RF_LISTEN_MODE_ROUTING_CONFIG[] = {0x00, // "more" (another message is coming) - 1, // number of table entries - 0x01, // type = protocol-based - 3, // length - 0, // DH NFCEE ID, a static ID representing the DH-NFCEE - 0x01, // power state - nfc::PROT_ISODEP}; // protocol +static constexpr uint8_t RF_LISTEN_MODE_ROUTING_CONFIG[] = {0x00, // "more" (another message is coming) + 1, // number of table entries + 0x01, // type = protocol-based + 3, // length + 0, // DH NFCEE ID, a static ID representing the DH-NFCEE + 0x01, // power state + nfc::PROT_ISODEP}; // protocol enum class CardEmulationState : uint8_t { CARD_EMU_IDLE, diff --git a/esphome/components/pn7160/pn7160.h b/esphome/components/pn7160/pn7160.h index 9f2d10c2d5..77ab49399c 100644 --- a/esphome/components/pn7160/pn7160.h +++ b/esphome/components/pn7160/pn7160.h @@ -14,48 +14,48 @@ namespace esphome { namespace pn7160 { -static const uint16_t NFCC_DEFAULT_TIMEOUT = 10; -static const uint16_t NFCC_INIT_TIMEOUT = 50; -static const uint16_t NFCC_TAG_WRITE_TIMEOUT = 15; +static constexpr uint16_t NFCC_DEFAULT_TIMEOUT = 10; +static constexpr uint16_t NFCC_INIT_TIMEOUT = 50; +static constexpr uint16_t NFCC_TAG_WRITE_TIMEOUT = 15; -static const uint8_t NFCC_MAX_COMM_FAILS = 3; -static const uint8_t NFCC_MAX_ERROR_COUNT = 10; +static constexpr uint8_t NFCC_MAX_COMM_FAILS = 3; +static constexpr uint8_t NFCC_MAX_ERROR_COUNT = 10; -static const uint8_t XCHG_DATA_OID = 0x10; -static const uint8_t MF_SECTORSEL_OID = 0x32; -static const uint8_t MFC_AUTHENTICATE_OID = 0x40; -static const uint8_t TEST_PRBS_OID = 0x30; -static const uint8_t TEST_ANTENNA_OID = 0x3D; -static const uint8_t TEST_GET_REGISTER_OID = 0x33; +static constexpr uint8_t XCHG_DATA_OID = 0x10; +static constexpr uint8_t MF_SECTORSEL_OID = 0x32; +static constexpr uint8_t MFC_AUTHENTICATE_OID = 0x40; +static constexpr uint8_t TEST_PRBS_OID = 0x30; +static constexpr uint8_t TEST_ANTENNA_OID = 0x3D; +static constexpr uint8_t TEST_GET_REGISTER_OID = 0x33; -static const uint8_t MFC_AUTHENTICATE_PARAM_KS_A = 0x00; // key select A -static const uint8_t MFC_AUTHENTICATE_PARAM_KS_B = 0x80; // key select B -static const uint8_t MFC_AUTHENTICATE_PARAM_EMBED_KEY = 0x10; +static constexpr uint8_t MFC_AUTHENTICATE_PARAM_KS_A = 0x00; // key select A +static constexpr uint8_t MFC_AUTHENTICATE_PARAM_KS_B = 0x80; // key select B +static constexpr uint8_t MFC_AUTHENTICATE_PARAM_EMBED_KEY = 0x10; -static const uint8_t CARD_EMU_T4T_APP_SELECT[] = {0x00, 0xA4, 0x04, 0x00, 0x07, 0xD2, 0x76, - 0x00, 0x00, 0x85, 0x01, 0x01, 0x00}; -static const uint8_t CARD_EMU_T4T_CC[] = {0x00, 0x0F, 0x20, 0x00, 0xFF, 0x00, 0xFF, 0x04, - 0x06, 0xE1, 0x04, 0x00, 0xFF, 0x00, 0x00}; -static const uint8_t CARD_EMU_T4T_CC_SELECT[] = {0x00, 0xA4, 0x00, 0x0C, 0x02, 0xE1, 0x03}; -static const uint8_t CARD_EMU_T4T_NDEF_SELECT[] = {0x00, 0xA4, 0x00, 0x0C, 0x02, 0xE1, 0x04}; -static const uint8_t CARD_EMU_T4T_READ[] = {0x00, 0xB0}; -static const uint8_t CARD_EMU_T4T_WRITE[] = {0x00, 0xD6}; -static const uint8_t CARD_EMU_T4T_OK[] = {0x90, 0x00}; -static const uint8_t CARD_EMU_T4T_NOK[] = {0x6A, 0x82}; +static constexpr uint8_t CARD_EMU_T4T_APP_SELECT[] = {0x00, 0xA4, 0x04, 0x00, 0x07, 0xD2, 0x76, + 0x00, 0x00, 0x85, 0x01, 0x01, 0x00}; +static constexpr uint8_t CARD_EMU_T4T_CC[] = {0x00, 0x0F, 0x20, 0x00, 0xFF, 0x00, 0xFF, 0x04, + 0x06, 0xE1, 0x04, 0x00, 0xFF, 0x00, 0x00}; +static constexpr uint8_t CARD_EMU_T4T_CC_SELECT[] = {0x00, 0xA4, 0x00, 0x0C, 0x02, 0xE1, 0x03}; +static constexpr uint8_t CARD_EMU_T4T_NDEF_SELECT[] = {0x00, 0xA4, 0x00, 0x0C, 0x02, 0xE1, 0x04}; +static constexpr uint8_t CARD_EMU_T4T_READ[] = {0x00, 0xB0}; +static constexpr uint8_t CARD_EMU_T4T_WRITE[] = {0x00, 0xD6}; +static constexpr uint8_t CARD_EMU_T4T_OK[] = {0x90, 0x00}; +static constexpr uint8_t CARD_EMU_T4T_NOK[] = {0x6A, 0x82}; -static const uint8_t CORE_CONFIG_SOLO[] = {0x01, // Number of parameter fields - 0x00, // config param identifier (TOTAL_DURATION) - 0x02, // length of value - 0x01, // TOTAL_DURATION (low)... - 0x00}; // TOTAL_DURATION (high): 1 ms +static constexpr uint8_t CORE_CONFIG_SOLO[] = {0x01, // Number of parameter fields + 0x00, // config param identifier (TOTAL_DURATION) + 0x02, // length of value + 0x01, // TOTAL_DURATION (low)... + 0x00}; // TOTAL_DURATION (high): 1 ms -static const uint8_t CORE_CONFIG_RW_CE[] = {0x01, // Number of parameter fields - 0x00, // config param identifier (TOTAL_DURATION) - 0x02, // length of value - 0xF8, // TOTAL_DURATION (low)... - 0x02}; // TOTAL_DURATION (high): 760 ms +static constexpr uint8_t CORE_CONFIG_RW_CE[] = {0x01, // Number of parameter fields + 0x00, // config param identifier (TOTAL_DURATION) + 0x02, // length of value + 0xF8, // TOTAL_DURATION (low)... + 0x02}; // TOTAL_DURATION (high): 760 ms -static const uint8_t PMU_CFG[] = { +static constexpr uint8_t PMU_CFG[] = { 0x01, // Number of parameters 0xA0, 0x0E, // ext. tag 11, // length @@ -74,7 +74,7 @@ static const uint8_t PMU_CFG[] = { 0x0C, // RFU }; -static const uint8_t RF_DISCOVER_MAP_CONFIG[] = { // poll modes +static constexpr uint8_t RF_DISCOVER_MAP_CONFIG[] = { // poll modes nfc::PROT_T1T, nfc::RF_DISCOVER_MAP_MODE_POLL, nfc::INTF_FRAME, // poll mode nfc::PROT_T2T, nfc::RF_DISCOVER_MAP_MODE_POLL, @@ -86,33 +86,34 @@ static const uint8_t RF_DISCOVER_MAP_CONFIG[] = { // poll modes nfc::PROT_MIFARE, nfc::RF_DISCOVER_MAP_MODE_POLL, nfc::INTF_TAGCMD}; // poll mode -static const uint8_t RF_DISCOVERY_LISTEN_CONFIG[] = {nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCA, // listen mode - nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCB, // listen mode - nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCF}; // listen mode +static constexpr uint8_t RF_DISCOVERY_LISTEN_CONFIG[] = { + nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCA, // listen mode + nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCB, // listen mode + nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCF}; // listen mode -static const uint8_t RF_DISCOVERY_POLL_CONFIG[] = {nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCA, // poll mode - nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCB, // poll mode - nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCF}; // poll mode +static constexpr uint8_t RF_DISCOVERY_POLL_CONFIG[] = {nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCA, // poll mode + nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCB, // poll mode + nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCF}; // poll mode -static const uint8_t RF_DISCOVERY_CONFIG[] = {nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCA, // poll mode - nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCB, // poll mode - nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCF, // poll mode - nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCA, // listen mode - nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCB, // listen mode - nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCF}; // listen mode +static constexpr uint8_t RF_DISCOVERY_CONFIG[] = {nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCA, // poll mode + nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCB, // poll mode + nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCF, // poll mode + nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCA, // listen mode + nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCB, // listen mode + nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCF}; // listen mode -static const uint8_t RF_LISTEN_MODE_ROUTING_CONFIG[] = {0x00, // "more" (another message is coming) - 2, // number of table entries - 0x01, // type = protocol-based - 3, // length - 0, // DH NFCEE ID, a static ID representing the DH-NFCEE - 0x07, // power state - nfc::PROT_ISODEP, // protocol - 0x00, // type = technology-based - 3, // length - 0, // DH NFCEE ID, a static ID representing the DH-NFCEE - 0x07, // power state - nfc::TECH_PASSIVE_NFCA}; // technology +static constexpr uint8_t RF_LISTEN_MODE_ROUTING_CONFIG[] = {0x00, // "more" (another message is coming) + 2, // number of table entries + 0x01, // type = protocol-based + 3, // length + 0, // DH NFCEE ID, a static ID representing the DH-NFCEE + 0x07, // power state + nfc::PROT_ISODEP, // protocol + 0x00, // type = technology-based + 3, // length + 0, // DH NFCEE ID, a static ID representing the DH-NFCEE + 0x07, // power state + nfc::TECH_PASSIVE_NFCA}; // technology enum class CardEmulationState : uint8_t { CARD_EMU_IDLE, diff --git a/esphome/components/pn7160_spi/pn7160_spi.h b/esphome/components/pn7160_spi/pn7160_spi.h index 7d4460a76d..9b6e21fa2a 100644 --- a/esphome/components/pn7160_spi/pn7160_spi.h +++ b/esphome/components/pn7160_spi/pn7160_spi.h @@ -10,8 +10,8 @@ namespace esphome { namespace pn7160_spi { -static const uint8_t TDD_SPI_READ = 0xFF; -static const uint8_t TDD_SPI_WRITE = 0x0A; +static constexpr uint8_t TDD_SPI_READ = 0xFF; +static constexpr uint8_t TDD_SPI_WRITE = 0x0A; class PN7160Spi : public pn7160::PN7160, public spi::SPIDevice Date: Wed, 18 Feb 2026 21:34:25 -0600 Subject: [PATCH 086/197] [bluetooth_proxy] Use constexpr for remaining compile-time constants (#14080) --- esphome/components/bluetooth_proxy/bluetooth_proxy.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.h b/esphome/components/bluetooth_proxy/bluetooth_proxy.h index 62a035e79f..85461755aa 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.h +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.h @@ -23,9 +23,9 @@ namespace esphome::bluetooth_proxy { -static const esp_err_t ESP_GATT_NOT_CONNECTED = -1; -static const int DONE_SENDING_SERVICES = -2; -static const int INIT_SENDING_SERVICES = -3; +static constexpr esp_err_t ESP_GATT_NOT_CONNECTED = -1; +static constexpr int DONE_SENDING_SERVICES = -2; +static constexpr int INIT_SENDING_SERVICES = -3; using namespace esp32_ble_client; From 3c227eeca46d5ca6e99e5e59a9585d0f168642fb Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Wed, 18 Feb 2026 21:50:39 -0600 Subject: [PATCH 087/197] [audio] Add support for sinking via an arbitrary callback (#14035) --- esphome/components/audio/audio_transfer_buffer.cpp | 2 ++ esphome/components/audio/audio_transfer_buffer.h | 11 +++++++++++ 2 files changed, 13 insertions(+) diff --git a/esphome/components/audio/audio_transfer_buffer.cpp b/esphome/components/audio/audio_transfer_buffer.cpp index ddb669e0eb..a8be55d62f 100644 --- a/esphome/components/audio/audio_transfer_buffer.cpp +++ b/esphome/components/audio/audio_transfer_buffer.cpp @@ -165,6 +165,8 @@ size_t AudioSinkTransferBuffer::transfer_data_to_sink(TickType_t ticks_to_wait, if (this->ring_buffer_.use_count() > 0) { bytes_written = this->ring_buffer_->write_without_replacement((void *) this->data_start_, this->available(), ticks_to_wait); + } else if (this->sink_callback_ != nullptr) { + bytes_written = this->sink_callback_->audio_sink_write(this->data_start_, this->available(), ticks_to_wait); } this->decrease_buffer_length(bytes_written); diff --git a/esphome/components/audio/audio_transfer_buffer.h b/esphome/components/audio/audio_transfer_buffer.h index 24c0670d1a..22c22cc9ae 100644 --- a/esphome/components/audio/audio_transfer_buffer.h +++ b/esphome/components/audio/audio_transfer_buffer.h @@ -15,6 +15,12 @@ namespace esphome { namespace audio { +/// @brief Abstract interface for writing decoded audio data to a sink. +class AudioSinkCallback { + public: + virtual size_t audio_sink_write(uint8_t *data, size_t length, TickType_t ticks_to_wait) = 0; +}; + class AudioTransferBuffer { /* * @brief Class that facilitates tranferring data between a buffer and an audio source or sink. @@ -108,6 +114,10 @@ class AudioSinkTransferBuffer : public AudioTransferBuffer { void set_sink(speaker::Speaker *speaker) { this->speaker_ = speaker; } #endif + /// @brief Adds a callback as the transfer buffer's sink. + /// @param callback Pointer to the AudioSinkCallback implementation + void set_sink(AudioSinkCallback *callback) { this->sink_callback_ = callback; } + void clear_buffered_data() override; bool has_buffered_data() const override; @@ -116,6 +126,7 @@ class AudioSinkTransferBuffer : public AudioTransferBuffer { #ifdef USE_SPEAKER speaker::Speaker *speaker_{nullptr}; #endif + AudioSinkCallback *sink_callback_{nullptr}; }; class AudioSourceTransferBuffer : public AudioTransferBuffer { From 264c8faedd0ab956b0437ec2ecc0bacae1708923 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Wed, 18 Feb 2026 21:51:01 -0600 Subject: [PATCH 088/197] [media_player] Add more commands to support Sendspin (#12258) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: J. Nick Koston --- esphome/components/media_player/__init__.py | 250 +++++++----------- esphome/components/media_player/automation.h | 27 ++ .../components/media_player/media_player.cpp | 52 ++++ .../components/media_player/media_player.h | 46 ++-- tests/components/media_player/common.yaml | 25 ++ 5 files changed, 232 insertions(+), 168 deletions(-) diff --git a/esphome/components/media_player/__init__.py b/esphome/components/media_player/__init__.py index c6ffe50d79..b2afbe5e58 100644 --- a/esphome/components/media_player/__init__.py +++ b/esphome/components/media_player/__init__.py @@ -35,86 +35,73 @@ MEDIA_PLAYER_FORMAT_PURPOSE_ENUM = { "announcement": MediaPlayerFormatPurpose.PURPOSE_ANNOUNCEMENT, } - -PlayAction = media_player_ns.class_( - "PlayAction", automation.Action, cg.Parented.template(MediaPlayer) -) -PlayMediaAction = media_player_ns.class_( - "PlayMediaAction", automation.Action, cg.Parented.template(MediaPlayer) -) -ToggleAction = media_player_ns.class_( - "ToggleAction", automation.Action, cg.Parented.template(MediaPlayer) -) -PauseAction = media_player_ns.class_( - "PauseAction", automation.Action, cg.Parented.template(MediaPlayer) -) -StopAction = media_player_ns.class_( - "StopAction", automation.Action, cg.Parented.template(MediaPlayer) -) -VolumeUpAction = media_player_ns.class_( - "VolumeUpAction", automation.Action, cg.Parented.template(MediaPlayer) -) -VolumeDownAction = media_player_ns.class_( - "VolumeDownAction", automation.Action, cg.Parented.template(MediaPlayer) -) -VolumeSetAction = media_player_ns.class_( - "VolumeSetAction", automation.Action, cg.Parented.template(MediaPlayer) -) -TurnOnAction = media_player_ns.class_( - "TurnOnAction", automation.Action, cg.Parented.template(MediaPlayer) -) -TurnOffAction = media_player_ns.class_( - "TurnOffAction", automation.Action, cg.Parented.template(MediaPlayer) -) - +# Local config key constants CONF_ANNOUNCEMENT = "announcement" CONF_ON_PLAY = "on_play" CONF_ON_PAUSE = "on_pause" CONF_ON_ANNOUNCEMENT = "on_announcement" CONF_MEDIA_URL = "media_url" -StateTrigger = media_player_ns.class_("StateTrigger", automation.Trigger.template()) -IdleTrigger = media_player_ns.class_("IdleTrigger", automation.Trigger.template()) -PlayTrigger = media_player_ns.class_("PlayTrigger", automation.Trigger.template()) -PauseTrigger = media_player_ns.class_("PauseTrigger", automation.Trigger.template()) -AnnoucementTrigger = media_player_ns.class_( - "AnnouncementTrigger", automation.Trigger.template() +# Command actions that all share the same schema and codegen handler +_COMMAND_ACTIONS = [ + "play", + "pause", + "stop", + "toggle", + "volume_up", + "volume_down", + "turn_on", + "turn_off", + "next", + "previous", + "mute", + "unmute", + "repeat_off", + "repeat_one", + "repeat_all", + "shuffle", + "unshuffle", + "group_join", + "clear_playlist", +] + +# State triggers: (config_key, C++ class name) +_STATE_TRIGGERS = [ + (CONF_ON_STATE, "StateTrigger"), + (CONF_ON_IDLE, "IdleTrigger"), + (CONF_ON_PLAY, "PlayTrigger"), + (CONF_ON_PAUSE, "PauseTrigger"), + (CONF_ON_ANNOUNCEMENT, "AnnouncementTrigger"), + (CONF_ON_TURN_ON, "OnTrigger"), + (CONF_ON_TURN_OFF, "OffTrigger"), +] + +# State conditions that all share the same schema and codegen handler +_STATE_CONDITIONS = [ + "idle", + "paused", + "playing", + "announcing", + "on", + "off", + "muted", +] + +# Special action classes with custom schemas/handlers +PlayMediaAction = media_player_ns.class_( + "PlayMediaAction", automation.Action, cg.Parented.template(MediaPlayer) ) -OnTrigger = media_player_ns.class_("OnTrigger", automation.Trigger.template()) -OffTrigger = media_player_ns.class_("OffTrigger", automation.Trigger.template()) -IsIdleCondition = media_player_ns.class_("IsIdleCondition", automation.Condition) -IsPausedCondition = media_player_ns.class_("IsPausedCondition", automation.Condition) -IsPlayingCondition = media_player_ns.class_("IsPlayingCondition", automation.Condition) -IsAnnouncingCondition = media_player_ns.class_( - "IsAnnouncingCondition", automation.Condition +VolumeSetAction = media_player_ns.class_( + "VolumeSetAction", automation.Action, cg.Parented.template(MediaPlayer) ) -IsOnCondition = media_player_ns.class_("IsOnCondition", automation.Condition) -IsOffCondition = media_player_ns.class_("IsOffCondition", automation.Condition) async def setup_media_player_core_(var, config): await setup_entity(var, config, "media_player") - for conf in config.get(CONF_ON_STATE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) - for conf in config.get(CONF_ON_IDLE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) - for conf in config.get(CONF_ON_PLAY, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) - for conf in config.get(CONF_ON_PAUSE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) - for conf in config.get(CONF_ON_ANNOUNCEMENT, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) - for conf in config.get(CONF_ON_TURN_ON, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) - for conf in config.get(CONF_ON_TURN_OFF, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) + for conf_key, _ in _STATE_TRIGGERS: + for conf in config.get(conf_key, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) async def register_media_player(var, config): @@ -133,41 +120,14 @@ async def new_media_player(config, *args): _MEDIA_PLAYER_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend( { - cv.Optional(CONF_ON_STATE): automation.validate_automation( + cv.Optional(conf_key): automation.validate_automation( { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StateTrigger), + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + media_player_ns.class_(class_name, automation.Trigger.template()) + ), } - ), - cv.Optional(CONF_ON_IDLE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(IdleTrigger), - } - ), - cv.Optional(CONF_ON_PLAY): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(PlayTrigger), - } - ), - cv.Optional(CONF_ON_PAUSE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(PauseTrigger), - } - ), - cv.Optional(CONF_ON_ANNOUNCEMENT): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(AnnoucementTrigger), - } - ), - cv.Optional(CONF_ON_TURN_ON): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(OnTrigger), - } - ), - cv.Optional(CONF_ON_TURN_OFF): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(OffTrigger), - } - ), + ) + for conf_key, class_name in _STATE_TRIGGERS } ) @@ -228,56 +188,48 @@ async def media_player_play_media_action(config, action_id, template_arg, args): return var -@automation.register_action("media_player.play", PlayAction, MEDIA_PLAYER_ACTION_SCHEMA) -@automation.register_action( - "media_player.toggle", ToggleAction, MEDIA_PLAYER_ACTION_SCHEMA -) -@automation.register_action( - "media_player.pause", PauseAction, MEDIA_PLAYER_ACTION_SCHEMA -) -@automation.register_action("media_player.stop", StopAction, MEDIA_PLAYER_ACTION_SCHEMA) -@automation.register_action( - "media_player.volume_up", VolumeUpAction, MEDIA_PLAYER_ACTION_SCHEMA -) -@automation.register_action( - "media_player.volume_down", VolumeDownAction, MEDIA_PLAYER_ACTION_SCHEMA -) -@automation.register_action( - "media_player.turn_on", TurnOnAction, MEDIA_PLAYER_ACTION_SCHEMA -) -@automation.register_action( - "media_player.turn_off", TurnOffAction, MEDIA_PLAYER_ACTION_SCHEMA -) -async def media_player_action(config, action_id, template_arg, args): - var = cg.new_Pvariable(action_id, template_arg) - await cg.register_parented(var, config[CONF_ID]) - announcement = await cg.templatable(config[CONF_ANNOUNCEMENT], args, cg.bool_) - cg.add(var.set_announcement(announcement)) - return var +def _snake_to_camel(name): + return "".join(word.capitalize() for word in name.split("_")) -@automation.register_condition( - "media_player.is_idle", IsIdleCondition, MEDIA_PLAYER_CONDITION_SCHEMA -) -@automation.register_condition( - "media_player.is_paused", IsPausedCondition, MEDIA_PLAYER_CONDITION_SCHEMA -) -@automation.register_condition( - "media_player.is_playing", IsPlayingCondition, MEDIA_PLAYER_CONDITION_SCHEMA -) -@automation.register_condition( - "media_player.is_announcing", IsAnnouncingCondition, MEDIA_PLAYER_CONDITION_SCHEMA -) -@automation.register_condition( - "media_player.is_on", IsOnCondition, MEDIA_PLAYER_CONDITION_SCHEMA -) -@automation.register_condition( - "media_player.is_off", IsOffCondition, MEDIA_PLAYER_CONDITION_SCHEMA -) -async def media_player_condition(config, action_id, template_arg, args): - var = cg.new_Pvariable(action_id, template_arg) - await cg.register_parented(var, config[CONF_ID]) - return var +def _register_command_actions(): + async def handler(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + announcement = await cg.templatable(config[CONF_ANNOUNCEMENT], args, cg.bool_) + cg.add(var.set_announcement(announcement)) + return var + + for action_name in _COMMAND_ACTIONS: + class_name = f"{_snake_to_camel(action_name)}Action" + action_class = media_player_ns.class_( + class_name, automation.Action, cg.Parented.template(MediaPlayer) + ) + automation.register_action( + f"media_player.{action_name}", action_class, MEDIA_PLAYER_ACTION_SCHEMA + )(handler) + + +_register_command_actions() + + +def _register_state_conditions(): + async def handler(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var + + for condition_name in _STATE_CONDITIONS: + class_name = f"Is{_snake_to_camel(condition_name)}Condition" + condition_class = media_player_ns.class_(class_name, automation.Condition) + automation.register_condition( + f"media_player.is_{condition_name}", + condition_class, + MEDIA_PLAYER_CONDITION_SCHEMA, + )(handler) + + +_register_state_conditions() @automation.register_action( diff --git a/esphome/components/media_player/automation.h b/esphome/components/media_player/automation.h index 50e7693cb5..90e7bf75b5 100644 --- a/esphome/components/media_player/automation.h +++ b/esphome/components/media_player/automation.h @@ -32,6 +32,28 @@ template using TurnOnAction = MediaPlayerCommandAction; template using TurnOffAction = MediaPlayerCommandAction; +template +using NextAction = MediaPlayerCommandAction; +template +using PreviousAction = MediaPlayerCommandAction; +template +using MuteAction = MediaPlayerCommandAction; +template +using UnmuteAction = MediaPlayerCommandAction; +template +using RepeatOffAction = MediaPlayerCommandAction; +template +using RepeatOneAction = MediaPlayerCommandAction; +template +using RepeatAllAction = MediaPlayerCommandAction; +template +using ShuffleAction = MediaPlayerCommandAction; +template +using UnshuffleAction = MediaPlayerCommandAction; +template +using GroupJoinAction = MediaPlayerCommandAction; +template +using ClearPlaylistAction = MediaPlayerCommandAction; template class PlayMediaAction : public Action, public Parented { TEMPLATABLE_VALUE(std::string, media_url) @@ -105,5 +127,10 @@ template class IsOffCondition : public Condition, public bool check(const Ts &...x) override { return this->parent_->state == MediaPlayerState::MEDIA_PLAYER_STATE_OFF; } }; +template class IsMutedCondition : public Condition, public Parented { + public: + bool check(const Ts &...x) override { return this->parent_->is_muted(); } +}; + } // namespace media_player } // namespace esphome diff --git a/esphome/components/media_player/media_player.cpp b/esphome/components/media_player/media_player.cpp index 17d9b054da..a53d598b0f 100644 --- a/esphome/components/media_player/media_player.cpp +++ b/esphome/components/media_player/media_player.cpp @@ -60,11 +60,39 @@ const char *media_player_command_to_string(MediaPlayerCommand command) { return "TURN_ON"; case MEDIA_PLAYER_COMMAND_TURN_OFF: return "TURN_OFF"; + case MEDIA_PLAYER_COMMAND_NEXT: + return "NEXT"; + case MEDIA_PLAYER_COMMAND_PREVIOUS: + return "PREVIOUS"; + case MEDIA_PLAYER_COMMAND_REPEAT_ALL: + return "REPEAT_ALL"; + case MEDIA_PLAYER_COMMAND_SHUFFLE: + return "SHUFFLE"; + case MEDIA_PLAYER_COMMAND_UNSHUFFLE: + return "UNSHUFFLE"; + case MEDIA_PLAYER_COMMAND_GROUP_JOIN: + return "GROUP_JOIN"; default: return "UNKNOWN"; } } +void MediaPlayerTraits::set_supports_pause(bool supports_pause) { + if (supports_pause) { + this->feature_flags_ |= MediaPlayerEntityFeature::PAUSE | MediaPlayerEntityFeature::PLAY; + } else { + this->feature_flags_ &= ~(MediaPlayerEntityFeature::PAUSE | MediaPlayerEntityFeature::PLAY); + } +} + +void MediaPlayerTraits::set_supports_turn_off_on(bool supports_turn_off_on) { + if (supports_turn_off_on) { + this->feature_flags_ |= MediaPlayerEntityFeature::TURN_OFF | MediaPlayerEntityFeature::TURN_ON; + } else { + this->feature_flags_ &= ~(MediaPlayerEntityFeature::TURN_OFF | MediaPlayerEntityFeature::TURN_ON); + } +} + void MediaPlayerCall::validate_() { if (this->media_url_.has_value()) { if (this->command_.has_value() && this->command_.value() != MEDIA_PLAYER_COMMAND_ENQUEUE) { @@ -125,6 +153,30 @@ MediaPlayerCall &MediaPlayerCall::set_command(const char *command) { this->set_command(MEDIA_PLAYER_COMMAND_TURN_ON); } else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("TURN_OFF")) == 0) { this->set_command(MEDIA_PLAYER_COMMAND_TURN_OFF); + } else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("VOLUME_UP")) == 0) { + this->set_command(MEDIA_PLAYER_COMMAND_VOLUME_UP); + } else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("VOLUME_DOWN")) == 0) { + this->set_command(MEDIA_PLAYER_COMMAND_VOLUME_DOWN); + } else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("ENQUEUE")) == 0) { + this->set_command(MEDIA_PLAYER_COMMAND_ENQUEUE); + } else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("REPEAT_ONE")) == 0) { + this->set_command(MEDIA_PLAYER_COMMAND_REPEAT_ONE); + } else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("REPEAT_OFF")) == 0) { + this->set_command(MEDIA_PLAYER_COMMAND_REPEAT_OFF); + } else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("REPEAT_ALL")) == 0) { + this->set_command(MEDIA_PLAYER_COMMAND_REPEAT_ALL); + } else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("CLEAR_PLAYLIST")) == 0) { + this->set_command(MEDIA_PLAYER_COMMAND_CLEAR_PLAYLIST); + } else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("NEXT")) == 0) { + this->set_command(MEDIA_PLAYER_COMMAND_NEXT); + } else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("PREVIOUS")) == 0) { + this->set_command(MEDIA_PLAYER_COMMAND_PREVIOUS); + } else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("SHUFFLE")) == 0) { + this->set_command(MEDIA_PLAYER_COMMAND_SHUFFLE); + } else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("UNSHUFFLE")) == 0) { + this->set_command(MEDIA_PLAYER_COMMAND_UNSHUFFLE); + } else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("GROUP_JOIN")) == 0) { + this->set_command(MEDIA_PLAYER_COMMAND_GROUP_JOIN); } else { ESP_LOGW(TAG, "'%s' - Unrecognized command %s", this->parent_->get_name().c_str(), command); } diff --git a/esphome/components/media_player/media_player.h b/esphome/components/media_player/media_player.h index f75a68dd85..3509747718 100644 --- a/esphome/components/media_player/media_player.h +++ b/esphome/components/media_player/media_player.h @@ -58,6 +58,12 @@ enum MediaPlayerCommand : uint8_t { MEDIA_PLAYER_COMMAND_CLEAR_PLAYLIST = 11, MEDIA_PLAYER_COMMAND_TURN_ON = 12, MEDIA_PLAYER_COMMAND_TURN_OFF = 13, + MEDIA_PLAYER_COMMAND_NEXT = 14, + MEDIA_PLAYER_COMMAND_PREVIOUS = 15, + MEDIA_PLAYER_COMMAND_REPEAT_ALL = 16, + MEDIA_PLAYER_COMMAND_SHUFFLE = 17, + MEDIA_PLAYER_COMMAND_UNSHUFFLE = 18, + MEDIA_PLAYER_COMMAND_GROUP_JOIN = 19, }; const char *media_player_command_to_string(MediaPlayerCommand command); @@ -74,38 +80,40 @@ struct MediaPlayerSupportedFormat { uint32_t sample_bytes; }; +// Base features always reported for all media players +static constexpr uint32_t BASE_MEDIA_PLAYER_FEATURES = + MediaPlayerEntityFeature::PLAY_MEDIA | MediaPlayerEntityFeature::BROWSE_MEDIA | MediaPlayerEntityFeature::STOP | + MediaPlayerEntityFeature::VOLUME_SET | MediaPlayerEntityFeature::VOLUME_MUTE | + MediaPlayerEntityFeature::MEDIA_ANNOUNCE; + class MediaPlayer; class MediaPlayerTraits { public: MediaPlayerTraits() = default; - void set_supports_pause(bool supports_pause) { this->supports_pause_ = supports_pause; } - bool get_supports_pause() const { return this->supports_pause_; } - - void set_supports_turn_off_on(bool supports_turn_off_on) { this->supports_turn_off_on_ = supports_turn_off_on; } - bool get_supports_turn_off_on() const { return this->supports_turn_off_on_; } + uint32_t get_feature_flags() const { return this->feature_flags_; } + void add_feature_flags(uint32_t feature_flags) { this->feature_flags_ |= feature_flags; } + void clear_feature_flags(uint32_t feature_flags) { this->feature_flags_ &= ~feature_flags; } + // Returns true only if all specified flags are set + bool has_feature_flags(uint32_t feature_flags) const { + return (this->feature_flags_ & feature_flags) == feature_flags; + } std::vector &get_supported_formats() { return this->supported_formats_; } - uint32_t get_feature_flags() const { - uint32_t flags = 0; - flags |= MediaPlayerEntityFeature::PLAY_MEDIA | MediaPlayerEntityFeature::BROWSE_MEDIA | - MediaPlayerEntityFeature::STOP | MediaPlayerEntityFeature::VOLUME_SET | - MediaPlayerEntityFeature::VOLUME_MUTE | MediaPlayerEntityFeature::MEDIA_ANNOUNCE; - if (this->get_supports_pause()) { - flags |= MediaPlayerEntityFeature::PAUSE | MediaPlayerEntityFeature::PLAY; - } - if (this->get_supports_turn_off_on()) { - flags |= MediaPlayerEntityFeature::TURN_OFF | MediaPlayerEntityFeature::TURN_ON; - } - return flags; + // Legacy setters/getters are kept for backward compatibility + void set_supports_pause(bool supports_pause); + bool get_supports_pause() const { return this->has_feature_flags(MediaPlayerEntityFeature::PAUSE); } + + void set_supports_turn_off_on(bool supports_turn_off_on); + bool get_supports_turn_off_on() const { + return this->has_feature_flags(MediaPlayerEntityFeature::TURN_ON | MediaPlayerEntityFeature::TURN_OFF); } protected: std::vector supported_formats_{}; - bool supports_pause_{false}; - bool supports_turn_off_on_{false}; + uint32_t feature_flags_{BASE_MEDIA_PLAYER_FEATURES}; }; class MediaPlayerCall { diff --git a/tests/components/media_player/common.yaml b/tests/components/media_player/common.yaml index 763bc231c0..c83ee89ad4 100644 --- a/tests/components/media_player/common.yaml +++ b/tests/components/media_player/common.yaml @@ -23,8 +23,27 @@ media_player: - media_player.stop: - media_player.stop: announcement: true + on_announcement: + - media_player.play: + on_turn_on: + - media_player.play: + on_turn_off: + - media_player.stop: on_pause: - media_player.toggle: + - media_player.turn_on: + - media_player.turn_off: + - media_player.next: + - media_player.previous: + - media_player.mute: + - media_player.unmute: + - media_player.repeat_off: + - media_player.repeat_one: + - media_player.repeat_all: + - media_player.shuffle: + - media_player.unshuffle: + - media_player.group_join: + - media_player.clear_playlist: - wait_until: media_player.is_idle: - wait_until: @@ -33,6 +52,12 @@ media_player: media_player.is_announcing: - wait_until: media_player.is_paused: + - wait_until: + media_player.is_on: + - wait_until: + media_player.is_off: + - wait_until: + media_player.is_muted: - media_player.volume_up: - media_player.volume_down: - media_player.volume_set: 50% From ba7134ee3f870ce00d2990d84b142b734028330f Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Wed, 18 Feb 2026 21:51:16 -0600 Subject: [PATCH 089/197] [mdns] add Sendspin advertisement support (#14013) Co-authored-by: J. Nick Koston --- esphome/components/mdns/__init__.py | 2 +- esphome/components/mdns/mdns_component.cpp | 19 ++++++++++++++++++- esphome/core/defines.h | 2 ++ 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/esphome/components/mdns/__init__.py b/esphome/components/mdns/__init__.py index 3088d8ad7e..f87f929615 100644 --- a/esphome/components/mdns/__init__.py +++ b/esphome/components/mdns/__init__.py @@ -21,7 +21,7 @@ DEPENDENCIES = ["network"] # Components that create mDNS services at runtime # IMPORTANT: If you add a new component here, you must also update the corresponding # #ifdef blocks in mdns_component.cpp compile_records_() method -COMPONENTS_WITH_MDNS_SERVICES = ("api", "prometheus", "web_server") +COMPONENTS_WITH_MDNS_SERVICES = ("api", "prometheus", "sendspin", "web_server") mdns_ns = cg.esphome_ns.namespace("mdns") MDNSComponent = mdns_ns.class_("MDNSComponent", cg.Component) diff --git a/esphome/components/mdns/mdns_component.cpp b/esphome/components/mdns/mdns_component.cpp index 47db92610a..5e5e1279d9 100644 --- a/esphome/components/mdns/mdns_component.cpp +++ b/esphome/components/mdns/mdns_component.cpp @@ -29,6 +29,10 @@ static const char *const TAG = "mdns"; #define USE_WEBSERVER_PORT 80 // NOLINT #endif +#ifndef USE_SENDSPIN_PORT +#define USE_SENDSPIN_PORT 8928 // NOLINT +#endif + // Define all constant strings using the macro MDNS_STATIC_CONST_CHAR(SERVICE_TCP, "_tcp"); @@ -150,6 +154,18 @@ void MDNSComponent::compile_records_(StaticVector Date: Wed, 18 Feb 2026 21:51:33 -0600 Subject: [PATCH 090/197] [audio, speaker] Add support for decoding Ogg Opus files (#13967) --- esphome/components/audio/__init__.py | 42 ++++++++++ esphome/components/audio/audio.cpp | 4 + esphome/components/audio/audio.h | 3 + esphome/components/audio/audio_decoder.cpp | 60 +++++++++++++- esphome/components/audio/audio_decoder.h | 15 +++- esphome/components/audio/audio_reader.cpp | 13 +++ .../speaker/media_player/__init__.py | 82 ++++++++++++++++--- .../speaker/media_player/audio_pipeline.cpp | 10 +++ esphome/core/defines.h | 1 + esphome/idf_component.yml | 2 + .../speaker/audio_dac.esp32-ard.yaml | 10 --- .../speaker/common-media_player.yaml | 7 ++ .../speaker/media_player.esp32-s3-idf.yaml | 9 -- ...idf.yaml => test-audio_dac.esp32-idf.yaml} | 0 ....yaml => test-media_player.esp32-idf.yaml} | 0 tests/components/speaker/test.esp32-ard.yaml | 10 --- 16 files changed, 222 insertions(+), 46 deletions(-) delete mode 100644 tests/components/speaker/audio_dac.esp32-ard.yaml delete mode 100644 tests/components/speaker/media_player.esp32-s3-idf.yaml rename tests/components/speaker/{audio_dac.esp32-idf.yaml => test-audio_dac.esp32-idf.yaml} (100%) rename tests/components/speaker/{media_player.esp32-idf.yaml => test-media_player.esp32-idf.yaml} (100%) delete mode 100644 tests/components/speaker/test.esp32-ard.yaml diff --git a/esphome/components/audio/__init__.py b/esphome/components/audio/__init__.py index f48b776ddd..d8d426ec63 100644 --- a/esphome/components/audio/__init__.py +++ b/esphome/components/audio/__init__.py @@ -1,10 +1,14 @@ +from dataclasses import dataclass + import esphome.codegen as cg from esphome.components.esp32 import add_idf_component, include_builtin_idf_component import esphome.config_validation as cv from esphome.const import CONF_BITS_PER_SAMPLE, CONF_NUM_CHANNELS, CONF_SAMPLE_RATE +from esphome.core import CORE import esphome.final_validate as fv CODEOWNERS = ["@kahrendt"] +DOMAIN = "audio" audio_ns = cg.esphome_ns.namespace("audio") AudioFile = audio_ns.struct("AudioFile") @@ -14,9 +18,38 @@ AUDIO_FILE_TYPE_ENUM = { "WAV": AudioFileType.WAV, "MP3": AudioFileType.MP3, "FLAC": AudioFileType.FLAC, + "OPUS": AudioFileType.OPUS, } +@dataclass +class AudioData: + flac_support: bool = False + mp3_support: bool = False + opus_support: bool = False + + +def _get_data() -> AudioData: + if DOMAIN not in CORE.data: + CORE.data[DOMAIN] = AudioData() + return CORE.data[DOMAIN] + + +def request_flac_support() -> None: + """Request FLAC codec support for audio decoding.""" + _get_data().flac_support = True + + +def request_mp3_support() -> None: + """Request MP3 codec support for audio decoding.""" + _get_data().mp3_support = True + + +def request_opus_support() -> None: + """Request Opus codec support for audio decoding.""" + _get_data().opus_support = True + + CONF_MIN_BITS_PER_SAMPLE = "min_bits_per_sample" CONF_MAX_BITS_PER_SAMPLE = "max_bits_per_sample" CONF_MIN_CHANNELS = "min_channels" @@ -173,3 +206,12 @@ async def to_code(config): name="esphome/esp-audio-libs", ref="2.0.3", ) + + data = _get_data() + if data.flac_support: + cg.add_define("USE_AUDIO_FLAC_SUPPORT") + if data.mp3_support: + cg.add_define("USE_AUDIO_MP3_SUPPORT") + if data.opus_support: + cg.add_define("USE_AUDIO_OPUS_SUPPORT") + add_idf_component(name="esphome/micro-opus", ref="0.3.3") diff --git a/esphome/components/audio/audio.cpp b/esphome/components/audio/audio.cpp index 9cc9b7d0da..40592f6107 100644 --- a/esphome/components/audio/audio.cpp +++ b/esphome/components/audio/audio.cpp @@ -46,6 +46,10 @@ const char *audio_file_type_to_string(AudioFileType file_type) { #ifdef USE_AUDIO_MP3_SUPPORT case AudioFileType::MP3: return "MP3"; +#endif +#ifdef USE_AUDIO_OPUS_SUPPORT + case AudioFileType::OPUS: + return "OPUS"; #endif case AudioFileType::WAV: return "WAV"; diff --git a/esphome/components/audio/audio.h b/esphome/components/audio/audio.h index e01d7eb101..7d7db9e944 100644 --- a/esphome/components/audio/audio.h +++ b/esphome/components/audio/audio.h @@ -112,6 +112,9 @@ enum class AudioFileType : uint8_t { #endif #ifdef USE_AUDIO_MP3_SUPPORT MP3, +#endif +#ifdef USE_AUDIO_OPUS_SUPPORT + OPUS, #endif WAV, }; diff --git a/esphome/components/audio/audio_decoder.cpp b/esphome/components/audio/audio_decoder.cpp index 8f514468c4..ee6d7d0a15 100644 --- a/esphome/components/audio/audio_decoder.cpp +++ b/esphome/components/audio/audio_decoder.cpp @@ -3,10 +3,13 @@ #ifdef USE_ESP32 #include "esphome/core/hal.h" +#include "esphome/core/log.h" namespace esphome { namespace audio { +static const char *const TAG = "audio.decoder"; + static const uint32_t DECODING_TIMEOUT_MS = 50; // The decode function will yield after this duration static const uint32_t READ_WRITE_TIMEOUT_MS = 20; // Timeout for transferring audio data @@ -79,6 +82,14 @@ esp_err_t AudioDecoder::start(AudioFileType audio_file_type) { // Always reallocate the output transfer buffer to the smallest necessary size this->output_transfer_buffer_->reallocate(this->free_buffer_required_); break; +#endif +#ifdef USE_AUDIO_OPUS_SUPPORT + case AudioFileType::OPUS: + this->opus_decoder_ = make_unique(); + this->free_buffer_required_ = + this->output_transfer_buffer_->capacity(); // Adjusted and reallocated after reading the header + this->decoder_buffers_internally_ = true; + break; #endif case AudioFileType::WAV: this->wav_decoder_ = make_unique(); @@ -158,8 +169,9 @@ AudioDecoderState AudioDecoder::decode(bool stop_gracefully) { // Decode more audio // Only shift data on the first loop iteration to avoid unnecessary, slow moves - size_t bytes_read = this->input_transfer_buffer_->transfer_data_from_source(pdMS_TO_TICKS(READ_WRITE_TIMEOUT_MS), - first_loop_iteration); + // If the decoder buffers internally, then never shift + size_t bytes_read = this->input_transfer_buffer_->transfer_data_from_source( + pdMS_TO_TICKS(READ_WRITE_TIMEOUT_MS), first_loop_iteration && !this->decoder_buffers_internally_); if (!first_loop_iteration && (this->input_transfer_buffer_->available() < bytes_processed)) { // Less data is available than what was processed in last iteration, so don't attempt to decode. @@ -195,6 +207,11 @@ AudioDecoderState AudioDecoder::decode(bool stop_gracefully) { case AudioFileType::MP3: state = this->decode_mp3_(); break; +#endif +#ifdef USE_AUDIO_OPUS_SUPPORT + case AudioFileType::OPUS: + state = this->decode_opus_(); + break; #endif case AudioFileType::WAV: state = this->decode_wav_(); @@ -339,6 +356,45 @@ FileDecoderState AudioDecoder::decode_mp3_() { } #endif +#ifdef USE_AUDIO_OPUS_SUPPORT +FileDecoderState AudioDecoder::decode_opus_() { + bool processed_header = this->opus_decoder_->is_initialized(); + + size_t bytes_consumed, samples_decoded; + + micro_opus::OggOpusResult result = this->opus_decoder_->decode( + this->input_transfer_buffer_->get_buffer_start(), this->input_transfer_buffer_->available(), + this->output_transfer_buffer_->get_buffer_end(), this->output_transfer_buffer_->free(), bytes_consumed, + samples_decoded); + + if (result == micro_opus::OGG_OPUS_OK) { + if (!processed_header && this->opus_decoder_->is_initialized()) { + // Header processed and stream info is available + this->audio_stream_info_ = + audio::AudioStreamInfo(this->opus_decoder_->get_bit_depth(), this->opus_decoder_->get_channels(), + this->opus_decoder_->get_sample_rate()); + } + if (samples_decoded > 0 && this->audio_stream_info_.has_value()) { + // Some audio was processed + this->output_transfer_buffer_->increase_buffer_length( + this->audio_stream_info_.value().frames_to_bytes(samples_decoded)); + } + this->input_transfer_buffer_->decrease_buffer_length(bytes_consumed); + } else if (result == micro_opus::OGG_OPUS_OUTPUT_BUFFER_TOO_SMALL) { + // Reallocate to decode the packet on the next call + this->free_buffer_required_ = this->opus_decoder_->get_required_output_buffer_size(); + if (!this->output_transfer_buffer_->reallocate(this->free_buffer_required_)) { + // Couldn't reallocate output buffer + return FileDecoderState::FAILED; + } + } else { + ESP_LOGE(TAG, "Opus decoder failed: %" PRId8, result); + return FileDecoderState::POTENTIALLY_FAILED; + } + return FileDecoderState::MORE_TO_PROCESS; +} +#endif + FileDecoderState AudioDecoder::decode_wav_() { if (!this->audio_stream_info_.has_value()) { // Header hasn't been processed diff --git a/esphome/components/audio/audio_decoder.h b/esphome/components/audio/audio_decoder.h index 2ca1d623fe..cad16110ae 100644 --- a/esphome/components/audio/audio_decoder.h +++ b/esphome/components/audio/audio_decoder.h @@ -24,6 +24,11 @@ #endif #include +// micro-opus +#ifdef USE_AUDIO_OPUS_SUPPORT +#include +#endif + namespace esphome { namespace audio { @@ -47,7 +52,7 @@ class AudioDecoder { * @brief Class that facilitates decoding an audio file. * The audio file is read from a ring buffer source, decoded, and sent to an audio sink (ring buffer or speaker * component). - * Supports wav, flac, and mp3 formats. + * Supports wav, flac, mp3, and ogg opus formats. */ public: /// @brief Allocates the input and output transfer buffers @@ -55,7 +60,7 @@ class AudioDecoder { /// @param output_buffer_size Size of the output transfer buffer in bytes. AudioDecoder(size_t input_buffer_size, size_t output_buffer_size); - /// @brief Deallocates the MP3 decoder (the flac and wav decoders are deallocated automatically) + /// @brief Deallocates the MP3 decoder (the flac, opus, and wav decoders are deallocated automatically) ~AudioDecoder(); /// @brief Adds a source ring buffer for raw file data. Takes ownership of the ring buffer in a shared_ptr. @@ -108,6 +113,10 @@ class AudioDecoder { #ifdef USE_AUDIO_MP3_SUPPORT FileDecoderState decode_mp3_(); esp_audio_libs::helix_decoder::HMP3Decoder mp3_decoder_; +#endif +#ifdef USE_AUDIO_OPUS_SUPPORT + FileDecoderState decode_opus_(); + std::unique_ptr opus_decoder_; #endif FileDecoderState decode_wav_(); @@ -124,6 +133,8 @@ class AudioDecoder { bool end_of_file_{false}; bool wav_has_known_end_{false}; + bool decoder_buffers_internally_{false}; + bool pause_output_{false}; uint32_t accumulated_frames_written_{0}; diff --git a/esphome/components/audio/audio_reader.cpp b/esphome/components/audio/audio_reader.cpp index 4e4bd31f9b..78d69d7a39 100644 --- a/esphome/components/audio/audio_reader.cpp +++ b/esphome/components/audio/audio_reader.cpp @@ -197,6 +197,11 @@ esp_err_t AudioReader::start(const std::string &uri, AudioFileType &file_type) { else if (str_endswith_ignore_case(url, ".flac")) { file_type = AudioFileType::FLAC; } +#endif +#ifdef USE_AUDIO_OPUS_SUPPORT + else if (str_endswith_ignore_case(url, ".opus")) { + file_type = AudioFileType::OPUS; + } #endif else { file_type = AudioFileType::NONE; @@ -241,6 +246,14 @@ AudioFileType AudioReader::get_audio_type(const char *content_type) { if (strcasecmp(content_type, "audio/flac") == 0 || strcasecmp(content_type, "audio/x-flac") == 0) { return AudioFileType::FLAC; } +#endif +#ifdef USE_AUDIO_OPUS_SUPPORT + // Match "audio/ogg" with a codecs parameter containing "opus" + // Valid forms: audio/ogg;codecs=opus, audio/ogg; codecs="opus", etc. + // Plain "audio/ogg" without a codecs parameter is not matched, as those are almost always Ogg Vorbis streams + if (strncasecmp(content_type, "audio/ogg", 9) == 0 && strcasestr(content_type + 9, "opus") != nullptr) { + return AudioFileType::OPUS; + } #endif return AudioFileType::NONE; } diff --git a/esphome/components/speaker/media_player/__init__.py b/esphome/components/speaker/media_player/__init__.py index 034312236c..b302bd9b23 100644 --- a/esphome/components/speaker/media_player/__init__.py +++ b/esphome/components/speaker/media_player/__init__.py @@ -26,7 +26,6 @@ from esphome.const import ( from esphome.core import CORE, HexInt from esphome.core.entity_helpers import inherit_property_from from esphome.external_files import download_content -from esphome.final_validate import full_config _LOGGER = logging.getLogger(__name__) @@ -37,6 +36,10 @@ DEPENDENCIES = ["network"] CODEOWNERS = ["@kahrendt", "@synesthesiam"] DOMAIN = "media_player" +CODEC_SUPPORT_ALL = "all" +CODEC_SUPPORT_NEEDED = "needed" +CODEC_SUPPORT_NONE = "none" + TYPE_LOCAL = "local" TYPE_WEB = "web" @@ -110,6 +113,8 @@ def _get_supported_format_struct(pipeline, type): args.append(("format", "flac")) elif pipeline[CONF_FORMAT] == "MP3": args.append(("format", "mp3")) + elif pipeline[CONF_FORMAT] == "OPUS": + args.append(("format", "opus")) elif pipeline[CONF_FORMAT] == "WAV": args.append(("format", "wav")) @@ -173,6 +178,13 @@ def _read_audio_file_and_type(file_config): media_file_type = audio.AUDIO_FILE_TYPE_ENUM["MP3"] elif file_type in ("flac"): media_file_type = audio.AUDIO_FILE_TYPE_ENUM["FLAC"] + elif ( + file_type in ("ogg") + and len(data) >= 36 + and data.startswith(b"OggS") + and data[28:36] == b"OpusHead" + ): + media_file_type = audio.AUDIO_FILE_TYPE_ENUM["OPUS"] return data, media_file_type @@ -199,6 +211,10 @@ def _validate_pipeline(config): inherit_property_from(CONF_NUM_CHANNELS, CONF_SPEAKER)(config) inherit_property_from(CONF_SAMPLE_RATE, CONF_SPEAKER)(config) + # Opus only supports 48 kHz + if config.get(CONF_FORMAT) == "OPUS" and config.get(CONF_SAMPLE_RATE) != 48000: + raise cv.Invalid("Opus only supports a sample rate of 48000 Hz") + # Validate the transcoder settings is compatible with the speaker audio.final_validate_audio_schema( "speaker media_player", @@ -225,12 +241,27 @@ def _validate_repeated_speaker(config): def _final_validate(config): - # Default to using codec if psram is enabled - if (use_codec := config.get(CONF_CODEC_SUPPORT_ENABLED)) is None: - use_codec = psram.DOMAIN in full_config.get() - conf_id = config[CONF_ID].id - core_data = CORE.data.setdefault(DOMAIN, {conf_id: {}}) - core_data[conf_id][CONF_CODEC_SUPPORT_ENABLED] = use_codec + # Normalize boolean values to string equivalents + codec_mode = config[CONF_CODEC_SUPPORT_ENABLED] + if codec_mode is True: + codec_mode = CODEC_SUPPORT_ALL + elif codec_mode is False: + codec_mode = CODEC_SUPPORT_NONE + + use_codec = codec_mode != CODEC_SUPPORT_NONE + + # In "needed" mode, collect formats from pipelines and files + needed_formats = set() + need_all = False + if codec_mode == CODEC_SUPPORT_NEEDED: + for pipeline_key in (CONF_ANNOUNCEMENT_PIPELINE, CONF_MEDIA_PIPELINE): + if pipeline := config.get(pipeline_key): + fmt = pipeline[CONF_FORMAT] + if fmt == "NONE": + # No preferred format means any format could arrive + need_all = True + else: + needed_formats.add(fmt) for file_config in config.get(CONF_FILES, []): _, media_file_type = _read_audio_file_and_type(file_config) @@ -243,6 +274,26 @@ def _final_validate(config): raise cv.Invalid( f"Unsupported local media file type, set {CONF_CODEC_SUPPORT_ENABLED} to true or convert the media file to wav" ) + # In "needed" mode, add file format to needed codecs + if codec_mode == CODEC_SUPPORT_NEEDED: + for fmt_name, fmt_enum in audio.AUDIO_FILE_TYPE_ENUM.items(): + if str(media_file_type) == str(fmt_enum): + if fmt_name not in ("WAV", "NONE"): + needed_formats.add(fmt_name) + break + + # Request codec support + if codec_mode == CODEC_SUPPORT_ALL or need_all: + audio.request_flac_support() + audio.request_mp3_support() + audio.request_opus_support() + elif codec_mode == CODEC_SUPPORT_NEEDED: + if "FLAC" in needed_formats: + audio.request_flac_support() + if "MP3" in needed_formats: + audio.request_mp3_support() + if "OPUS" in needed_formats: + audio.request_opus_support() return config @@ -307,7 +358,17 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_BUFFER_SIZE, default=1000000): cv.int_range( min=4000, max=4000000 ), - cv.Optional(CONF_CODEC_SUPPORT_ENABLED): cv.boolean, + cv.Optional( + CONF_CODEC_SUPPORT_ENABLED, default=CODEC_SUPPORT_NEEDED + ): cv.Any( + cv.boolean, + cv.one_of( + CODEC_SUPPORT_ALL, + CODEC_SUPPORT_NEEDED, + CODEC_SUPPORT_NONE, + lower=True, + ), + ), cv.Optional(CONF_FILES): cv.ensure_list(MEDIA_FILE_TYPE_SCHEMA), cv.Optional(CONF_TASK_STACK_IN_PSRAM): cv.All( cv.boolean, cv.requires_component(psram.DOMAIN) @@ -340,11 +401,6 @@ FINAL_VALIDATE_SCHEMA = cv.All( async def to_code(config): - if CORE.data[DOMAIN][config[CONF_ID].id][CONF_CODEC_SUPPORT_ENABLED]: - # Compile all supported audio codecs - cg.add_define("USE_AUDIO_FLAC_SUPPORT", True) - cg.add_define("USE_AUDIO_MP3_SUPPORT", True) - var = await media_player.new_media_player(config) await cg.register_component(var, config) diff --git a/esphome/components/speaker/media_player/audio_pipeline.cpp b/esphome/components/speaker/media_player/audio_pipeline.cpp index 8be37d740a..177743feb1 100644 --- a/esphome/components/speaker/media_player/audio_pipeline.cpp +++ b/esphome/components/speaker/media_player/audio_pipeline.cpp @@ -13,7 +13,12 @@ namespace speaker { static const uint32_t INITIAL_BUFFER_MS = 1000; // Start playback after buffering this duration of the file static const uint32_t READ_TASK_STACK_SIZE = 5 * 1024; +// Opus decoding uses more stack than other codecs +#ifdef USE_AUDIO_OPUS_SUPPORT +static const uint32_t DECODE_TASK_STACK_SIZE = 5 * 1024; +#else static const uint32_t DECODE_TASK_STACK_SIZE = 3 * 1024; +#endif static const uint32_t INFO_ERROR_QUEUE_COUNT = 5; @@ -552,6 +557,11 @@ void AudioPipeline::decode_task(void *params) { case audio::AudioFileType::FLAC: initial_bytes_to_buffer /= 2; // Estimate the FLAC compression factor is 2 break; +#endif +#ifdef USE_AUDIO_OPUS_SUPPORT + case audio::AudioFileType::OPUS: + initial_bytes_to_buffer /= 8; // Estimate the Opus compression factor is 8 + break; #endif default: break; diff --git a/esphome/core/defines.h b/esphome/core/defines.h index a70c6cd0d0..5559ec88d0 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -130,6 +130,7 @@ #define USE_AUDIO_DAC #define USE_AUDIO_FLAC_SUPPORT #define USE_AUDIO_MP3_SUPPORT +#define USE_AUDIO_OPUS_SUPPORT #define USE_API #define USE_API_CLIENT_CONNECTED_TRIGGER #define USE_API_CLIENT_DISCONNECTED_TRIGGER diff --git a/esphome/idf_component.yml b/esphome/idf_component.yml index f39ea9b3ae..83b2d9d95c 100644 --- a/esphome/idf_component.yml +++ b/esphome/idf_component.yml @@ -3,6 +3,8 @@ dependencies: version: "7.4.2" esphome/esp-audio-libs: version: 2.0.3 + esphome/micro-opus: + version: 0.3.3 espressif/esp-tflite-micro: version: 1.3.3~1 espressif/esp32-camera: diff --git a/tests/components/speaker/audio_dac.esp32-ard.yaml b/tests/components/speaker/audio_dac.esp32-ard.yaml deleted file mode 100644 index 3f5d1bba7c..0000000000 --- a/tests/components/speaker/audio_dac.esp32-ard.yaml +++ /dev/null @@ -1,10 +0,0 @@ -substitutions: - i2s_bclk_pin: GPIO27 - i2s_lrclk_pin: GPIO26 - i2s_mclk_pin: GPIO25 - i2s_dout_pin: GPIO23 - -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-ard.yaml - -<<: !include common-audio_dac.yaml diff --git a/tests/components/speaker/common-media_player.yaml b/tests/components/speaker/common-media_player.yaml index edc9f670fc..c958c0d912 100644 --- a/tests/components/speaker/common-media_player.yaml +++ b/tests/components/speaker/common-media_player.yaml @@ -1,5 +1,11 @@ <<: !include common.yaml +wifi: + ap: + +psram: + mode: quad + media_player: - platform: speaker id: speaker_media_player_id @@ -10,3 +16,4 @@ media_player: volume_max: 0.95 volume_min: 0.0 task_stack_in_psram: true + codec_support_enabled: all diff --git a/tests/components/speaker/media_player.esp32-s3-idf.yaml b/tests/components/speaker/media_player.esp32-s3-idf.yaml deleted file mode 100644 index b3eec04d23..0000000000 --- a/tests/components/speaker/media_player.esp32-s3-idf.yaml +++ /dev/null @@ -1,9 +0,0 @@ -substitutions: - scl_pin: GPIO2 - sda_pin: GPIO3 - i2s_bclk_pin: GPIO4 - i2s_lrclk_pin: GPIO5 - i2s_mclk_pin: GPIO6 - i2s_dout_pin: GPIO7 - -<<: !include common-media_player.yaml diff --git a/tests/components/speaker/audio_dac.esp32-idf.yaml b/tests/components/speaker/test-audio_dac.esp32-idf.yaml similarity index 100% rename from tests/components/speaker/audio_dac.esp32-idf.yaml rename to tests/components/speaker/test-audio_dac.esp32-idf.yaml diff --git a/tests/components/speaker/media_player.esp32-idf.yaml b/tests/components/speaker/test-media_player.esp32-idf.yaml similarity index 100% rename from tests/components/speaker/media_player.esp32-idf.yaml rename to tests/components/speaker/test-media_player.esp32-idf.yaml diff --git a/tests/components/speaker/test.esp32-ard.yaml b/tests/components/speaker/test.esp32-ard.yaml deleted file mode 100644 index 13350cd097..0000000000 --- a/tests/components/speaker/test.esp32-ard.yaml +++ /dev/null @@ -1,10 +0,0 @@ -substitutions: - i2s_bclk_pin: GPIO27 - i2s_lrclk_pin: GPIO26 - i2s_mclk_pin: GPIO25 - i2s_dout_pin: GPIO4 - -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-ard.yaml - -<<: !include common.yaml From 4d05e4d5765c037f234ddebb8442af2007c4fc60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc=20H=C3=B6rsken?= Date: Thu, 19 Feb 2026 04:52:38 +0100 Subject: [PATCH 091/197] [esp32_camera] Add support for sensors without JPEG support (#9496) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: J. Nick Koston Co-authored-by: J. Nick Koston --- esphome/components/esp32_camera/__init__.py | 50 ++++- .../components/esp32_camera/esp32_camera.cpp | 207 +++++++++++++----- .../components/esp32_camera/esp32_camera.h | 14 ++ esphome/core/defines.h | 1 + .../common/i2c_camera/esp32-idf.yaml | 1 + 5 files changed, 212 insertions(+), 61 deletions(-) diff --git a/esphome/components/esp32_camera/__init__.py b/esphome/components/esp32_camera/__init__.py index db6244fb3f..3a5d87792b 100644 --- a/esphome/components/esp32_camera/__init__.py +++ b/esphome/components/esp32_camera/__init__.py @@ -22,8 +22,10 @@ from esphome.const import ( CONF_TRIGGER_ID, CONF_VSYNC_PIN, ) +from esphome.core import CORE from esphome.core.entity_helpers import setup_entity import esphome.final_validate as fv +from esphome.types import ConfigType _LOGGER = logging.getLogger(__name__) @@ -84,6 +86,18 @@ FRAME_SIZES = { "2560X1920": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_2560X1920, "QSXGA": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_2560X1920, } +ESP32CameraPixelFormat = esp32_camera_ns.enum("ESP32CameraPixelFormat") +PIXEL_FORMATS = { + "RGB565": ESP32CameraPixelFormat.ESP32_PIXEL_FORMAT_RGB565, + "YUV422": ESP32CameraPixelFormat.ESP32_PIXEL_FORMAT_YUV422, + "YUV420": ESP32CameraPixelFormat.ESP32_PIXEL_FORMAT_YUV420, + "GRAYSCALE": ESP32CameraPixelFormat.ESP32_PIXEL_FORMAT_GRAYSCALE, + "JPEG": ESP32CameraPixelFormat.ESP32_PIXEL_FORMAT_JPEG, + "RGB888": ESP32CameraPixelFormat.ESP32_PIXEL_FORMAT_RGB888, + "RAW": ESP32CameraPixelFormat.ESP32_PIXEL_FORMAT_RAW, + "RGB444": ESP32CameraPixelFormat.ESP32_PIXEL_FORMAT_RGB444, + "RGB555": ESP32CameraPixelFormat.ESP32_PIXEL_FORMAT_RGB555, +} ESP32GainControlMode = esp32_camera_ns.enum("ESP32GainControlMode") ENUM_GAIN_CONTROL_MODE = { "MANUAL": ESP32GainControlMode.ESP32_GC_MODE_MANU, @@ -131,6 +145,7 @@ CONF_EXTERNAL_CLOCK = "external_clock" CONF_I2C_PINS = "i2c_pins" CONF_POWER_DOWN_PIN = "power_down_pin" # image +CONF_PIXEL_FORMAT = "pixel_format" CONF_JPEG_QUALITY = "jpeg_quality" CONF_VERTICAL_FLIP = "vertical_flip" CONF_HORIZONTAL_MIRROR = "horizontal_mirror" @@ -171,6 +186,21 @@ def validate_fb_location_(value): return validator(value) +def validate_jpeg_quality(config: ConfigType) -> ConfigType: + quality = config.get(CONF_JPEG_QUALITY) + pixel_format = config.get(CONF_PIXEL_FORMAT, "JPEG") + + if quality == 0: + # Set default JPEG quality if not specified for backwards compatibility + if pixel_format == "JPEG": + config[CONF_JPEG_QUALITY] = 10 + # For pixel formats other than JPEG, the valid 0 means no conversion + elif quality < 6 or quality > 63: + raise cv.Invalid(f"jpeg_quality must be between 6 and 63, got {quality}") + + return config + + CONFIG_SCHEMA = cv.All( cv.ENTITY_BASE_SCHEMA.extend( { @@ -206,7 +236,12 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_RESOLUTION, default="640X480"): cv.enum( FRAME_SIZES, upper=True ), - cv.Optional(CONF_JPEG_QUALITY, default=10): cv.int_range(min=6, max=63), + cv.Optional(CONF_PIXEL_FORMAT, default="JPEG"): cv.enum( + PIXEL_FORMATS, upper=True + ), + cv.Optional(CONF_JPEG_QUALITY, default=0): cv.Any( + cv.one_of(0), cv.int_range(min=6, max=63) + ), cv.Optional(CONF_CONTRAST, default=0): camera_range_param, cv.Optional(CONF_BRIGHTNESS, default=0): camera_range_param, cv.Optional(CONF_SATURATION, default=0): camera_range_param, @@ -270,11 +305,21 @@ CONFIG_SCHEMA = cv.All( ), } ).extend(cv.COMPONENT_SCHEMA), + validate_jpeg_quality, cv.has_exactly_one_key(CONF_I2C_PINS, CONF_I2C_ID), ) def _final_validate(config): + # Check psram requirement for non-JPEG formats + if ( + config.get(CONF_PIXEL_FORMAT, "JPEG") != "JPEG" + and psram_domain not in CORE.loaded_integrations + ): + raise cv.Invalid( + f"Non-JPEG pixel formats require the '{psram_domain}' component for JPEG conversion" + ) + if CONF_I2C_PINS not in config: return fconf = fv.full_config.get() @@ -298,6 +343,7 @@ SETTERS = { CONF_RESET_PIN: "set_reset_pin", CONF_POWER_DOWN_PIN: "set_power_down_pin", # image + CONF_PIXEL_FORMAT: "set_pixel_format", CONF_JPEG_QUALITY: "set_jpeg_quality", CONF_VERTICAL_FLIP: "set_vertical_flip", CONF_HORIZONTAL_MIRROR: "set_horizontal_mirror", @@ -351,6 +397,8 @@ async def to_code(config): cg.add(var.set_frame_size(config[CONF_RESOLUTION])) cg.add_define("USE_CAMERA") + if config[CONF_JPEG_QUALITY] != 0 and config[CONF_PIXEL_FORMAT] != "JPEG": + cg.add_define("USE_ESP32_CAMERA_JPEG_CONVERSION") add_idf_component(name="espressif/esp32-camera", ref="2.1.1") add_idf_sdkconfig_option("CONFIG_SCCB_HARDWARE_I2C_DRIVER_NEW", True) diff --git a/esphome/components/esp32_camera/esp32_camera.cpp b/esphome/components/esp32_camera/esp32_camera.cpp index cfe06b1673..655ae54f0a 100644 --- a/esphome/components/esp32_camera/esp32_camera.cpp +++ b/esphome/components/esp32_camera/esp32_camera.cpp @@ -16,6 +16,74 @@ static constexpr size_t FRAMEBUFFER_TASK_STACK_SIZE = 1792; static constexpr uint32_t FRAME_LOG_INTERVAL_MS = 60000; #endif +static const char *frame_size_to_str(framesize_t size) { + switch (size) { + case FRAMESIZE_QQVGA: + return "160x120 (QQVGA)"; + case FRAMESIZE_QCIF: + return "176x155 (QCIF)"; + case FRAMESIZE_HQVGA: + return "240x176 (HQVGA)"; + case FRAMESIZE_QVGA: + return "320x240 (QVGA)"; + case FRAMESIZE_CIF: + return "400x296 (CIF)"; + case FRAMESIZE_VGA: + return "640x480 (VGA)"; + case FRAMESIZE_SVGA: + return "800x600 (SVGA)"; + case FRAMESIZE_XGA: + return "1024x768 (XGA)"; + case FRAMESIZE_SXGA: + return "1280x1024 (SXGA)"; + case FRAMESIZE_UXGA: + return "1600x1200 (UXGA)"; + case FRAMESIZE_FHD: + return "1920x1080 (FHD)"; + case FRAMESIZE_P_HD: + return "720x1280 (P_HD)"; + case FRAMESIZE_P_3MP: + return "864x1536 (P_3MP)"; + case FRAMESIZE_QXGA: + return "2048x1536 (QXGA)"; + case FRAMESIZE_QHD: + return "2560x1440 (QHD)"; + case FRAMESIZE_WQXGA: + return "2560x1600 (WQXGA)"; + case FRAMESIZE_P_FHD: + return "1080x1920 (P_FHD)"; + case FRAMESIZE_QSXGA: + return "2560x1920 (QSXGA)"; + default: + return "UNKNOWN"; + } +} + +static const char *pixel_format_to_str(pixformat_t format) { + switch (format) { + case PIXFORMAT_RGB565: + return "RGB565"; + case PIXFORMAT_YUV422: + return "YUV422"; + case PIXFORMAT_YUV420: + return "YUV420"; + case PIXFORMAT_GRAYSCALE: + return "GRAYSCALE"; + case PIXFORMAT_JPEG: + return "JPEG"; + case PIXFORMAT_RGB888: + return "RGB888"; + case PIXFORMAT_RAW: + return "RAW"; + case PIXFORMAT_RGB444: + return "RGB444"; + case PIXFORMAT_RGB555: + return "RGB555"; + default: + return "UNKNOWN"; + } +} + /* ---------------- public API (derivated) ---------------- */ void ESP32Camera::setup() { #ifdef USE_I2C @@ -68,64 +136,9 @@ void ESP32Camera::dump_config() { this->name_.c_str(), YESNO(this->is_internal()), conf.pin_d0, conf.pin_d1, conf.pin_d2, conf.pin_d3, conf.pin_d4, conf.pin_d5, conf.pin_d6, conf.pin_d7, conf.pin_vsync, conf.pin_href, conf.pin_pclk, conf.pin_xclk, conf.xclk_freq_hz, conf.pin_sccb_sda, conf.pin_sccb_scl, conf.pin_reset); - switch (this->config_.frame_size) { - case FRAMESIZE_QQVGA: - ESP_LOGCONFIG(TAG, " Resolution: 160x120 (QQVGA)"); - break; - case FRAMESIZE_QCIF: - ESP_LOGCONFIG(TAG, " Resolution: 176x155 (QCIF)"); - break; - case FRAMESIZE_HQVGA: - ESP_LOGCONFIG(TAG, " Resolution: 240x176 (HQVGA)"); - break; - case FRAMESIZE_QVGA: - ESP_LOGCONFIG(TAG, " Resolution: 320x240 (QVGA)"); - break; - case FRAMESIZE_CIF: - ESP_LOGCONFIG(TAG, " Resolution: 400x296 (CIF)"); - break; - case FRAMESIZE_VGA: - ESP_LOGCONFIG(TAG, " Resolution: 640x480 (VGA)"); - break; - case FRAMESIZE_SVGA: - ESP_LOGCONFIG(TAG, " Resolution: 800x600 (SVGA)"); - break; - case FRAMESIZE_XGA: - ESP_LOGCONFIG(TAG, " Resolution: 1024x768 (XGA)"); - break; - case FRAMESIZE_SXGA: - ESP_LOGCONFIG(TAG, " Resolution: 1280x1024 (SXGA)"); - break; - case FRAMESIZE_UXGA: - ESP_LOGCONFIG(TAG, " Resolution: 1600x1200 (UXGA)"); - break; - case FRAMESIZE_FHD: - ESP_LOGCONFIG(TAG, " Resolution: 1920x1080 (FHD)"); - break; - case FRAMESIZE_P_HD: - ESP_LOGCONFIG(TAG, " Resolution: 720x1280 (P_HD)"); - break; - case FRAMESIZE_P_3MP: - ESP_LOGCONFIG(TAG, " Resolution: 864x1536 (P_3MP)"); - break; - case FRAMESIZE_QXGA: - ESP_LOGCONFIG(TAG, " Resolution: 2048x1536 (QXGA)"); - break; - case FRAMESIZE_QHD: - ESP_LOGCONFIG(TAG, " Resolution: 2560x1440 (QHD)"); - break; - case FRAMESIZE_WQXGA: - ESP_LOGCONFIG(TAG, " Resolution: 2560x1600 (WQXGA)"); - break; - case FRAMESIZE_P_FHD: - ESP_LOGCONFIG(TAG, " Resolution: 1080x1920 (P_FHD)"); - break; - case FRAMESIZE_QSXGA: - ESP_LOGCONFIG(TAG, " Resolution: 2560x1920 (QSXGA)"); - break; - default: - break; - } + + ESP_LOGCONFIG(TAG, " Resolution: %s", frame_size_to_str(this->config_.frame_size)); + ESP_LOGCONFIG(TAG, " Pixel Format: %s", pixel_format_to_str(this->config_.pixel_format)); if (this->is_failed()) { ESP_LOGE(TAG, " Setup Failed: %s", esp_err_to_name(this->init_error_)); @@ -184,8 +197,19 @@ void ESP32Camera::loop() { // check if we can return the image if (this->can_return_image_()) { // return image - auto *fb = this->current_image_->get_raw_buffer(); - xQueueSend(this->framebuffer_return_queue_, &fb, portMAX_DELAY); +#ifdef USE_ESP32_CAMERA_JPEG_CONVERSION + if (this->config_.pixel_format != PIXFORMAT_JPEG && this->config_.jpeg_quality > 0) { + // for non-JPEG format, we need to free the data and raw buffer + auto *jpg_buf = this->current_image_->get_data_buffer(); + free(jpg_buf); // NOLINT(cppcoreguidelines-no-malloc) + auto *fb = this->current_image_->get_raw_buffer(); + this->fb_allocator_.deallocate(fb, 1); + } else +#endif + { + auto *fb = this->current_image_->get_raw_buffer(); + xQueueSend(this->framebuffer_return_queue_, &fb, portMAX_DELAY); + } this->current_image_.reset(); } @@ -212,6 +236,38 @@ void ESP32Camera::loop() { xQueueSend(this->framebuffer_return_queue_, &fb, portMAX_DELAY); return; } + +#ifdef USE_ESP32_CAMERA_JPEG_CONVERSION + if (this->config_.pixel_format != PIXFORMAT_JPEG && this->config_.jpeg_quality > 0) { + // for non-JPEG format, we need to convert the frame to JPEG + uint8_t *jpg_buf; + size_t jpg_buf_len; + size_t width = fb->width; + size_t height = fb->height; + struct timeval timestamp = fb->timestamp; + bool ok = frame2jpg(fb, 100 - this->config_.jpeg_quality, &jpg_buf, &jpg_buf_len); + // return the original frame buffer to the queue + xQueueSend(this->framebuffer_return_queue_, &fb, portMAX_DELAY); + if (!ok) { + ESP_LOGE(TAG, "Failed to convert frame to JPEG!"); + return; + } + // create a new camera_fb_t for the JPEG data + fb = this->fb_allocator_.allocate(1); + if (fb == nullptr) { + ESP_LOGE(TAG, "Failed to allocate memory for camera frame buffer!"); + free(jpg_buf); // NOLINT(cppcoreguidelines-no-malloc) + return; + } + memset(fb, 0, sizeof(camera_fb_t)); + fb->buf = jpg_buf; + fb->len = jpg_buf_len; + fb->width = width; + fb->height = height; + fb->format = PIXFORMAT_JPEG; + fb->timestamp = timestamp; + } +#endif this->current_image_ = std::make_shared(fb, this->single_requesters_ | this->stream_requesters_); #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE @@ -342,6 +398,37 @@ void ESP32Camera::set_frame_size(ESP32CameraFrameSize size) { break; } } +void ESP32Camera::set_pixel_format(ESP32CameraPixelFormat format) { + switch (format) { + case ESP32_PIXEL_FORMAT_RGB565: + this->config_.pixel_format = PIXFORMAT_RGB565; + break; + case ESP32_PIXEL_FORMAT_YUV422: + this->config_.pixel_format = PIXFORMAT_YUV422; + break; + case ESP32_PIXEL_FORMAT_YUV420: + this->config_.pixel_format = PIXFORMAT_YUV420; + break; + case ESP32_PIXEL_FORMAT_GRAYSCALE: + this->config_.pixel_format = PIXFORMAT_GRAYSCALE; + break; + case ESP32_PIXEL_FORMAT_JPEG: + this->config_.pixel_format = PIXFORMAT_JPEG; + break; + case ESP32_PIXEL_FORMAT_RGB888: + this->config_.pixel_format = PIXFORMAT_RGB888; + break; + case ESP32_PIXEL_FORMAT_RAW: + this->config_.pixel_format = PIXFORMAT_RAW; + break; + case ESP32_PIXEL_FORMAT_RGB444: + this->config_.pixel_format = PIXFORMAT_RGB444; + break; + case ESP32_PIXEL_FORMAT_RGB555: + this->config_.pixel_format = PIXFORMAT_RGB555; + break; + } +} void ESP32Camera::set_jpeg_quality(uint8_t quality) { this->config_.jpeg_quality = quality; } void ESP32Camera::set_vertical_flip(bool vertical_flip) { this->vertical_flip_ = vertical_flip; } void ESP32Camera::set_horizontal_mirror(bool horizontal_mirror) { this->horizontal_mirror_ = horizontal_mirror; } diff --git a/esphome/components/esp32_camera/esp32_camera.h b/esphome/components/esp32_camera/esp32_camera.h index eea93b7e01..9fbd3848f2 100644 --- a/esphome/components/esp32_camera/esp32_camera.h +++ b/esphome/components/esp32_camera/esp32_camera.h @@ -41,6 +41,18 @@ enum ESP32CameraFrameSize { ESP32_CAMERA_SIZE_2560X1920, // QSXGA }; +enum ESP32CameraPixelFormat { + ESP32_PIXEL_FORMAT_RGB565, + ESP32_PIXEL_FORMAT_YUV422, + ESP32_PIXEL_FORMAT_YUV420, + ESP32_PIXEL_FORMAT_GRAYSCALE, + ESP32_PIXEL_FORMAT_JPEG, + ESP32_PIXEL_FORMAT_RGB888, + ESP32_PIXEL_FORMAT_RAW, + ESP32_PIXEL_FORMAT_RGB444, + ESP32_PIXEL_FORMAT_RGB555, +}; + enum ESP32AgcGainCeiling { ESP32_GAINCEILING_2X = GAINCEILING_2X, ESP32_GAINCEILING_4X = GAINCEILING_4X, @@ -126,6 +138,7 @@ class ESP32Camera : public camera::Camera { void set_reset_pin(uint8_t pin); void set_power_down_pin(uint8_t pin); /* -- image */ + void set_pixel_format(ESP32CameraPixelFormat format); void set_frame_size(ESP32CameraFrameSize size); void set_jpeg_quality(uint8_t quality); void set_vertical_flip(bool vertical_flip); @@ -220,6 +233,7 @@ class ESP32Camera : public camera::Camera { #ifdef USE_I2C i2c::InternalI2CBus *i2c_bus_{nullptr}; #endif // USE_I2C + RAMAllocator fb_allocator_{RAMAllocator::ALLOC_INTERNAL}; }; class ESP32CameraImageTrigger : public Trigger, public camera::CameraListener { diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 5559ec88d0..a7fb9f197c 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -43,6 +43,7 @@ #define USE_DEVICES #define USE_DISPLAY #define USE_ENTITY_ICON +#define USE_ESP32_CAMERA_JPEG_CONVERSION #define USE_ESP32_HOSTED #define USE_ESP32_IMPROV_STATE_CALLBACK #define USE_EVENT diff --git a/tests/test_build_components/common/i2c_camera/esp32-idf.yaml b/tests/test_build_components/common/i2c_camera/esp32-idf.yaml index 443ebbebd9..07ab6cdc8d 100644 --- a/tests/test_build_components/common/i2c_camera/esp32-idf.yaml +++ b/tests/test_build_components/common/i2c_camera/esp32-idf.yaml @@ -30,6 +30,7 @@ esp32_camera: resolution: 640x480 jpeg_quality: 10 frame_buffer_location: PSRAM + pixel_format: JPEG on_image: then: - lambda: |- From 4cc1e6a9103dc804e9e4516ff1f6fe313701ccf1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Mart=C3=ADn?= Date: Thu, 19 Feb 2026 15:23:22 +0100 Subject: [PATCH 092/197] [esp32_ble_server] add test for lambda characteristic (#14091) --- tests/components/esp32_ble_server/common.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/components/esp32_ble_server/common.yaml b/tests/components/esp32_ble_server/common.yaml index e9576a8262..7fe0b2eb5f 100644 --- a/tests/components/esp32_ble_server/common.yaml +++ b/tests/components/esp32_ble_server/common.yaml @@ -33,6 +33,10 @@ esp32_ble_server: - uuid: 2a24b789-7a1b-4535-af3e-ee76a35cc42d advertise: false characteristics: + - id: test_lambda_characteristic + uuid: 2a24b789-7a1b-4535-af3e-ee76a35cc12c + read: true + value: !lambda return { 1, 2 }; - id: test_change_characteristic uuid: 2a24b789-7a1b-4535-af3e-ee76a35cc11c read: true From 7b53a9895000219aac5895de48c5bd15715d7aea Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 19 Feb 2026 08:39:44 -0600 Subject: [PATCH 093/197] [socket] Log error when UDP socket requested on LWIP TCP-only platforms (#14089) --- esphome/components/socket/lwip_raw_tcp_impl.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/esphome/components/socket/lwip_raw_tcp_impl.cpp b/esphome/components/socket/lwip_raw_tcp_impl.cpp index aa37386d70..6e95f5bc7a 100644 --- a/esphome/components/socket/lwip_raw_tcp_impl.cpp +++ b/esphome/components/socket/lwip_raw_tcp_impl.cpp @@ -680,6 +680,11 @@ class LWIPRawListenImpl final : public LWIPRawImpl { }; std::unique_ptr socket(int domain, int type, int protocol) { + if (type != SOCK_STREAM) { + ESP_LOGE(TAG, "UDP sockets not supported on this platform, use WiFiUDP"); + errno = EPROTOTYPE; + return nullptr; + } auto *pcb = tcp_new(); if (pcb == nullptr) return nullptr; From 6daca09794d7d48478e4c6751907ae5a4b5aa7b3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 19 Feb 2026 08:40:08 -0600 Subject: [PATCH 094/197] [logger] Replace LogListener virtual interface with LogCallback struct (#14084) --- esphome/components/api/api_server.cpp | 5 +- esphome/components/api/api_server.h | 6 +- esphome/components/ble_nus/ble_nus.cpp | 5 +- esphome/components/ble_nus/ble_nus.h | 9 +-- esphome/components/logger/logger.h | 66 ++++++++++---------- esphome/components/mqtt/mqtt_client.cpp | 5 +- esphome/components/mqtt/mqtt_client.h | 9 +-- esphome/components/syslog/esphome_syslog.cpp | 7 ++- esphome/components/syslog/esphome_syslog.h | 5 +- esphome/components/web_server/web_server.cpp | 5 +- esphome/components/web_server/web_server.h | 11 +--- 11 files changed, 65 insertions(+), 68 deletions(-) diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 67a117e68f..1a1d0b229b 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -92,7 +92,10 @@ void APIServer::setup() { #ifdef USE_LOGGER if (logger::global_logger != nullptr) { - logger::global_logger->add_log_listener(this); + logger::global_logger->add_log_callback( + this, [](void *self, uint8_t level, const char *tag, const char *message, size_t message_len) { + static_cast(self)->on_log(level, tag, message, message_len); + }); } #endif diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index 323acc2efb..3b9ba0e23b 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -37,10 +37,6 @@ struct SavedNoisePsk { class APIServer : public Component, public Controller -#ifdef USE_LOGGER - , - public logger::LogListener -#endif #ifdef USE_CAMERA , public camera::CameraListener @@ -56,7 +52,7 @@ class APIServer : public Component, void on_shutdown() override; bool teardown() override; #ifdef USE_LOGGER - void on_log(uint8_t level, const char *tag, const char *message, size_t message_len) override; + void on_log(uint8_t level, const char *tag, const char *message, size_t message_len); #endif #ifdef USE_CAMERA void on_camera_image(const std::shared_ptr &image) override; diff --git a/esphome/components/ble_nus/ble_nus.cpp b/esphome/components/ble_nus/ble_nus.cpp index 0de65b623f..a10132eb3e 100644 --- a/esphome/components/ble_nus/ble_nus.cpp +++ b/esphome/components/ble_nus/ble_nus.cpp @@ -87,7 +87,10 @@ void BLENUS::setup() { global_ble_nus = this; #ifdef USE_LOGGER if (logger::global_logger != nullptr && this->expose_log_) { - logger::global_logger->add_log_listener(this); + logger::global_logger->add_log_callback( + this, [](void *self, uint8_t level, const char *tag, const char *message, size_t message_len) { + static_cast(self)->on_log(level, tag, message, message_len); + }); } #endif } diff --git a/esphome/components/ble_nus/ble_nus.h b/esphome/components/ble_nus/ble_nus.h index ef20fc5e5b..b2b0ee7713 100644 --- a/esphome/components/ble_nus/ble_nus.h +++ b/esphome/components/ble_nus/ble_nus.h @@ -10,12 +10,7 @@ namespace esphome::ble_nus { -class BLENUS : public Component -#ifdef USE_LOGGER - , - public logger::LogListener -#endif -{ +class BLENUS : public Component { enum TxStatus { TX_DISABLED, TX_ENABLED, @@ -29,7 +24,7 @@ class BLENUS : public Component size_t write_array(const uint8_t *data, size_t len); void set_expose_log(bool expose_log) { this->expose_log_ = expose_log; } #ifdef USE_LOGGER - void on_log(uint8_t level, const char *tag, const char *message, size_t message_len) override; + void on_log(uint8_t level, const char *tag, const char *message, size_t message_len); #endif protected: diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index 835542dd8f..2a7552af92 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -40,26 +40,25 @@ struct device; namespace esphome::logger { -/** Interface for receiving log messages without std::function overhead. +/** Lightweight callback for receiving log messages without virtual dispatch overhead. * - * Components can implement this interface instead of using lambdas with std::function - * to reduce flash usage from std::function type erasure machinery. + * Replaces the former LogListener virtual interface to eliminate per-implementer + * vtable sub-tables and thunk code (~39 bytes saved per class that used LogListener). * * Usage: - * class MyComponent : public Component, public LogListener { - * public: - * void setup() override { - * if (logger::global_logger != nullptr) - * logger::global_logger->add_log_listener(this); - * } - * void on_log(uint8_t level, const char *tag, const char *message, size_t message_len) override { - * // Handle log message - * } - * }; + * // In your component's setup(): + * if (logger::global_logger != nullptr) + * logger::global_logger->add_log_callback( + * this, [](void *self, uint8_t level, const char *tag, const char *message, size_t message_len) { + * static_cast(self)->on_log(level, tag, message, message_len); + * }); */ -class LogListener { - public: - virtual void on_log(uint8_t level, const char *tag, const char *message, size_t message_len) = 0; +struct LogCallback { + void *instance; + void (*fn)(void *, uint8_t, const char *, const char *, size_t); + void invoke(uint8_t level, const char *tag, const char *message, size_t message_len) const { + this->fn(this->instance, level, tag, message, message_len); + } }; #ifdef USE_LOGGER_LEVEL_LISTENERS @@ -187,11 +186,13 @@ class Logger : public Component { inline uint8_t level_for(const char *tag); #ifdef USE_LOG_LISTENERS - /// Register a log listener to receive log messages - void add_log_listener(LogListener *listener) { this->log_listeners_.push_back(listener); } + /// Register a log callback to receive log messages + void add_log_callback(void *instance, void (*fn)(void *, uint8_t, const char *, const char *, size_t)) { + this->log_callbacks_.push_back(LogCallback{instance, fn}); + } #else /// No-op when log listeners are disabled - void add_log_listener(LogListener *listener) {} + void add_log_callback(void *instance, void (*fn)(void *, uint8_t, const char *, const char *, size_t)) {} #endif #ifdef USE_LOGGER_LEVEL_LISTENERS @@ -253,11 +254,11 @@ class Logger : public Component { } #endif - // Helper to notify log listeners + // Helper to notify log callbacks inline void HOT notify_listeners_(uint8_t level, const char *tag, const LogBuffer &buf) { #ifdef USE_LOG_LISTENERS - for (auto *listener : this->log_listeners_) - listener->on_log(level, tag, buf.data, buf.pos); + for (auto &cb : this->log_callbacks_) + cb.invoke(level, tag, buf.data, buf.pos); #endif } @@ -341,8 +342,8 @@ class Logger : public Component { std::map log_levels_{}; #endif #ifdef USE_LOG_LISTENERS - StaticVector - log_listeners_; // Log message listeners (API, MQTT, syslog, etc.) + StaticVector + log_callbacks_; // Log message callbacks (API, MQTT, syslog, etc.) #endif #ifdef USE_LOGGER_LEVEL_LISTENERS std::vector level_listeners_; // Log level change listeners @@ -478,15 +479,16 @@ class Logger : public Component { }; extern Logger *global_logger; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -class LoggerMessageTrigger : public Trigger, public LogListener { +class LoggerMessageTrigger : public Trigger { public: - explicit LoggerMessageTrigger(Logger *parent, uint8_t level) : level_(level) { parent->add_log_listener(this); } - - void on_log(uint8_t level, const char *tag, const char *message, size_t message_len) override { - (void) message_len; - if (level <= this->level_) { - this->trigger(level, tag, message); - } + explicit LoggerMessageTrigger(Logger *parent, uint8_t level) : level_(level) { + parent->add_log_callback(this, + [](void *self, uint8_t level, const char *tag, const char *message, size_t message_len) { + auto *trigger = static_cast(self); + if (level <= trigger->level_) { + trigger->trigger(level, tag, message); + } + }); } protected: diff --git a/esphome/components/mqtt/mqtt_client.cpp b/esphome/components/mqtt/mqtt_client.cpp index 90b423c386..9905b4677e 100644 --- a/esphome/components/mqtt/mqtt_client.cpp +++ b/esphome/components/mqtt/mqtt_client.cpp @@ -64,7 +64,10 @@ void MQTTClientComponent::setup() { }); #ifdef USE_LOGGER if (this->is_log_message_enabled() && logger::global_logger != nullptr) { - logger::global_logger->add_log_listener(this); + logger::global_logger->add_log_callback( + this, [](void *self, uint8_t level, const char *tag, const char *message, size_t message_len) { + static_cast(self)->on_log(level, tag, message, message_len); + }); } #endif diff --git a/esphome/components/mqtt/mqtt_client.h b/esphome/components/mqtt/mqtt_client.h index 38bc0b4da3..21edd53eda 100644 --- a/esphome/components/mqtt/mqtt_client.h +++ b/esphome/components/mqtt/mqtt_client.h @@ -99,12 +99,7 @@ enum MQTTClientState { class MQTTComponent; -class MQTTClientComponent : public Component -#ifdef USE_LOGGER - , - public logger::LogListener -#endif -{ +class MQTTClientComponent : public Component { public: MQTTClientComponent(); @@ -252,7 +247,7 @@ class MQTTClientComponent : public Component float get_setup_priority() const override; #ifdef USE_LOGGER - void on_log(uint8_t level, const char *tag, const char *message, size_t message_len) override; + void on_log(uint8_t level, const char *tag, const char *message, size_t message_len); #endif void on_message(const std::string &topic, const std::string &payload); diff --git a/esphome/components/syslog/esphome_syslog.cpp b/esphome/components/syslog/esphome_syslog.cpp index 376de54db4..790d08ffa6 100644 --- a/esphome/components/syslog/esphome_syslog.cpp +++ b/esphome/components/syslog/esphome_syslog.cpp @@ -18,7 +18,12 @@ constexpr int LOG_LEVEL_TO_SYSLOG_SEVERITY[] = { 7 // VERY_VERBOSE }; -void Syslog::setup() { logger::global_logger->add_log_listener(this); } +void Syslog::setup() { + logger::global_logger->add_log_callback( + this, [](void *self, uint8_t level, const char *tag, const char *message, size_t message_len) { + static_cast(self)->on_log(level, tag, message, message_len); + }); +} void Syslog::on_log(uint8_t level, const char *tag, const char *message, size_t message_len) { this->log_(level, tag, message, message_len); diff --git a/esphome/components/syslog/esphome_syslog.h b/esphome/components/syslog/esphome_syslog.h index bde6ab5ed4..be4fa91436 100644 --- a/esphome/components/syslog/esphome_syslog.h +++ b/esphome/components/syslog/esphome_syslog.h @@ -2,17 +2,16 @@ #include "esphome/core/component.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" -#include "esphome/components/logger/logger.h" #include "esphome/components/udp/udp_component.h" #include "esphome/components/time/real_time_clock.h" #ifdef USE_NETWORK namespace esphome::syslog { -class Syslog : public Component, public Parented, public logger::LogListener { +class Syslog : public Component, public Parented { public: Syslog(int level, time::RealTimeClock *time) : log_level_(level), time_(time) {} void setup() override; - void on_log(uint8_t level, const char *tag, const char *message, size_t message_len) override; + void on_log(uint8_t level, const char *tag, const char *message, size_t message_len); void set_strip(bool strip) { this->strip_ = strip; } void set_facility(int facility) { this->facility_ = facility; } diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index e3d131f58e..a44a47379e 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -395,7 +395,10 @@ void WebServer::setup() { #ifdef USE_LOGGER if (logger::global_logger != nullptr && this->expose_log_) { - logger::global_logger->add_log_listener(this); + logger::global_logger->add_log_callback( + this, [](void *self, uint8_t level, const char *tag, const char *message, size_t message_len) { + static_cast(self)->on_log(level, tag, message, message_len); + }); } #endif diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index 026da763ea..6afe618b59 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -186,14 +186,7 @@ class DeferredUpdateEventSourceList : public std::list Date: Thu, 19 Feb 2026 08:40:23 -0600 Subject: [PATCH 095/197] [core] Devirtualize call_loop() and mark_failed() in Component (#14083) --- esphome/core/component.cpp | 12 +++++------- esphome/core/component.h | 4 ++-- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index ba0f1663b9..f283a69064 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -207,7 +207,7 @@ bool Component::cancel_retry(uint32_t id) { #pragma GCC diagnostic pop } -void Component::call_loop() { this->loop(); } +void Component::call_loop_() { this->loop(); } void Component::call_setup() { this->setup(); } void Component::call_dump_config() { this->dump_config(); @@ -258,11 +258,11 @@ void Component::call() { case COMPONENT_STATE_SETUP: // State setup: Call first loop and set state to loop this->set_component_state_(COMPONENT_STATE_LOOP); - this->call_loop(); + this->call_loop_(); break; case COMPONENT_STATE_LOOP: // State loop: Call loop - this->call_loop(); + this->call_loop_(); break; case COMPONENT_STATE_FAILED: // State failed: Do nothing @@ -497,16 +497,14 @@ void Component::set_setup_priority(float priority) { bool Component::has_overridden_loop() const { #if defined(USE_HOST) || defined(CLANG_TIDY) - bool loop_overridden = true; - bool call_loop_overridden = true; + return true; #else #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wpmf-conversions" bool loop_overridden = (void *) (this->*(&Component::loop)) != (void *) (&Component::loop); - bool call_loop_overridden = (void *) (this->*(&Component::call_loop)) != (void *) (&Component::call_loop); #pragma GCC diagnostic pop + return loop_overridden; #endif - return loop_overridden || call_loop_overridden; } PollingComponent::PollingComponent(uint32_t update_interval) : update_interval_(update_interval) {} diff --git a/esphome/core/component.h b/esphome/core/component.h index 6f7f77dbc1..d4dad3c9a6 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -165,7 +165,7 @@ class Component { * For example, i2c based components can check if the remote device is responding and otherwise * mark the component as failed. Eventually this will also enable smart status LEDs. */ - virtual void mark_failed(); + void mark_failed(); // Remove before 2026.6.0 ESPDEPRECATED("Use mark_failed(LOG_STR(\"static string literal\")) instead. Do NOT use .c_str() from temporary " @@ -286,7 +286,7 @@ class Component { protected: friend class Application; - virtual void call_loop(); + void call_loop_(); virtual void call_setup(); virtual void call_dump_config(); From 535980b9bd37ebf405a6305e34e980e3f16a5af6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 19 Feb 2026 08:40:41 -0600 Subject: [PATCH 096/197] [cse7761] Use constexpr for compile-time constants (#14081) --- esphome/components/cse7761/cse7761.cpp | 38 +++++++++++++------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/esphome/components/cse7761/cse7761.cpp b/esphome/components/cse7761/cse7761.cpp index 7c5ee833a4..f4966357d4 100644 --- a/esphome/components/cse7761/cse7761.cpp +++ b/esphome/components/cse7761/cse7761.cpp @@ -15,29 +15,29 @@ static const char *const TAG = "cse7761"; * https://github.com/arendst/Tasmota/blob/development/tasmota/xnrg_19_cse7761.ino \*********************************************************************************************/ -static const int CSE7761_UREF = 42563; // RmsUc -static const int CSE7761_IREF = 52241; // RmsIAC -static const int CSE7761_PREF = 44513; // PowerPAC +static constexpr int CSE7761_UREF = 42563; // RmsUc +static constexpr int CSE7761_IREF = 52241; // RmsIAC +static constexpr int CSE7761_PREF = 44513; // PowerPAC -static const uint8_t CSE7761_REG_SYSCON = 0x00; // (2) System Control Register (0x0A04) -static const uint8_t CSE7761_REG_EMUCON = 0x01; // (2) Metering control register (0x0000) -static const uint8_t CSE7761_REG_EMUCON2 = 0x13; // (2) Metering control register 2 (0x0001) -static const uint8_t CSE7761_REG_PULSE1SEL = 0x1D; // (2) Pin function output select register (0x3210) +static constexpr uint8_t CSE7761_REG_SYSCON = 0x00; // (2) System Control Register (0x0A04) +static constexpr uint8_t CSE7761_REG_EMUCON = 0x01; // (2) Metering control register (0x0000) +static constexpr uint8_t CSE7761_REG_EMUCON2 = 0x13; // (2) Metering control register 2 (0x0001) +static constexpr uint8_t CSE7761_REG_PULSE1SEL = 0x1D; // (2) Pin function output select register (0x3210) -static const uint8_t CSE7761_REG_RMSIA = 0x24; // (3) The effective value of channel A current (0x000000) -static const uint8_t CSE7761_REG_RMSIB = 0x25; // (3) The effective value of channel B current (0x000000) -static const uint8_t CSE7761_REG_RMSU = 0x26; // (3) Voltage RMS (0x000000) -static const uint8_t CSE7761_REG_POWERPA = 0x2C; // (4) Channel A active power, update rate 27.2Hz (0x00000000) -static const uint8_t CSE7761_REG_POWERPB = 0x2D; // (4) Channel B active power, update rate 27.2Hz (0x00000000) -static const uint8_t CSE7761_REG_SYSSTATUS = 0x43; // (1) System status register +static constexpr uint8_t CSE7761_REG_RMSIA = 0x24; // (3) The effective value of channel A current (0x000000) +static constexpr uint8_t CSE7761_REG_RMSIB = 0x25; // (3) The effective value of channel B current (0x000000) +static constexpr uint8_t CSE7761_REG_RMSU = 0x26; // (3) Voltage RMS (0x000000) +static constexpr uint8_t CSE7761_REG_POWERPA = 0x2C; // (4) Channel A active power, update rate 27.2Hz (0x00000000) +static constexpr uint8_t CSE7761_REG_POWERPB = 0x2D; // (4) Channel B active power, update rate 27.2Hz (0x00000000) +static constexpr uint8_t CSE7761_REG_SYSSTATUS = 0x43; // (1) System status register -static const uint8_t CSE7761_REG_COEFFCHKSUM = 0x6F; // (2) Coefficient checksum -static const uint8_t CSE7761_REG_RMSIAC = 0x70; // (2) Channel A effective current conversion coefficient +static constexpr uint8_t CSE7761_REG_COEFFCHKSUM = 0x6F; // (2) Coefficient checksum +static constexpr uint8_t CSE7761_REG_RMSIAC = 0x70; // (2) Channel A effective current conversion coefficient -static const uint8_t CSE7761_SPECIAL_COMMAND = 0xEA; // Start special command -static const uint8_t CSE7761_CMD_RESET = 0x96; // Reset command, after receiving the command, the chip resets -static const uint8_t CSE7761_CMD_CLOSE_WRITE = 0xDC; // Close write operation -static const uint8_t CSE7761_CMD_ENABLE_WRITE = 0xE5; // Enable write operation +static constexpr uint8_t CSE7761_SPECIAL_COMMAND = 0xEA; // Start special command +static constexpr uint8_t CSE7761_CMD_RESET = 0x96; // Reset command, after receiving the command, the chip resets +static constexpr uint8_t CSE7761_CMD_CLOSE_WRITE = 0xDC; // Close write operation +static constexpr uint8_t CSE7761_CMD_ENABLE_WRITE = 0xE5; // Enable write operation enum CSE7761 { RMS_IAC, RMS_IBC, RMS_UC, POWER_PAC, POWER_PBC, POWER_SC, ENERGY_AC, ENERGY_BC }; From 01a46f665f550085022bed7d994e043045f5553a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 19 Feb 2026 09:42:22 -0500 Subject: [PATCH 097/197] Bump esptool from 5.1.0 to 5.2.0 (#14058) Signed-off-by: dependabot[bot] 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 2e6268284a..be3445dceb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,7 +9,7 @@ tzlocal==5.3.1 # from time tzdata>=2021.1 # from time pyserial==3.5 platformio==6.1.19 -esptool==5.1.0 +esptool==5.2.0 click==8.1.7 esphome-dashboard==20260210.0 aioesphomeapi==44.0.0 From b5a8e1c94cfa2d82c2e9b9241ae62c1abd7dd3a7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 19 Feb 2026 09:06:46 -0600 Subject: [PATCH 098/197] [ci] Update lint message to recommend constexpr over static const (#14099) --- script/ci-custom.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/ci-custom.py b/script/ci-custom.py index 8c405b04ae..231f587068 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -301,7 +301,7 @@ def highlight(s): ], ) def lint_no_defines(fname, match): - s = highlight(f"static const uint8_t {match.group(1)} = {match.group(2)};") + s = highlight(f"static constexpr uint8_t {match.group(1)} = {match.group(2)};") return ( "#define macros for integer constants are not allowed, please use " f"{s} style instead (replace uint8_t with the appropriate " From 0484b2852dcf76161f5ae36d447e8d9e1cdad747 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 19 Feb 2026 09:27:05 -0600 Subject: [PATCH 099/197] [e131] Fix E1.31 on ESP8266 and RP2040 by restoring WiFiUDP support (#14086) --- esphome/components/e131/e131.cpp | 31 ++++++++++++++++++++++++- esphome/components/e131/e131.h | 8 +++++++ esphome/components/e131/e131_packet.cpp | 2 ++ 3 files changed, 40 insertions(+), 1 deletion(-) diff --git a/esphome/components/e131/e131.cpp b/esphome/components/e131/e131.cpp index 4187857901..941927122c 100644 --- a/esphome/components/e131/e131.cpp +++ b/esphome/components/e131/e131.cpp @@ -14,12 +14,17 @@ static const int PORT = 5568; E131Component::E131Component() {} E131Component::~E131Component() { +#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) if (this->socket_) { this->socket_->close(); } +#elif defined(USE_SOCKET_IMPL_LWIP_TCP) + this->udp_.stop(); +#endif } void E131Component::setup() { +#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) this->socket_ = socket::socket_ip(SOCK_DGRAM, IPPROTO_IP); int enable = 1; @@ -50,6 +55,13 @@ void E131Component::setup() { this->mark_failed(); return; } +#elif defined(USE_SOCKET_IMPL_LWIP_TCP) + if (!this->udp_.begin(PORT)) { + ESP_LOGW(TAG, "Cannot bind E1.31 to port %d.", PORT); + this->mark_failed(); + return; + } +#endif join_igmp_groups_(); } @@ -59,19 +71,36 @@ void E131Component::loop() { int universe = 0; uint8_t buf[1460]; +#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) ssize_t len = this->socket_->read(buf, sizeof(buf)); if (len == -1) { return; } if (!this->packet_(buf, (size_t) len, universe, packet)) { - ESP_LOGV(TAG, "Invalid packet received of size %zd.", len); + ESP_LOGV(TAG, "Invalid packet received of size %d.", (int) len); return; } if (!this->process_(universe, packet)) { ESP_LOGV(TAG, "Ignored packet for %d universe of size %d.", universe, packet.count); } +#elif defined(USE_SOCKET_IMPL_LWIP_TCP) + while (auto packet_size = this->udp_.parsePacket()) { + auto len = this->udp_.read(buf, sizeof(buf)); + if (len <= 0) + continue; + + if (!this->packet_(buf, (size_t) len, universe, packet)) { + ESP_LOGV(TAG, "Invalid packet received of size %d.", (int) len); + continue; + } + + if (!this->process_(universe, packet)) { + ESP_LOGV(TAG, "Ignored packet for %d universe of size %d.", universe, packet.count); + } + } +#endif } void E131Component::add_effect(E131AddressableLightEffect *light_effect) { diff --git a/esphome/components/e131/e131.h b/esphome/components/e131/e131.h index d4b272eae2..72da9ddebe 100644 --- a/esphome/components/e131/e131.h +++ b/esphome/components/e131/e131.h @@ -1,7 +1,11 @@ #pragma once #include "esphome/core/defines.h" #ifdef USE_NETWORK +#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) #include "esphome/components/socket/socket.h" +#elif defined(USE_SOCKET_IMPL_LWIP_TCP) +#include +#endif #include "esphome/core/component.h" #include @@ -45,7 +49,11 @@ class E131Component : public esphome::Component { void leave_(int universe); E131ListenMethod listen_method_{E131_MULTICAST}; +#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) std::unique_ptr socket_; +#elif defined(USE_SOCKET_IMPL_LWIP_TCP) + WiFiUDP udp_; +#endif std::vector light_effects_; std::map universe_consumers_; }; diff --git a/esphome/components/e131/e131_packet.cpp b/esphome/components/e131/e131_packet.cpp index ed081e5758..aa5c740454 100644 --- a/esphome/components/e131/e131_packet.cpp +++ b/esphome/components/e131/e131_packet.cpp @@ -62,8 +62,10 @@ const size_t E131_MIN_PACKET_SIZE = reinterpret_cast(&((E131RawPacket *) bool E131Component::join_igmp_groups_() { if (listen_method_ != E131_MULTICAST) return false; +#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) if (this->socket_ == nullptr) return false; +#endif for (auto universe : universe_consumers_) { if (!universe.second) From 916cf0d8b7f945bb6e3ee30702469f22013b09c6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 19 Feb 2026 09:28:00 -0600 Subject: [PATCH 100/197] [e131] Replace std::map with std::vector for universe tracking (#14087) --- esphome/components/e131/e131.h | 9 ++++-- esphome/components/e131/e131_packet.cpp | 39 ++++++++++++++++--------- 2 files changed, 33 insertions(+), 15 deletions(-) diff --git a/esphome/components/e131/e131.h b/esphome/components/e131/e131.h index 72da9ddebe..fee447b678 100644 --- a/esphome/components/e131/e131.h +++ b/esphome/components/e131/e131.h @@ -9,7 +9,6 @@ #include "esphome/core/component.h" #include -#include #include #include @@ -27,6 +26,11 @@ struct E131Packet { uint8_t values[E131_MAX_PROPERTY_VALUES_COUNT]; }; +struct UniverseConsumer { + uint16_t universe; + uint16_t consumers; +}; + class E131Component : public esphome::Component { public: E131Component(); @@ -45,6 +49,7 @@ class E131Component : public esphome::Component { bool packet_(const uint8_t *data, size_t len, int &universe, E131Packet &packet); bool process_(int universe, const E131Packet &packet); bool join_igmp_groups_(); + UniverseConsumer *find_universe_(int universe); void join_(int universe); void leave_(int universe); @@ -55,7 +60,7 @@ class E131Component : public esphome::Component { WiFiUDP udp_; #endif std::vector light_effects_; - std::map universe_consumers_; + std::vector universe_consumers_; }; } // namespace e131 diff --git a/esphome/components/e131/e131_packet.cpp b/esphome/components/e131/e131_packet.cpp index aa5c740454..b90e6d5c91 100644 --- a/esphome/components/e131/e131_packet.cpp +++ b/esphome/components/e131/e131_packet.cpp @@ -60,19 +60,19 @@ union E131RawPacket { const size_t E131_MIN_PACKET_SIZE = reinterpret_cast(&((E131RawPacket *) nullptr)->property_values[1]); bool E131Component::join_igmp_groups_() { - if (listen_method_ != E131_MULTICAST) + if (this->listen_method_ != E131_MULTICAST) return false; #if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) if (this->socket_ == nullptr) return false; #endif - for (auto universe : universe_consumers_) { - if (!universe.second) + for (auto &entry : this->universe_consumers_) { + if (!entry.consumers) continue; ip4_addr_t multicast_addr = - network::IPAddress(239, 255, ((universe.first >> 8) & 0xff), ((universe.first >> 0) & 0xff)); + network::IPAddress(239, 255, ((entry.universe >> 8) & 0xff), ((entry.universe >> 0) & 0xff)); err_t err; { @@ -81,34 +81,47 @@ bool E131Component::join_igmp_groups_() { } if (err) { - ESP_LOGW(TAG, "IGMP join for %d universe of E1.31 failed. Multicast might not work.", universe.first); + ESP_LOGW(TAG, "IGMP join for %d universe of E1.31 failed. Multicast might not work.", entry.universe); } } return true; } +UniverseConsumer *E131Component::find_universe_(int universe) { + for (auto &entry : this->universe_consumers_) { + if (entry.universe == universe) + return &entry; + } + return nullptr; +} + void E131Component::join_(int universe) { // store only latest received packet for the given universe - auto consumers = ++universe_consumers_[universe]; - - if (consumers > 1) { - return; // we already joined before + auto *consumer = this->find_universe_(universe); + if (consumer != nullptr) { + if (consumer->consumers++ > 0) { + return; // we already joined before + } + } else { + this->universe_consumers_.push_back({static_cast(universe), 1}); } - if (join_igmp_groups_()) { + if (this->join_igmp_groups_()) { ESP_LOGD(TAG, "Joined %d universe for E1.31.", universe); } } void E131Component::leave_(int universe) { - auto consumers = --universe_consumers_[universe]; + auto *consumer = this->find_universe_(universe); + if (consumer == nullptr) + return; - if (consumers > 0) { + if (--consumer->consumers > 0) { return; // we have other consumers of the given universe } - if (listen_method_ == E131_MULTICAST) { + if (this->listen_method_ == E131_MULTICAST) { ip4_addr_t multicast_addr = network::IPAddress(239, 255, ((universe >> 8) & 0xff), ((universe >> 0) & 0xff)); LwIPLock lock; From a8171da0033ce797ef59125a69b270cfe35e36eb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 19 Feb 2026 09:38:57 -0600 Subject: [PATCH 101/197] [web_server] Reduce set_json_id flash and stack usage (#14029) --- esphome/components/web_server/web_server.cpp | 46 ++++++++++---------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index a44a47379e..f352e5896c 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -374,7 +374,7 @@ std::string WebServer::get_config_json() { json::JsonBuilder builder; JsonObject root = builder.root(); - root[ESPHOME_F("title")] = App.get_friendly_name().empty() ? App.get_name() : App.get_friendly_name(); + root[ESPHOME_F("title")] = App.get_friendly_name().empty() ? App.get_name().c_str() : App.get_friendly_name().c_str(); char comment_buffer[ESPHOME_COMMENT_SIZE]; App.get_comment_string(comment_buffer); root[ESPHOME_F("comment")] = comment_buffer; @@ -513,21 +513,27 @@ static void set_json_id(JsonObject &root, EntityBase *obj, const char *prefix, J size_t device_len = device_name ? strlen(device_name) : 0; #endif - // Build id into stack buffer - ArduinoJson copies the string - // Format: {prefix}/{device?}/{name} + // Single stack buffer for both id formats - ArduinoJson copies the string before we overwrite // Buffer sizes use constants from entity_base.h validated in core/config.py // Note: Device name uses ESPHOME_FRIENDLY_NAME_MAX_LEN (sub-device max 120), not ESPHOME_DEVICE_NAME_MAX_LEN // (hostname) + // Without USE_DEVICES: legacy id ({prefix}-{object_id}) is the largest format + // With USE_DEVICES: name_id ({prefix}/{device}/{name}) is the largest format + static constexpr size_t LEGACY_ID_SIZE = ESPHOME_DOMAIN_MAX_LEN + 1 + OBJECT_ID_MAX_LEN; #ifdef USE_DEVICES static constexpr size_t ID_BUF_SIZE = - ESPHOME_DOMAIN_MAX_LEN + 1 + ESPHOME_FRIENDLY_NAME_MAX_LEN + 1 + ESPHOME_FRIENDLY_NAME_MAX_LEN + 1; + std::max(ESPHOME_DOMAIN_MAX_LEN + 1 + ESPHOME_FRIENDLY_NAME_MAX_LEN + 1 + ESPHOME_FRIENDLY_NAME_MAX_LEN + 1, + LEGACY_ID_SIZE); #else - static constexpr size_t ID_BUF_SIZE = ESPHOME_DOMAIN_MAX_LEN + 1 + ESPHOME_FRIENDLY_NAME_MAX_LEN + 1; + static constexpr size_t ID_BUF_SIZE = + std::max(ESPHOME_DOMAIN_MAX_LEN + 1 + ESPHOME_FRIENDLY_NAME_MAX_LEN + 1, LEGACY_ID_SIZE); #endif char id_buf[ID_BUF_SIZE]; - char *p = id_buf; - memcpy(p, prefix, prefix_len); - p += prefix_len; + memcpy(id_buf, prefix, prefix_len); // NOLINT(bugprone-not-null-terminated-result) + + // name_id: new format {prefix}/{device?}/{name} - frontend should prefer this + // Remove in 2026.8.0 when id switches to new format permanently + char *p = id_buf + prefix_len; *p++ = '/'; #ifdef USE_DEVICES if (device_name) { @@ -538,31 +544,25 @@ static void set_json_id(JsonObject &root, EntityBase *obj, const char *prefix, J #endif memcpy(p, name.c_str(), name_len); p[name_len] = '\0'; - - // name_id: new format {prefix}/{device?}/{name} - frontend should prefer this - // Remove in 2026.8.0 when id switches to new format permanently root[ESPHOME_F("name_id")] = id_buf; // id: old format {prefix}-{object_id} for backward compatibility - // Will switch to new format in 2026.8.0 - char legacy_buf[ESPHOME_DOMAIN_MAX_LEN + 1 + OBJECT_ID_MAX_LEN]; - char *lp = legacy_buf; - memcpy(lp, prefix, prefix_len); - lp += prefix_len; - *lp++ = '-'; - obj->write_object_id_to(lp, sizeof(legacy_buf) - (lp - legacy_buf)); - root[ESPHOME_F("id")] = legacy_buf; + // Will switch to new format in 2026.8.0 - reuses prefix already in id_buf + id_buf[prefix_len] = '-'; + obj->write_object_id_to(id_buf + prefix_len + 1, ID_BUF_SIZE - prefix_len - 1); + root[ESPHOME_F("id")] = id_buf; if (start_config == DETAIL_ALL) { root[ESPHOME_F("domain")] = prefix; - root[ESPHOME_F("name")] = name; + // Use .c_str() to avoid instantiating set template (saves ~24B) + root[ESPHOME_F("name")] = name.c_str(); #ifdef USE_DEVICES if (device_name) { root[ESPHOME_F("device")] = device_name; } #endif #ifdef USE_ENTITY_ICON - root[ESPHOME_F("icon")] = obj->get_icon_ref(); + root[ESPHOME_F("icon")] = obj->get_icon_ref().c_str(); #endif root[ESPHOME_F("entity_category")] = obj->get_entity_category(); bool is_disabled = obj->is_disabled_by_default(); @@ -632,7 +632,7 @@ std::string WebServer::sensor_json_(sensor::Sensor *obj, float value, JsonDetail if (start_config == DETAIL_ALL) { this->add_sorting_info_(root, obj); if (!uom_ref.empty()) - root[ESPHOME_F("uom")] = uom_ref; + root[ESPHOME_F("uom")] = uom_ref.c_str(); } return builder.serialize(); @@ -1147,7 +1147,7 @@ std::string WebServer::number_json_(number::Number *obj, float value, JsonDetail 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; + root[ESPHOME_F("uom")] = uom_ref.c_str(); this->add_sorting_info_(root, obj); } From 5304750215a7be803d87bb9f059d43952fe084f6 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 19 Feb 2026 11:00:34 -0500 Subject: [PATCH 102/197] [socket] Fix IPv6 compilation error on host platform (#14101) Co-authored-by: Claude Opus 4.6 --- esphome/components/socket/socket.cpp | 10 ++++++++-- tests/components/socket/common.yaml | 11 +++++++++++ tests/components/socket/test-ipv6.esp32-idf.yaml | 4 ++++ tests/components/socket/test-ipv6.host.yaml | 4 ++++ tests/components/socket/test.esp32-idf.yaml | 1 + tests/components/socket/test.host.yaml | 3 +++ 6 files changed, 31 insertions(+), 2 deletions(-) create mode 100644 tests/components/socket/common.yaml create mode 100644 tests/components/socket/test-ipv6.esp32-idf.yaml create mode 100644 tests/components/socket/test-ipv6.host.yaml create mode 100644 tests/components/socket/test.esp32-idf.yaml create mode 100644 tests/components/socket/test.host.yaml diff --git a/esphome/components/socket/socket.cpp b/esphome/components/socket/socket.cpp index 2fcc162ead..6154c497e0 100644 --- a/esphome/components/socket/socket.cpp +++ b/esphome/components/socket/socket.cpp @@ -59,8 +59,14 @@ size_t format_sockaddr_to(const struct sockaddr *addr_ptr, socklen_t len, std::s #if USE_NETWORK_IPV6 else if (addr_ptr->sa_family == AF_INET6 && len >= sizeof(sockaddr_in6)) { const auto *addr = reinterpret_cast(addr_ptr); -#ifndef USE_SOCKET_IMPL_LWIP_TCP - // Format IPv4-mapped IPv6 addresses as regular IPv4 (not supported on ESP8266 raw TCP) +#ifdef USE_HOST + // Format IPv4-mapped IPv6 addresses as regular IPv4 (POSIX layout, no LWIP union) + if (IN6_IS_ADDR_V4MAPPED(&addr->sin6_addr) && + esphome_inet_ntop4(&addr->sin6_addr.s6_addr[12], buf.data(), buf.size()) != nullptr) { + return strlen(buf.data()); + } +#elif !defined(USE_SOCKET_IMPL_LWIP_TCP) + // Format IPv4-mapped IPv6 addresses as regular IPv4 (LWIP layout) if (addr->sin6_addr.un.u32_addr[0] == 0 && addr->sin6_addr.un.u32_addr[1] == 0 && addr->sin6_addr.un.u32_addr[2] == htonl(0xFFFF) && esphome_inet_ntop4(&addr->sin6_addr.un.u32_addr[3], buf.data(), buf.size()) != nullptr) { diff --git a/tests/components/socket/common.yaml b/tests/components/socket/common.yaml new file mode 100644 index 0000000000..aaf49f1611 --- /dev/null +++ b/tests/components/socket/common.yaml @@ -0,0 +1,11 @@ +substitutions: + network_enable_ipv6: "false" + +socket: + +wifi: + ssid: MySSID + password: password1 + +network: + enable_ipv6: ${network_enable_ipv6} diff --git a/tests/components/socket/test-ipv6.esp32-idf.yaml b/tests/components/socket/test-ipv6.esp32-idf.yaml new file mode 100644 index 0000000000..da1324b17e --- /dev/null +++ b/tests/components/socket/test-ipv6.esp32-idf.yaml @@ -0,0 +1,4 @@ +substitutions: + network_enable_ipv6: "true" + +<<: !include common.yaml diff --git a/tests/components/socket/test-ipv6.host.yaml b/tests/components/socket/test-ipv6.host.yaml new file mode 100644 index 0000000000..fdd52c574e --- /dev/null +++ b/tests/components/socket/test-ipv6.host.yaml @@ -0,0 +1,4 @@ +socket: + +network: + enable_ipv6: true diff --git a/tests/components/socket/test.esp32-idf.yaml b/tests/components/socket/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/socket/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/socket/test.host.yaml b/tests/components/socket/test.host.yaml new file mode 100644 index 0000000000..e0c5d7cea3 --- /dev/null +++ b/tests/components/socket/test.host.yaml @@ -0,0 +1,3 @@ +socket: + +network: From f7459670d34b47f66210eaaca7586be33f84205c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 19 Feb 2026 10:10:22 -0600 Subject: [PATCH 103/197] [core] Optimize WarnIfComponentBlockingGuard::finish() hot path (#14040) Co-authored-by: Claude Opus 4.6 --- esphome/core/component.cpp | 38 ++++++++++++++++++-------------------- esphome/core/component.h | 7 ++++--- 2 files changed, 22 insertions(+), 23 deletions(-) diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index f283a69064..b458ea2a84 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -82,8 +82,8 @@ void store_component_error_message(const Component *component, const char *messa // setup_priority, component state, and status LED constants are now // constexpr in component.h -const uint16_t WARN_IF_BLOCKING_OVER_MS = 50U; ///< Initial blocking time allowed without warning -const uint16_t WARN_IF_BLOCKING_INCREMENT_MS = 10U; ///< How long the blocking time must be larger to warn again +static constexpr uint16_t WARN_IF_BLOCKING_INCREMENT_MS = + 10U; ///< How long the blocking time must be larger to warn again float Component::get_loop_priority() const { return 0.0f; } @@ -529,37 +529,35 @@ void PollingComponent::stop_poller() { uint32_t PollingComponent::get_update_interval() const { return this->update_interval_; } void PollingComponent::set_update_interval(uint32_t update_interval) { this->update_interval_ = update_interval; } -WarnIfComponentBlockingGuard::WarnIfComponentBlockingGuard(Component *component, uint32_t start_time) - : started_(start_time), component_(component) {} +static void __attribute__((noinline, cold)) warn_blocking(Component *component, uint32_t blocking_time) { + bool should_warn; + if (component != nullptr) { + should_warn = component->should_warn_of_blocking(blocking_time); + } else { + should_warn = true; // Already checked > WARN_IF_BLOCKING_OVER_MS in caller + } + if (should_warn) { + ESP_LOGW(TAG, "%s took a long time for an operation (%" PRIu32 " ms), max is 30 ms", + component == nullptr ? LOG_STR_LITERAL("") : LOG_STR_ARG(component->get_component_log_str()), + blocking_time); + } +} + uint32_t WarnIfComponentBlockingGuard::finish() { uint32_t curr_time = millis(); - uint32_t blocking_time = curr_time - this->started_; - #ifdef USE_RUNTIME_STATS // Record component runtime stats if (global_runtime_stats != nullptr) { global_runtime_stats->record_component_time(this->component_, blocking_time, curr_time); } #endif - bool should_warn; - if (this->component_ != nullptr) { - should_warn = this->component_->should_warn_of_blocking(blocking_time); - } else { - should_warn = blocking_time > WARN_IF_BLOCKING_OVER_MS; + if (blocking_time > WARN_IF_BLOCKING_OVER_MS) { + warn_blocking(this->component_, blocking_time); } - if (should_warn) { - ESP_LOGW(TAG, "%s took a long time for an operation (%" PRIu32 " ms)", - component_ == nullptr ? LOG_STR_LITERAL("") : LOG_STR_ARG(component_->get_component_log_str()), - blocking_time); - ESP_LOGW(TAG, "Components should block for at most 30 ms"); - } - return curr_time; } -WarnIfComponentBlockingGuard::~WarnIfComponentBlockingGuard() {} - #ifdef USE_SETUP_PRIORITY_OVERRIDE void clear_setup_priority_overrides() { // Free the setup priority map completely diff --git a/esphome/core/component.h b/esphome/core/component.h index d4dad3c9a6..b99641a275 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -79,7 +79,7 @@ inline constexpr uint8_t STATUS_LED_ERROR = 0x10; // Remove before 2026.8.0 enum class RetryResult { DONE, RETRY }; -extern const uint16_t WARN_IF_BLOCKING_OVER_MS; +inline constexpr uint16_t WARN_IF_BLOCKING_OVER_MS = 50U; class Component { public: @@ -550,12 +550,13 @@ class PollingComponent : public Component { class WarnIfComponentBlockingGuard { public: - WarnIfComponentBlockingGuard(Component *component, uint32_t start_time); + WarnIfComponentBlockingGuard(Component *component, uint32_t start_time) + : started_(start_time), component_(component) {} // Finish the timing operation and return the current time uint32_t finish(); - ~WarnIfComponentBlockingGuard(); + ~WarnIfComponentBlockingGuard() = default; protected: uint32_t started_; From b11ad26c4f70050d1656200234b0642760bdea68 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Thu, 19 Feb 2026 10:20:19 -0600 Subject: [PATCH 104/197] [audio] Support decoding audio directly from flash (#14098) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/audio/audio_decoder.cpp | 102 ++++++++++-------- esphome/components/audio/audio_decoder.h | 26 +++-- .../audio/audio_transfer_buffer.cpp | 17 ++- .../components/audio/audio_transfer_buffer.h | 67 +++++++++++- 4 files changed, 159 insertions(+), 53 deletions(-) diff --git a/esphome/components/audio/audio_decoder.cpp b/esphome/components/audio/audio_decoder.cpp index ee6d7d0a15..7794b5b0d3 100644 --- a/esphome/components/audio/audio_decoder.cpp +++ b/esphome/components/audio/audio_decoder.cpp @@ -15,8 +15,8 @@ static const uint32_t READ_WRITE_TIMEOUT_MS = 20; // Timeout for transferring a static const uint32_t MAX_POTENTIALLY_FAILED_COUNT = 10; -AudioDecoder::AudioDecoder(size_t input_buffer_size, size_t output_buffer_size) { - this->input_transfer_buffer_ = AudioSourceTransferBuffer::create(input_buffer_size); +AudioDecoder::AudioDecoder(size_t input_buffer_size, size_t output_buffer_size) + : input_buffer_size_(input_buffer_size) { this->output_transfer_buffer_ = AudioSinkTransferBuffer::create(output_buffer_size); } @@ -29,11 +29,20 @@ AudioDecoder::~AudioDecoder() { } esp_err_t AudioDecoder::add_source(std::weak_ptr &input_ring_buffer) { - if (this->input_transfer_buffer_ != nullptr) { - this->input_transfer_buffer_->set_source(input_ring_buffer); - return ESP_OK; + auto source = AudioSourceTransferBuffer::create(this->input_buffer_size_); + if (source == nullptr) { + return ESP_ERR_NO_MEM; } - return ESP_ERR_NO_MEM; + source->set_source(input_ring_buffer); + this->input_buffer_ = std::move(source); + return ESP_OK; +} + +esp_err_t AudioDecoder::add_source(const uint8_t *data_pointer, size_t length) { + auto source = make_unique(); + source->set_data(data_pointer, length); + this->input_buffer_ = std::move(source); + return ESP_OK; } esp_err_t AudioDecoder::add_sink(std::weak_ptr &output_ring_buffer) { @@ -54,8 +63,16 @@ esp_err_t AudioDecoder::add_sink(speaker::Speaker *speaker) { } #endif +esp_err_t AudioDecoder::add_sink(AudioSinkCallback *callback) { + if (this->output_transfer_buffer_ != nullptr) { + this->output_transfer_buffer_->set_sink(callback); + return ESP_OK; + } + return ESP_ERR_NO_MEM; +} + esp_err_t AudioDecoder::start(AudioFileType audio_file_type) { - if ((this->input_transfer_buffer_ == nullptr) || (this->output_transfer_buffer_ == nullptr)) { + if (this->output_transfer_buffer_ == nullptr) { return ESP_ERR_NO_MEM; } @@ -112,6 +129,10 @@ esp_err_t AudioDecoder::start(AudioFileType audio_file_type) { } AudioDecoderState AudioDecoder::decode(bool stop_gracefully) { + if (this->input_buffer_ == nullptr) { + return AudioDecoderState::FAILED; + } + if (stop_gracefully) { if (this->output_transfer_buffer_->available() == 0) { if (this->end_of_file_) { @@ -119,7 +140,7 @@ AudioDecoderState AudioDecoder::decode(bool stop_gracefully) { return AudioDecoderState::FINISHED; } - if (!this->input_transfer_buffer_->has_buffered_data()) { + if (!this->input_buffer_->has_buffered_data()) { // If all the internal buffers are empty, the decoding is done return AudioDecoderState::FINISHED; } @@ -170,10 +191,10 @@ AudioDecoderState AudioDecoder::decode(bool stop_gracefully) { // Only shift data on the first loop iteration to avoid unnecessary, slow moves // If the decoder buffers internally, then never shift - size_t bytes_read = this->input_transfer_buffer_->transfer_data_from_source( - pdMS_TO_TICKS(READ_WRITE_TIMEOUT_MS), first_loop_iteration && !this->decoder_buffers_internally_); + size_t bytes_read = this->input_buffer_->fill(pdMS_TO_TICKS(READ_WRITE_TIMEOUT_MS), + first_loop_iteration && !this->decoder_buffers_internally_); - if (!first_loop_iteration && (this->input_transfer_buffer_->available() < bytes_processed)) { + if (!first_loop_iteration && (this->input_buffer_->available() < bytes_processed)) { // Less data is available than what was processed in last iteration, so don't attempt to decode. // This attempts to avoid the decoder from consistently trying to decode an incomplete frame. The transfer buffer // will shift the remaining data to the start and copy more from the source the next time the decode function is @@ -181,19 +202,21 @@ AudioDecoderState AudioDecoder::decode(bool stop_gracefully) { break; } - bytes_available_before_processing = this->input_transfer_buffer_->available(); + bytes_available_before_processing = this->input_buffer_->available(); if ((this->potentially_failed_count_ > 0) && (bytes_read == 0)) { // Failed to decode in last attempt and there is no new data - if ((this->input_transfer_buffer_->free() == 0) && first_loop_iteration) { - // The input buffer is full. Since it previously failed on the exact same data, we can never recover + if ((this->input_buffer_->free() == 0) && first_loop_iteration) { + // The input buffer is full (or read-only, e.g. const flash source). Since it previously failed on the exact + // same data, we can never recover. For const sources this is correct: the entire file is already available, so + // a decode failure is genuine, not a transient out-of-data condition. state = FileDecoderState::FAILED; } else { // Attempt to get more data next time state = FileDecoderState::IDLE; } - } else if (this->input_transfer_buffer_->available() == 0) { + } else if (this->input_buffer_->available() == 0) { // No data to decode, attempt to get more data next time state = FileDecoderState::IDLE; } else { @@ -224,7 +247,7 @@ AudioDecoderState AudioDecoder::decode(bool stop_gracefully) { } first_loop_iteration = false; - bytes_processed = bytes_available_before_processing - this->input_transfer_buffer_->available(); + bytes_processed = bytes_available_before_processing - this->input_buffer_->available(); if (state == FileDecoderState::POTENTIALLY_FAILED) { ++this->potentially_failed_count_; @@ -243,8 +266,7 @@ AudioDecoderState AudioDecoder::decode(bool stop_gracefully) { FileDecoderState AudioDecoder::decode_flac_() { if (!this->audio_stream_info_.has_value()) { // Header hasn't been read - auto result = this->flac_decoder_->read_header(this->input_transfer_buffer_->get_buffer_start(), - this->input_transfer_buffer_->available()); + auto result = this->flac_decoder_->read_header(this->input_buffer_->data(), this->input_buffer_->available()); if (result > esp_audio_libs::flac::FLAC_DECODER_HEADER_OUT_OF_DATA) { // Serrious error reading FLAC header, there is no recovery @@ -252,7 +274,7 @@ FileDecoderState AudioDecoder::decode_flac_() { } size_t bytes_consumed = this->flac_decoder_->get_bytes_index(); - this->input_transfer_buffer_->decrease_buffer_length(bytes_consumed); + this->input_buffer_->consume(bytes_consumed); if (result == esp_audio_libs::flac::FLAC_DECODER_HEADER_OUT_OF_DATA) { return FileDecoderState::MORE_TO_PROCESS; @@ -273,8 +295,7 @@ FileDecoderState AudioDecoder::decode_flac_() { } uint32_t output_samples = 0; - auto result = this->flac_decoder_->decode_frame(this->input_transfer_buffer_->get_buffer_start(), - this->input_transfer_buffer_->available(), + auto result = this->flac_decoder_->decode_frame(this->input_buffer_->data(), this->input_buffer_->available(), this->output_transfer_buffer_->get_buffer_end(), &output_samples); if (result == esp_audio_libs::flac::FLAC_DECODER_ERROR_OUT_OF_DATA) { @@ -283,7 +304,7 @@ FileDecoderState AudioDecoder::decode_flac_() { } size_t bytes_consumed = this->flac_decoder_->get_bytes_index(); - this->input_transfer_buffer_->decrease_buffer_length(bytes_consumed); + this->input_buffer_->consume(bytes_consumed); if (result > esp_audio_libs::flac::FLAC_DECODER_ERROR_OUT_OF_DATA) { // Corrupted frame, don't retry with current buffer content, wait for new sync @@ -305,26 +326,25 @@ FileDecoderState AudioDecoder::decode_flac_() { #ifdef USE_AUDIO_MP3_SUPPORT FileDecoderState AudioDecoder::decode_mp3_() { // Look for the next sync word - int buffer_length = (int) this->input_transfer_buffer_->available(); - int32_t offset = - esp_audio_libs::helix_decoder::MP3FindSyncWord(this->input_transfer_buffer_->get_buffer_start(), buffer_length); + int buffer_length = (int) this->input_buffer_->available(); + int32_t offset = esp_audio_libs::helix_decoder::MP3FindSyncWord(this->input_buffer_->data(), buffer_length); if (offset < 0) { // New data may have the sync word - this->input_transfer_buffer_->decrease_buffer_length(buffer_length); + this->input_buffer_->consume(buffer_length); return FileDecoderState::POTENTIALLY_FAILED; } // Advance read pointer to match the offset for the syncword - this->input_transfer_buffer_->decrease_buffer_length(offset); - const uint8_t *buffer_start = this->input_transfer_buffer_->get_buffer_start(); + this->input_buffer_->consume(offset); + const uint8_t *buffer_start = this->input_buffer_->data(); - buffer_length = (int) this->input_transfer_buffer_->available(); + buffer_length = (int) this->input_buffer_->available(); int err = esp_audio_libs::helix_decoder::MP3Decode(this->mp3_decoder_, &buffer_start, &buffer_length, (int16_t *) this->output_transfer_buffer_->get_buffer_end(), 0); - size_t consumed = this->input_transfer_buffer_->available() - buffer_length; - this->input_transfer_buffer_->decrease_buffer_length(consumed); + size_t consumed = this->input_buffer_->available() - buffer_length; + this->input_buffer_->consume(consumed); if (err) { switch (err) { @@ -363,9 +383,8 @@ FileDecoderState AudioDecoder::decode_opus_() { size_t bytes_consumed, samples_decoded; micro_opus::OggOpusResult result = this->opus_decoder_->decode( - this->input_transfer_buffer_->get_buffer_start(), this->input_transfer_buffer_->available(), - this->output_transfer_buffer_->get_buffer_end(), this->output_transfer_buffer_->free(), bytes_consumed, - samples_decoded); + this->input_buffer_->data(), this->input_buffer_->available(), this->output_transfer_buffer_->get_buffer_end(), + this->output_transfer_buffer_->free(), bytes_consumed, samples_decoded); if (result == micro_opus::OGG_OPUS_OK) { if (!processed_header && this->opus_decoder_->is_initialized()) { @@ -379,7 +398,7 @@ FileDecoderState AudioDecoder::decode_opus_() { this->output_transfer_buffer_->increase_buffer_length( this->audio_stream_info_.value().frames_to_bytes(samples_decoded)); } - this->input_transfer_buffer_->decrease_buffer_length(bytes_consumed); + this->input_buffer_->consume(bytes_consumed); } else if (result == micro_opus::OGG_OPUS_OUTPUT_BUFFER_TOO_SMALL) { // Reallocate to decode the packet on the next call this->free_buffer_required_ = this->opus_decoder_->get_required_output_buffer_size(); @@ -399,11 +418,11 @@ FileDecoderState AudioDecoder::decode_wav_() { if (!this->audio_stream_info_.has_value()) { // Header hasn't been processed - esp_audio_libs::wav_decoder::WAVDecoderResult result = this->wav_decoder_->decode_header( - this->input_transfer_buffer_->get_buffer_start(), this->input_transfer_buffer_->available()); + esp_audio_libs::wav_decoder::WAVDecoderResult result = + this->wav_decoder_->decode_header(this->input_buffer_->data(), this->input_buffer_->available()); if (result == esp_audio_libs::wav_decoder::WAV_DECODER_SUCCESS_IN_DATA) { - this->input_transfer_buffer_->decrease_buffer_length(this->wav_decoder_->bytes_processed()); + this->input_buffer_->consume(this->wav_decoder_->bytes_processed()); this->audio_stream_info_ = audio::AudioStreamInfo( this->wav_decoder_->bits_per_sample(), this->wav_decoder_->num_channels(), this->wav_decoder_->sample_rate()); @@ -419,7 +438,7 @@ FileDecoderState AudioDecoder::decode_wav_() { } } else { if (!this->wav_has_known_end_ || (this->wav_bytes_left_ > 0)) { - size_t bytes_to_copy = this->input_transfer_buffer_->available(); + size_t bytes_to_copy = this->input_buffer_->available(); if (this->wav_has_known_end_) { bytes_to_copy = std::min(bytes_to_copy, this->wav_bytes_left_); @@ -428,9 +447,8 @@ FileDecoderState AudioDecoder::decode_wav_() { bytes_to_copy = std::min(bytes_to_copy, this->output_transfer_buffer_->free()); if (bytes_to_copy > 0) { - std::memcpy(this->output_transfer_buffer_->get_buffer_end(), this->input_transfer_buffer_->get_buffer_start(), - bytes_to_copy); - this->input_transfer_buffer_->decrease_buffer_length(bytes_to_copy); + std::memcpy(this->output_transfer_buffer_->get_buffer_end(), this->input_buffer_->data(), bytes_to_copy); + this->input_buffer_->consume(bytes_to_copy); this->output_transfer_buffer_->increase_buffer_length(bytes_to_copy); if (this->wav_has_known_end_) { this->wav_bytes_left_ -= bytes_to_copy; diff --git a/esphome/components/audio/audio_decoder.h b/esphome/components/audio/audio_decoder.h index cad16110ae..726baa289e 100644 --- a/esphome/components/audio/audio_decoder.h +++ b/esphome/components/audio/audio_decoder.h @@ -50,12 +50,12 @@ enum class FileDecoderState : uint8_t { class AudioDecoder { /* * @brief Class that facilitates decoding an audio file. - * The audio file is read from a ring buffer source, decoded, and sent to an audio sink (ring buffer or speaker - * component). + * The audio file is read from a source (ring buffer or const data pointer), decoded, and sent to an audio sink + * (ring buffer, speaker component, or callback). * Supports wav, flac, mp3, and ogg opus formats. */ public: - /// @brief Allocates the input and output transfer buffers + /// @brief Allocates the output transfer buffer and stores the input buffer size for later use by add_source() /// @param input_buffer_size Size of the input transfer buffer in bytes. /// @param output_buffer_size Size of the output transfer buffer in bytes. AudioDecoder(size_t input_buffer_size, size_t output_buffer_size); @@ -80,6 +80,17 @@ class AudioDecoder { esp_err_t add_sink(speaker::Speaker *speaker); #endif + /// @brief Adds a const data pointer as the source for raw file data. Does not allocate a transfer buffer. + /// @param data_pointer Pointer to the const audio data (e.g., stored in flash memory) + /// @param length Size of the data in bytes + /// @return ESP_OK + esp_err_t add_source(const uint8_t *data_pointer, size_t length); + + /// @brief Adds a callback as the sink for decoded audio. + /// @param callback Pointer to the AudioSinkCallback implementation + /// @return ESP_OK if successful, ESP_ERR_NO_MEM if the transfer buffer wasn't allocated + esp_err_t add_sink(AudioSinkCallback *callback); + /// @brief Sets up decoding the file /// @param audio_file_type AudioFileType of the file /// @return ESP_OK if successful, ESP_ERR_NO_MEM if the transfer buffers fail to allocate, or ESP_ERR_NOT_SUPPORTED if @@ -120,25 +131,26 @@ class AudioDecoder { #endif FileDecoderState decode_wav_(); - std::unique_ptr input_transfer_buffer_; + std::unique_ptr input_buffer_; std::unique_ptr output_transfer_buffer_; AudioFileType audio_file_type_{AudioFileType::NONE}; optional audio_stream_info_{}; + size_t input_buffer_size_{0}; size_t free_buffer_required_{0}; size_t wav_bytes_left_{0}; uint32_t potentially_failed_count_{0}; + uint32_t accumulated_frames_written_{0}; + uint32_t playback_ms_{0}; + bool end_of_file_{false}; bool wav_has_known_end_{false}; bool decoder_buffers_internally_{false}; bool pause_output_{false}; - - uint32_t accumulated_frames_written_{0}; - uint32_t playback_ms_{0}; }; } // namespace audio } // namespace esphome diff --git a/esphome/components/audio/audio_transfer_buffer.cpp b/esphome/components/audio/audio_transfer_buffer.cpp index a8be55d62f..5cd7cf9e63 100644 --- a/esphome/components/audio/audio_transfer_buffer.cpp +++ b/esphome/components/audio/audio_transfer_buffer.cpp @@ -142,7 +142,7 @@ size_t AudioSourceTransferBuffer::transfer_data_from_source(TickType_t ticks_to_ this->data_start_ = this->buffer_; } - size_t bytes_to_read = this->free(); + size_t bytes_to_read = AudioTransferBuffer::free(); size_t bytes_read = 0; if (bytes_to_read > 0) { if (this->ring_buffer_.use_count() > 0) { @@ -193,6 +193,21 @@ bool AudioSinkTransferBuffer::has_buffered_data() const { return (this->available() > 0); } +size_t AudioSourceTransferBuffer::free() const { return AudioTransferBuffer::free(); } + +bool AudioSourceTransferBuffer::has_buffered_data() const { return AudioTransferBuffer::has_buffered_data(); } + +void ConstAudioSourceBuffer::set_data(const uint8_t *data, size_t length) { + this->data_start_ = data; + this->length_ = length; +} + +void ConstAudioSourceBuffer::consume(size_t bytes) { + bytes = std::min(bytes, this->length_); + this->length_ -= bytes; + this->data_start_ += bytes; +} + } // namespace audio } // namespace esphome diff --git a/esphome/components/audio/audio_transfer_buffer.h b/esphome/components/audio/audio_transfer_buffer.h index 22c22cc9ae..c32d4d0e41 100644 --- a/esphome/components/audio/audio_transfer_buffer.h +++ b/esphome/components/audio/audio_transfer_buffer.h @@ -32,7 +32,7 @@ class AudioTransferBuffer { /// @brief Destructor that deallocates the transfer buffer ~AudioTransferBuffer(); - /// @brief Returns a pointer to the start of the transfer buffer where available() bytes of exisiting data can be read + /// @brief Returns a pointer to the start of the transfer buffer where available() bytes of existing data can be read uint8_t *get_buffer_start() const { return this->data_start_; } /// @brief Returns a pointer to the end of the transfer buffer where free() bytes of new data can be written @@ -129,10 +129,41 @@ class AudioSinkTransferBuffer : public AudioTransferBuffer { AudioSinkCallback *sink_callback_{nullptr}; }; -class AudioSourceTransferBuffer : public AudioTransferBuffer { +/// @brief Abstract interface for reading audio data from a buffer. +/// Provides a common read interface for both mutable transfer buffers and read-only const buffers. +class AudioReadableBuffer { + public: + virtual ~AudioReadableBuffer() = default; + + /// @brief Returns a pointer to the start of readable data + virtual const uint8_t *data() const = 0; + + /// @brief Returns the number of bytes available to read + virtual size_t available() const = 0; + + /// @brief Returns the number of free bytes available to write. Defaults to 0 for read-only buffers. + virtual size_t free() const { return 0; } + + /// @brief Advances past consumed data + /// @param bytes Number of bytes consumed + virtual void consume(size_t bytes) = 0; + + /// @brief Tests if there is any buffered data + virtual bool has_buffered_data() const = 0; + + /// @brief Refills the buffer from its source. No-op by default for read-only buffers. + /// @param ticks_to_wait FreeRTOS ticks to block while waiting for data + /// @param pre_shift If true, shifts existing data to the start of the buffer before reading + /// @return Number of bytes read + virtual size_t fill(TickType_t ticks_to_wait, bool pre_shift) { return 0; } + size_t fill(TickType_t ticks_to_wait) { return this->fill(ticks_to_wait, true); } +}; + +class AudioSourceTransferBuffer : public AudioTransferBuffer, public AudioReadableBuffer { /* * @brief A class that implements a transfer buffer for audio sources. * Supports reading audio data from a ring buffer into the transfer buffer for processing. + * Implements AudioReadableBuffer for use by consumers that only need read access. */ public: /// @brief Creates a new source transfer buffer. @@ -140,7 +171,7 @@ class AudioSourceTransferBuffer : public AudioTransferBuffer { /// @return unique_ptr if successfully allocated, nullptr otherwise static std::unique_ptr create(size_t buffer_size); - /// @brief Reads any available data from the sink into the transfer buffer. + /// @brief Reads any available data from the source into the transfer buffer. /// @param ticks_to_wait FreeRTOS ticks to block while waiting for the source to have enough data /// @param pre_shift If true, any unwritten data is moved to the start of the buffer before transferring from the /// source. Defaults to true. @@ -150,6 +181,36 @@ class AudioSourceTransferBuffer : public AudioTransferBuffer { /// @brief Adds a ring buffer as the transfer buffer's source. /// @param ring_buffer weak_ptr to the allocated ring buffer void set_source(const std::weak_ptr &ring_buffer) { this->ring_buffer_ = ring_buffer.lock(); }; + + // AudioReadableBuffer interface + const uint8_t *data() const override { return this->data_start_; } + size_t available() const override { return this->buffer_length_; } + size_t free() const override; + void consume(size_t bytes) override { this->decrease_buffer_length(bytes); } + bool has_buffered_data() const override; + size_t fill(TickType_t ticks_to_wait, bool pre_shift) override { + return this->transfer_data_from_source(ticks_to_wait, pre_shift); + } +}; + +/// @brief A lightweight read-only audio buffer for const data sources (e.g., flash memory). +/// Does not allocate memory or transfer data from external sources. +class ConstAudioSourceBuffer : public AudioReadableBuffer { + public: + /// @brief Sets the data pointer and length for the buffer + /// @param data Pointer to the const audio data + /// @param length Size of the data in bytes + void set_data(const uint8_t *data, size_t length); + + // AudioReadableBuffer interface + const uint8_t *data() const override { return this->data_start_; } + size_t available() const override { return this->length_; } + void consume(size_t bytes) override; + bool has_buffered_data() const override { return this->length_ > 0; } + + protected: + const uint8_t *data_start_{nullptr}; + size_t length_{0}; }; } // namespace audio From bd50b80882d990855561216548965d4c01aad9fd Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 19 Feb 2026 11:34:40 -0500 Subject: [PATCH 105/197] [opentherm] Remove deprecated opentherm_version config option (#14103) Co-authored-by: Claude Opus 4.6 --- esphome/components/opentherm/__init__.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/esphome/components/opentherm/__init__.py b/esphome/components/opentherm/__init__.py index dddb9dc891..36f85a9766 100644 --- a/esphome/components/opentherm/__init__.py +++ b/esphome/components/opentherm/__init__.py @@ -1,4 +1,3 @@ -import logging from typing import Any from esphome import automation, pins @@ -24,7 +23,6 @@ CONF_CH2_ACTIVE = "ch2_active" CONF_SUMMER_MODE_ACTIVE = "summer_mode_active" CONF_DHW_BLOCK = "dhw_block" CONF_SYNC_MODE = "sync_mode" -CONF_OPENTHERM_VERSION = "opentherm_version" # Deprecated, will be removed CONF_BEFORE_SEND = "before_send" CONF_BEFORE_PROCESS_RESPONSE = "before_process_response" @@ -38,8 +36,6 @@ BeforeProcessResponseTrigger = generate.opentherm_ns.class_( automation.Trigger.template(generate.OpenthermData.operator("ref")), ) -_LOGGER = logging.getLogger(__name__) - CONFIG_SCHEMA = cv.All( cv.Schema( { @@ -54,7 +50,6 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_SUMMER_MODE_ACTIVE, False): cv.boolean, cv.Optional(CONF_DHW_BLOCK, False): cv.boolean, cv.Optional(CONF_SYNC_MODE, False): cv.boolean, - cv.Optional(CONF_OPENTHERM_VERSION): cv.positive_float, # Deprecated cv.Optional(CONF_BEFORE_SEND): automation.validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(BeforeSendTrigger), @@ -123,11 +118,6 @@ async def to_code(config: dict[str, Any]) -> None: cg.add(getattr(var, f"set_{key}_{const.SETTING}")(value)) settings.append(key) else: - if key == CONF_OPENTHERM_VERSION: - _LOGGER.warning( - "opentherm_version is deprecated and will be removed in esphome 2025.2.0\n" - "Please change to 'opentherm_version_controller'." - ) cg.add(getattr(var, f"set_{key}")(value)) if len(input_sensors) > 0: From bf2e22da4f7ebf660d517f27702500a67d11ea9b Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 19 Feb 2026 11:55:03 -0500 Subject: [PATCH 106/197] [esp32] Remove deprecated add_idf_component() parameters and IDF component refresh option (#14105) Co-authored-by: Claude Opus 4.6 --- esphome/components/esp32/__init__.py | 68 ++++++---------------------- 1 file changed, 14 insertions(+), 54 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 8b3e1afea6..b1b3f0dc16 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -25,7 +25,6 @@ from esphome.const import ( CONF_PLATFORM_VERSION, CONF_PLATFORMIO_OPTIONS, CONF_REF, - CONF_REFRESH, CONF_SAFE_MODE, CONF_SOURCE, CONF_TYPE, @@ -41,7 +40,7 @@ from esphome.const import ( ThreadModel, __version__, ) -from esphome.core import CORE, HexInt, TimePeriod +from esphome.core import CORE, HexInt from esphome.coroutine import CoroPriority, coroutine_with_priority import esphome.final_validate as fv from esphome.helpers import copy_file_if_changed, rmtree, write_file_if_changed @@ -499,49 +498,24 @@ def add_idf_component( repo: str | None = None, ref: str | None = None, path: str | None = None, - refresh: TimePeriod | None = None, - components: list[str] | None = None, - submodules: list[str] | None = None, ): """Add an esp-idf component to the project.""" if not repo and not ref and not path: raise ValueError("Requires at least one of repo, ref or path") - if refresh or submodules or components: - _LOGGER.warning( - "The refresh, components and submodules parameters in add_idf_component() are " - "deprecated and will be removed in ESPHome 2026.1. If you are seeing this, report " - "an issue to the external_component author and ask them to update it." - ) components_registry = CORE.data[KEY_ESP32][KEY_COMPONENTS] - if components: - for comp in components: - existing = components_registry.get(comp) - if existing and existing.get(KEY_REF) != ref: - _LOGGER.warning( - "IDF component %s version conflict %s replaced by %s", - comp, - existing.get(KEY_REF), - ref, - ) - components_registry[comp] = { - KEY_REPO: repo, - KEY_REF: ref, - KEY_PATH: f"{path}/{comp}" if path else comp, - } - else: - existing = components_registry.get(name) - if existing and existing.get(KEY_REF) != ref: - _LOGGER.warning( - "IDF component %s version conflict %s replaced by %s", - name, - existing.get(KEY_REF), - ref, - ) - components_registry[name] = { - KEY_REPO: repo, - KEY_REF: ref, - KEY_PATH: path, - } + existing = components_registry.get(name) + if existing and existing.get(KEY_REF) != ref: + _LOGGER.warning( + "IDF component %s version conflict %s replaced by %s", + name, + existing.get(KEY_REF), + ref, + ) + components_registry[name] = { + KEY_REPO: repo, + KEY_REF: ref, + KEY_PATH: path, + } def exclude_builtin_idf_component(name: str) -> None: @@ -1037,16 +1011,6 @@ def _parse_idf_component(value: str) -> ConfigType: ) -def _validate_idf_component(config: ConfigType) -> ConfigType: - """Validate IDF component config and warn about deprecated options.""" - if CONF_REFRESH in config: - _LOGGER.warning( - "The 'refresh' option for IDF components is deprecated and has no effect. " - "It will be removed in ESPHome 2026.1. Please remove it from your configuration." - ) - return config - - FRAMEWORK_ESP_IDF = "esp-idf" FRAMEWORK_ARDUINO = "arduino" FRAMEWORK_SCHEMA = cv.Schema( @@ -1135,13 +1099,9 @@ FRAMEWORK_SCHEMA = cv.Schema( cv.Optional(CONF_SOURCE): cv.git_ref, cv.Optional(CONF_REF): cv.string, cv.Optional(CONF_PATH): cv.string, - cv.Optional(CONF_REFRESH): cv.All( - cv.string, cv.source_refresh - ), } ), ), - _validate_idf_component, ) ), } From ed74790eed87afff0f252fb2330dceb9b1a293de Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 19 Feb 2026 11:56:06 -0500 Subject: [PATCH 107/197] [i2c] Remove deprecated stop parameter overloads and readv/writev methods (#14106) Co-authored-by: Claude Opus 4.6 --- esphome/components/i2c/i2c.h | 31 ----------------- esphome/components/i2c/i2c_bus.h | 58 -------------------------------- 2 files changed, 89 deletions(-) diff --git a/esphome/components/i2c/i2c.h b/esphome/components/i2c/i2c.h index 48a6e751cf..aab98d5f46 100644 --- a/esphome/components/i2c/i2c.h +++ b/esphome/components/i2c/i2c.h @@ -267,37 +267,6 @@ class I2CDevice { bool write_byte_16(uint8_t a_register, uint16_t data) const { return write_bytes_16(a_register, &data, 1); } - // Deprecated functions - - ESPDEPRECATED("The stop argument is no longer used. This will be removed from ESPHome 2026.3.0", "2025.9.0") - ErrorCode read_register(uint8_t a_register, uint8_t *data, size_t len, bool stop) { - return this->read_register(a_register, data, len); - } - - ESPDEPRECATED("The stop argument is no longer used. This will be removed from ESPHome 2026.3.0", "2025.9.0") - ErrorCode read_register16(uint16_t a_register, uint8_t *data, size_t len, bool stop) { - return this->read_register16(a_register, data, len); - } - - ESPDEPRECATED("The stop argument is no longer used; use write_read() for consecutive write and read. This will be " - "removed from ESPHome 2026.3.0", - "2025.9.0") - ErrorCode write(const uint8_t *data, size_t len, bool stop) const { return this->write(data, len); } - - ESPDEPRECATED("The stop argument is no longer used; use write_read() for consecutive write and read. This will be " - "removed from ESPHome 2026.3.0", - "2025.9.0") - ErrorCode write_register(uint8_t a_register, const uint8_t *data, size_t len, bool stop) const { - return this->write_register(a_register, data, len); - } - - ESPDEPRECATED("The stop argument is no longer used; use write_read() for consecutive write and read. This will be " - "removed from ESPHome 2026.3.0", - "2025.9.0") - ErrorCode write_register16(uint16_t a_register, const uint8_t *data, size_t len, bool stop) const { - return this->write_register16(a_register, data, len); - } - protected: uint8_t address_{0x00}; ///< store the address of the device on the bus I2CBus *bus_{nullptr}; ///< pointer to I2CBus instance diff --git a/esphome/components/i2c/i2c_bus.h b/esphome/components/i2c/i2c_bus.h index 3de5d5ca7b..2bc0dc1ef9 100644 --- a/esphome/components/i2c/i2c_bus.h +++ b/esphome/components/i2c/i2c_bus.h @@ -1,8 +1,6 @@ #pragma once #include #include -#include -#include #include #include @@ -24,18 +22,6 @@ enum ErrorCode { ERROR_CRC = 7, ///< bytes received with a CRC error }; -/// @brief the ReadBuffer structure stores a pointer to a read buffer and its length -struct ReadBuffer { - uint8_t *data; ///< pointer to the read buffer - size_t len; ///< length of the buffer -}; - -/// @brief the WriteBuffer structure stores a pointer to a write buffer and its length -struct WriteBuffer { - const uint8_t *data; ///< pointer to the write buffer - size_t len; ///< length of the buffer -}; - /// @brief This Class provides the methods to read and write bytes from an I2CBus. /// @note The I2CBus virtual class follows a *Factory design pattern* that provides all the interfaces methods required /// by clients while deferring the actual implementation of these methods to a subclasses. I2C-bus specification and @@ -68,50 +54,6 @@ class I2CBus { return this->write_readv(address, buffer, len, nullptr, 0); } - ESPDEPRECATED("This method is deprecated and will be removed in ESPHome 2026.3.0. Use write_readv() instead.", - "2025.9.0") - ErrorCode readv(uint8_t address, ReadBuffer *read_buffers, size_t count) { - size_t total_len = 0; - for (size_t i = 0; i != count; i++) { - total_len += read_buffers[i].len; - } - - SmallBufferWithHeapFallback<128> buffer_alloc(total_len); // Most I2C reads are small - uint8_t *buffer = buffer_alloc.get(); - - auto err = this->write_readv(address, nullptr, 0, buffer, total_len); - if (err != ERROR_OK) - return err; - size_t pos = 0; - for (size_t i = 0; i != count; i++) { - if (read_buffers[i].len != 0) { - std::memcpy(read_buffers[i].data, buffer + pos, read_buffers[i].len); - pos += read_buffers[i].len; - } - } - return ERROR_OK; - } - - ESPDEPRECATED("This method is deprecated and will be removed in ESPHome 2026.3.0. Use write_readv() instead.", - "2025.9.0") - ErrorCode writev(uint8_t address, const WriteBuffer *write_buffers, size_t count, bool stop = true) { - size_t total_len = 0; - for (size_t i = 0; i != count; i++) { - total_len += write_buffers[i].len; - } - - SmallBufferWithHeapFallback<128> buffer_alloc(total_len); // Most I2C writes are small - uint8_t *buffer = buffer_alloc.get(); - - size_t pos = 0; - for (size_t i = 0; i != count; i++) { - std::memcpy(buffer + pos, write_buffers[i].data, write_buffers[i].len); - pos += write_buffers[i].len; - } - - return this->write_readv(address, buffer, total_len, nullptr, 0); - } - protected: /// @brief Scans the I2C bus for devices. Devices presence is kept in an array of std::pair /// that contains the address and the corresponding bool presence flag. From d2026b4cd74bbeb9b86941bf1887f3d93d639387 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Thu, 19 Feb 2026 10:56:34 -0600 Subject: [PATCH 108/197] [audio] Disable FLAC CRC validation to improve decoding efficiency (#14108) --- esphome/components/audio/audio_decoder.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/esphome/components/audio/audio_decoder.cpp b/esphome/components/audio/audio_decoder.cpp index 7794b5b0d3..bc05bc0006 100644 --- a/esphome/components/audio/audio_decoder.cpp +++ b/esphome/components/audio/audio_decoder.cpp @@ -85,6 +85,10 @@ esp_err_t AudioDecoder::start(AudioFileType audio_file_type) { #ifdef USE_AUDIO_FLAC_SUPPORT case AudioFileType::FLAC: this->flac_decoder_ = make_unique(); + // CRC check slows down decoding by 15-20% on an ESP32-S3. FLAC sources in ESPHome are either from an http source + // or built into the firmware, so the data integrity is already verified by the time it gets to the decoder, + // making the CRC check unnecessary. + this->flac_decoder_->set_crc_check_enabled(false); this->free_buffer_required_ = this->output_transfer_buffer_->capacity(); // Adjusted and reallocated after reading the header break; From da616e05574af7216a3b5668ab1d2d0e3dce0ca1 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 19 Feb 2026 12:00:05 -0500 Subject: [PATCH 109/197] [ethernet] Improve clk_mode deprecation warning with actionable YAML (#14104) Co-authored-by: Claude Opus 4.6 --- esphome/components/ethernet/__init__.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py index 52f5f44d41..935d2004d4 100644 --- a/esphome/components/ethernet/__init__.py +++ b/esphome/components/ethernet/__init__.py @@ -218,12 +218,19 @@ def _validate(config): ) elif config[CONF_TYPE] != "OPENETH": if CONF_CLK_MODE in config: + mode, pin = CLK_MODES_DEPRECATED[config[CONF_CLK_MODE]] LOGGER.warning( - "[ethernet] The 'clk_mode' option is deprecated and will be removed in ESPHome 2026.1. " - "Please update your configuration to use 'clk' instead." + "[ethernet] The 'clk_mode' option is deprecated. " + "Please replace 'clk_mode: %s' with:\n" + " clk:\n" + " mode: %s\n" + " pin: %s\n" + "Removal scheduled for 2026.7.0.", + config[CONF_CLK_MODE], + mode, + pin, ) - mode = CLK_MODES_DEPRECATED[config[CONF_CLK_MODE]] - config[CONF_CLK] = CLK_SCHEMA({CONF_MODE: mode[0], CONF_PIN: mode[1]}) + config[CONF_CLK] = CLK_SCHEMA({CONF_MODE: mode, CONF_PIN: pin}) del config[CONF_CLK_MODE] elif CONF_CLK not in config: raise cv.Invalid("'clk' is a required option for [ethernet].") From 9aa17984df3a0766c18c02ed60f4575ba69a7fb9 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 19 Feb 2026 12:25:26 -0500 Subject: [PATCH 110/197] [pulse_counter] Fix build failure when use_pcnt is false (#14111) Co-authored-by: Claude Opus 4.6 --- .../components/pulse_counter/pulse_counter_sensor.h | 4 ++-- .../pulse_counter/test-no-pcnt.esp32-idf.yaml | 10 ++++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) create mode 100644 tests/components/pulse_counter/test-no-pcnt.esp32-idf.yaml diff --git a/esphome/components/pulse_counter/pulse_counter_sensor.h b/esphome/components/pulse_counter/pulse_counter_sensor.h index a7913d5d66..7a68858099 100644 --- a/esphome/components/pulse_counter/pulse_counter_sensor.h +++ b/esphome/components/pulse_counter/pulse_counter_sensor.h @@ -8,10 +8,10 @@ #if defined(USE_ESP32) #include -#ifdef SOC_PCNT_SUPPORTED +#if defined(SOC_PCNT_SUPPORTED) && __has_include() #include #define HAS_PCNT -#endif // SOC_PCNT_SUPPORTED +#endif // defined(SOC_PCNT_SUPPORTED) && __has_include() #endif // USE_ESP32 namespace esphome { diff --git a/tests/components/pulse_counter/test-no-pcnt.esp32-idf.yaml b/tests/components/pulse_counter/test-no-pcnt.esp32-idf.yaml new file mode 100644 index 0000000000..cd15cc781d --- /dev/null +++ b/tests/components/pulse_counter/test-no-pcnt.esp32-idf.yaml @@ -0,0 +1,10 @@ +sensor: + - platform: pulse_counter + name: Pulse Counter + pin: 4 + use_pcnt: false + count_mode: + rising_edge: INCREMENT + falling_edge: DECREMENT + internal_filter: 13us + update_interval: 15s From 7a5c3cee0d0f3fd77423438f036e8b6596a3246b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 19 Feb 2026 11:41:00 -0600 Subject: [PATCH 111/197] [esp32_ble] Enable CONFIG_BT_RELEASE_IRAM on ESP32-C2 (#14109) Co-authored-by: Claude Opus 4.6 --- esphome/components/esp32_ble/__init__.py | 10 ++++++++++ tests/components/esp32_ble/test.esp32-c2-idf.yaml | 5 +++++ 2 files changed, 15 insertions(+) create mode 100644 tests/components/esp32_ble/test.esp32-c2-idf.yaml diff --git a/esphome/components/esp32_ble/__init__.py b/esphome/components/esp32_ble/__init__.py index dcc3ce71cf..d2020ada22 100644 --- a/esphome/components/esp32_ble/__init__.py +++ b/esphome/components/esp32_ble/__init__.py @@ -9,6 +9,7 @@ from esphome import automation import esphome.codegen as cg from esphome.components import socket from esphome.components.esp32 import add_idf_sdkconfig_option, const, get_esp32_variant +from esphome.components.esp32.const import VARIANT_ESP32C2 import esphome.config_validation as cv from esphome.const import ( CONF_ENABLE_ON_BOOT, @@ -387,6 +388,15 @@ def final_validation(config): f"Name '{name}' is too long, maximum length is {max_length} characters" ) + # ESP32-C2 has very limited RAM (~272KB). Without releasing BLE IRAM, + # esp_bt_controller_init fails with ESP_ERR_NO_MEM. + # CONFIG_BT_RELEASE_IRAM changes the memory layout so IRAM and DRAM share + # space more flexibly, giving the BT controller enough contiguous memory. + # This requires CONFIG_ESP_SYSTEM_PMP_IDRAM_SPLIT to be disabled. + if get_esp32_variant() == VARIANT_ESP32C2: + add_idf_sdkconfig_option("CONFIG_BT_RELEASE_IRAM", True) + add_idf_sdkconfig_option("CONFIG_ESP_SYSTEM_PMP_IDRAM_SPLIT", False) + # Set GATT Client/Server sdkconfig options based on which components are loaded full_config = fv.full_config.get() diff --git a/tests/components/esp32_ble/test.esp32-c2-idf.yaml b/tests/components/esp32_ble/test.esp32-c2-idf.yaml new file mode 100644 index 0000000000..f8defaf28f --- /dev/null +++ b/tests/components/esp32_ble/test.esp32-c2-idf.yaml @@ -0,0 +1,5 @@ +<<: !include common.yaml + +esp32_ble: + io_capability: keyboard_only + disable_bt_logs: false From f2c98d612607d61c483d08cc77e92795af85c131 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 19 Feb 2026 12:45:04 -0500 Subject: [PATCH 112/197] [safe_mode] Log brownout as reset reason on OTA rollback (#14113) Co-authored-by: Claude Opus 4.6 --- esphome/components/safe_mode/safe_mode.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/esphome/components/safe_mode/safe_mode.cpp b/esphome/components/safe_mode/safe_mode.cpp index f32511531a..6cae4bf9d5 100644 --- a/esphome/components/safe_mode/safe_mode.cpp +++ b/esphome/components/safe_mode/safe_mode.cpp @@ -11,6 +11,7 @@ #if defined(USE_ESP32) && defined(USE_OTA_ROLLBACK) #include +#include #endif namespace esphome::safe_mode { @@ -54,6 +55,10 @@ void SafeModeComponent::dump_config() { "OTA rollback detected! Rolled back from partition '%s'\n" "The device reset before the boot was marked successful", last_invalid->label); + if (esp_reset_reason() == ESP_RST_BROWNOUT) { + ESP_LOGW(TAG, "Last reset was due to brownout - check your power supply!\n" + "See https://esphome.io/guides/faq.html#brownout-detector-was-triggered"); + } } #endif } From 4aa8f57d3609c12c10c3877c83b06d0c52975e7a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 19 Feb 2026 14:08:44 -0600 Subject: [PATCH 113/197] [json] Add SerializationBuffer for stack-first JSON serialization (#13625) --- esphome/components/api/user_services.h | 4 +- esphome/components/json/json_util.cpp | 81 +++++++- esphome/components/json/json_util.h | 111 +++++++++- esphome/components/mqtt/mqtt_client.cpp | 4 +- esphome/components/web_server/web_server.cpp | 191 +++++++++--------- esphome/components/web_server/web_server.h | 138 +++++++------ .../web_server_idf/web_server_idf.cpp | 6 +- .../web_server_idf/web_server_idf.h | 3 +- tests/components/json/common.yaml | 13 +- 9 files changed, 367 insertions(+), 184 deletions(-) diff --git a/esphome/components/api/user_services.h b/esphome/components/api/user_services.h index 85fba2a435..0fc529108c 100644 --- a/esphome/components/api/user_services.h +++ b/esphome/components/api/user_services.h @@ -264,9 +264,9 @@ template class APIRespondAction : public Action { // Build and send JSON response json::JsonBuilder builder; this->json_builder_(x..., builder.root()); - std::string json_str = builder.serialize(); + auto json_buf = builder.serialize(); this->parent_->send_action_response(call_id, success, StringRef(error_message), - reinterpret_cast(json_str.data()), json_str.size()); + reinterpret_cast(json_buf.data()), json_buf.size()); return; } #endif diff --git a/esphome/components/json/json_util.cpp b/esphome/components/json/json_util.cpp index 69f8bfc61a..6c60a04d20 100644 --- a/esphome/components/json/json_util.cpp +++ b/esphome/components/json/json_util.cpp @@ -15,7 +15,7 @@ static const char *const TAG = "json"; static SpiRamAllocator global_json_allocator; #endif -std::string build_json(const json_build_t &f) { +SerializationBuffer<> build_json(const json_build_t &f) { // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson JsonBuilder builder; JsonObject root = builder.root(); @@ -66,14 +66,83 @@ JsonDocument parse_json(const uint8_t *data, size_t len) { // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) } -std::string JsonBuilder::serialize() { +SerializationBuffer<> JsonBuilder::serialize() { + // =========================================================================================== + // CRITICAL: NRVO (Named Return Value Optimization) - DO NOT REFACTOR WITHOUT UNDERSTANDING + // =========================================================================================== + // + // This function is carefully structured to enable NRVO. The compiler constructs `result` + // directly in the caller's stack frame, eliminating the move constructor call entirely. + // + // WITHOUT NRVO: Each return would trigger SerializationBuffer's move constructor, which + // must memcpy up to 512 bytes of stack buffer content. This happens on EVERY JSON + // serialization (sensor updates, web server responses, MQTT publishes, etc.). + // + // WITH NRVO: Zero memcpy, zero move constructor overhead. The buffer lives directly + // where the caller needs it. + // + // Requirements for NRVO to work: + // 1. Single named variable (`result`) returned from ALL paths + // 2. All paths must return the SAME variable (not different variables) + // 3. No std::move() on the return statement + // + // If you must modify this function: + // - Keep a single `result` variable declared at the top + // - All code paths must return `result` (not a different variable) + // - Verify NRVO still works by checking the disassembly for move constructor calls + // - Test: objdump -d -C firmware.elf | grep "SerializationBuffer.*SerializationBuffer" + // Should show only destructor, NOT move constructor + // + // Try stack buffer first. 640 bytes covers 99.9% of JSON payloads (sensors ~200B, + // lights ~170B, climate ~500-700B). Only entities with 40+ options exceed this. + // + // IMPORTANT: ArduinoJson's serializeJson() with a bounded buffer returns the actual + // bytes written (truncated count), NOT the would-be size like snprintf(). When the + // payload exceeds the buffer, the return value equals the buffer capacity. The heap + // fallback doubles the buffer size until the payload fits. This avoids instantiating + // measureJson()'s DummyWriter templates (~736 bytes flash) at the cost of temporarily + // over-allocating heap (at most 2x) for the rare payloads that exceed 640 bytes. + // + // =========================================================================================== + constexpr size_t buf_size = SerializationBuffer<>::BUFFER_SIZE; + SerializationBuffer<> result(buf_size - 1); // Max content size (reserve 1 for null) + if (doc_.overflowed()) { ESP_LOGE(TAG, "JSON document overflow"); - return "{}"; + auto *buf = result.data_writable_(); + buf[0] = '{'; + buf[1] = '}'; + buf[2] = '\0'; + result.set_size_(2); + return result; } - std::string output; - serializeJson(doc_, output); - return output; + + size_t size = serializeJson(doc_, result.data_writable_(), buf_size); + if (size < buf_size) { + // Fits in stack buffer - update size to actual length + result.set_size_(size); + return result; + } + + // Payload exceeded stack buffer. Double the buffer and retry until it fits. + // In practice, one iteration (1024 bytes) covers all known entity types. + // Payloads exceeding 1024 bytes are not known to exist in real configurations. + // Cap at 5120 as a safety limit to prevent runaway allocation. + constexpr size_t max_heap_size = 5120; + size_t heap_size = buf_size * 2; + while (heap_size <= max_heap_size) { + result.reallocate_heap_(heap_size - 1); + size = serializeJson(doc_, result.data_writable_(), heap_size); + if (size < heap_size) { + result.set_size_(size); + return result; + } + heap_size *= 2; + } + // Payload exceeds 5120 bytes - return truncated result + ESP_LOGW(TAG, "JSON payload too large, truncated to %zu bytes", size); + result.set_size_(size); + return result; } } // namespace json diff --git a/esphome/components/json/json_util.h b/esphome/components/json/json_util.h index c472b9a9ec..0dc9ff883c 100644 --- a/esphome/components/json/json_util.h +++ b/esphome/components/json/json_util.h @@ -1,5 +1,7 @@ #pragma once +#include +#include #include #include "esphome/core/defines.h" @@ -14,6 +16,108 @@ namespace esphome { namespace json { +/// Buffer for JSON serialization that uses stack allocation for small payloads. +/// Template parameter STACK_SIZE specifies the stack buffer size (default 512 bytes). +/// Supports move semantics for efficient return-by-value. +template class SerializationBuffer { + public: + static constexpr size_t BUFFER_SIZE = STACK_SIZE; ///< Stack buffer size for this instantiation + + /// Construct with known size (typically from measureJson) + explicit SerializationBuffer(size_t size) : size_(size) { + if (size + 1 <= STACK_SIZE) { + buffer_ = stack_buffer_; + } else { + heap_buffer_ = new char[size + 1]; + buffer_ = heap_buffer_; + } + buffer_[0] = '\0'; + } + + ~SerializationBuffer() { delete[] heap_buffer_; } + + // Move constructor - works with same template instantiation + SerializationBuffer(SerializationBuffer &&other) noexcept : heap_buffer_(other.heap_buffer_), size_(other.size_) { + if (other.buffer_ == other.stack_buffer_) { + // Stack buffer - must copy content + std::memcpy(stack_buffer_, other.stack_buffer_, size_ + 1); + buffer_ = stack_buffer_; + } else { + // Heap buffer - steal ownership + buffer_ = heap_buffer_; + other.heap_buffer_ = nullptr; + } + // Leave moved-from object in valid empty state + other.stack_buffer_[0] = '\0'; + other.buffer_ = other.stack_buffer_; + other.size_ = 0; + } + + // Move assignment + SerializationBuffer &operator=(SerializationBuffer &&other) noexcept { + if (this != &other) { + delete[] heap_buffer_; + heap_buffer_ = other.heap_buffer_; + size_ = other.size_; + if (other.buffer_ == other.stack_buffer_) { + std::memcpy(stack_buffer_, other.stack_buffer_, size_ + 1); + buffer_ = stack_buffer_; + } else { + buffer_ = heap_buffer_; + other.heap_buffer_ = nullptr; + } + // Leave moved-from object in valid empty state + other.stack_buffer_[0] = '\0'; + other.buffer_ = other.stack_buffer_; + other.size_ = 0; + } + return *this; + } + + // Delete copy operations + SerializationBuffer(const SerializationBuffer &) = delete; + SerializationBuffer &operator=(const SerializationBuffer &) = delete; + + /// Get null-terminated C string + const char *c_str() const { return buffer_; } + /// Get data pointer + const char *data() const { return buffer_; } + /// Get string length (excluding null terminator) + size_t size() const { return size_; } + + /// Implicit conversion to std::string for backward compatibility + /// WARNING: This allocates a new std::string on the heap. Prefer using + /// c_str() or data()/size() directly when possible to avoid allocation. + operator std::string() const { return std::string(buffer_, size_); } // NOLINT(google-explicit-constructor) + + private: + friend class JsonBuilder; ///< Allows JsonBuilder::serialize() to call private methods + + /// Get writable buffer (for serialization) + char *data_writable_() { return buffer_; } + /// Set actual size after serialization (must not exceed allocated size) + /// Also ensures null termination for c_str() safety + void set_size_(size_t size) { + size_ = size; + buffer_[size] = '\0'; + } + + /// Reallocate to heap buffer with new size (for when stack buffer is too small) + /// This invalidates any previous buffer content. Used by JsonBuilder::serialize(). + void reallocate_heap_(size_t size) { + delete[] heap_buffer_; + heap_buffer_ = new char[size + 1]; + buffer_ = heap_buffer_; + size_ = size; + buffer_[0] = '\0'; + } + + char stack_buffer_[STACK_SIZE]; + char *heap_buffer_{nullptr}; + char *buffer_; + size_t size_; +}; + #ifdef USE_PSRAM // Build an allocator for the JSON Library using the RAMAllocator class // This is only compiled when PSRAM is enabled @@ -47,7 +151,8 @@ using json_parse_t = std::function; using json_build_t = std::function; /// Build a JSON string with the provided json build function. -std::string build_json(const json_build_t &f); +/// Returns SerializationBuffer for stack-first allocation; implicitly converts to std::string. +SerializationBuffer<> build_json(const json_build_t &f); /// Parse a JSON string and run the provided json parse function if it's valid. bool parse_json(const std::string &data, const json_parse_t &f); @@ -72,7 +177,9 @@ class JsonBuilder { return root_; } - std::string serialize(); + /// Serialize the JSON document to a SerializationBuffer (stack-first allocation) + /// Uses 512-byte stack buffer by default, falls back to heap for larger JSON + SerializationBuffer<> serialize(); private: #ifdef USE_PSRAM diff --git a/esphome/components/mqtt/mqtt_client.cpp b/esphome/components/mqtt/mqtt_client.cpp index 9905b4677e..c433804dd9 100644 --- a/esphome/components/mqtt/mqtt_client.cpp +++ b/esphome/components/mqtt/mqtt_client.cpp @@ -543,8 +543,8 @@ bool MQTTClientComponent::publish(const char *topic, const char *payload, size_t } bool MQTTClientComponent::publish_json(const char *topic, const json::json_build_t &f, uint8_t qos, bool retain) { - std::string message = json::build_json(f); - return this->publish(topic, message.c_str(), message.length(), qos, retain); + auto message = json::build_json(f); + return this->publish(topic, message.c_str(), message.size(), qos, retain); } void MQTTClientComponent::enable() { diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index f352e5896c..4b572417c1 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -214,7 +214,7 @@ DeferredUpdateEventSource::deq_push_back_with_dedup_(void *source, message_gener void DeferredUpdateEventSource::process_deferred_queue_() { while (!deferred_queue_.empty()) { DeferredEvent &de = deferred_queue_.front(); - std::string message = de.message_generator_(web_server_, de.source_); + auto message = de.message_generator_(web_server_, de.source_); if (this->send(message.c_str(), "state") != DISCARDED) { // O(n) but memory efficiency is more important than speed here which is why std::vector was chosen deferred_queue_.erase(deferred_queue_.begin()); @@ -271,7 +271,7 @@ void DeferredUpdateEventSource::deferrable_send_state(void *source, const char * // deferred queue still not empty which means downstream event queue full, no point trying to send first deq_push_back_with_dedup_(source, message_generator); } else { - std::string message = message_generator(web_server_, source); + auto message = message_generator(web_server_, source); if (this->send(message.c_str(), "state") == DISCARDED) { deq_push_back_with_dedup_(source, message_generator); } else { @@ -325,7 +325,7 @@ void DeferredUpdateEventSourceList::on_client_connect_(DeferredUpdateEventSource ws->defer([ws, source]() { // Configure reconnect timeout and send config // this should always go through since the AsyncEventSourceClient event queue is empty on connect - std::string message = ws->get_config_json(); + auto message = ws->get_config_json(); source->try_send_nodefer(message.c_str(), "ping", millis(), 30000); #ifdef USE_WEBSERVER_SORTING @@ -334,10 +334,10 @@ void DeferredUpdateEventSourceList::on_client_connect_(DeferredUpdateEventSource JsonObject root = builder.root(); root[ESPHOME_F("name")] = group.second.name; root[ESPHOME_F("sorting_weight")] = group.second.weight; - message = builder.serialize(); + auto group_msg = builder.serialize(); // up to 31 groups should be able to be queued initially without defer - source->try_send_nodefer(message.c_str(), "sorting_group"); + source->try_send_nodefer(group_msg.c_str(), "sorting_group"); } #endif @@ -370,7 +370,7 @@ void WebServer::set_css_include(const char *css_include) { this->css_include_ = void WebServer::set_js_include(const char *js_include) { this->js_include_ = js_include; } #endif -std::string WebServer::get_config_json() { +json::SerializationBuffer<> WebServer::get_config_json() { json::JsonBuilder builder; JsonObject root = builder.root(); @@ -606,20 +606,20 @@ void WebServer::handle_sensor_request(AsyncWebServerRequest *request, const UrlM // Note: request->method() is always HTTP_GET here (canHandle ensures this) if (entity_match.action_is_empty) { auto detail = get_request_detail(request); - std::string data = this->sensor_json_(obj, obj->state, detail); + auto data = this->sensor_json_(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); return; } } request->send(404); } -std::string WebServer::sensor_state_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> WebServer::sensor_state_json_generator(WebServer *web_server, void *source) { 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) { +json::SerializationBuffer<> WebServer::sensor_all_json_generator(WebServer *web_server, void *source) { 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) { +json::SerializationBuffer<> WebServer::sensor_json_(sensor::Sensor *obj, float value, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); @@ -653,23 +653,23 @@ void WebServer::handle_text_sensor_request(AsyncWebServerRequest *request, const // Note: request->method() is always HTTP_GET here (canHandle ensures this) if (entity_match.action_is_empty) { auto detail = get_request_detail(request); - std::string data = this->text_sensor_json_(obj, obj->state, detail); + auto data = this->text_sensor_json_(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); return; } } request->send(404); } -std::string WebServer::text_sensor_state_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> 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); } -std::string WebServer::text_sensor_all_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> 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); } -std::string WebServer::text_sensor_json_(text_sensor::TextSensor *obj, const std::string &value, - JsonDetail start_config) { +json::SerializationBuffer<> WebServer::text_sensor_json_(text_sensor::TextSensor *obj, const std::string &value, + JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); @@ -714,7 +714,7 @@ void WebServer::handle_switch_request(AsyncWebServerRequest *request, const UrlM if (request->method() == HTTP_GET && entity_match.action_is_empty) { auto detail = get_request_detail(request); - std::string data = this->switch_json_(obj, obj->state, detail); + auto data = this->switch_json_(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); return; } @@ -739,13 +739,13 @@ void WebServer::handle_switch_request(AsyncWebServerRequest *request, const UrlM } request->send(404); } -std::string WebServer::switch_state_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> WebServer::switch_state_json_generator(WebServer *web_server, void *source) { 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) { +json::SerializationBuffer<> WebServer::switch_all_json_generator(WebServer *web_server, void *source) { 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) { +json::SerializationBuffer<> WebServer::switch_json_(switch_::Switch *obj, bool value, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); @@ -767,7 +767,7 @@ void WebServer::handle_button_request(AsyncWebServerRequest *request, const UrlM continue; if (request->method() == HTTP_GET && entity_match.action_is_empty) { auto detail = get_request_detail(request); - std::string data = this->button_json_(obj, detail); + auto data = this->button_json_(obj, detail); request->send(200, "application/json", data.c_str()); } else if (match.method_equals(ESPHOME_F("press"))) { DEFER_ACTION(obj, obj->press()); @@ -780,10 +780,10 @@ void WebServer::handle_button_request(AsyncWebServerRequest *request, const UrlM } request->send(404); } -std::string WebServer::button_all_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> WebServer::button_all_json_generator(WebServer *web_server, void *source) { return web_server->button_json_((button::Button *) (source), DETAIL_ALL); } -std::string WebServer::button_json_(button::Button *obj, JsonDetail start_config) { +json::SerializationBuffer<> WebServer::button_json_(button::Button *obj, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); @@ -810,22 +810,23 @@ void WebServer::handle_binary_sensor_request(AsyncWebServerRequest *request, con // Note: request->method() is always HTTP_GET here (canHandle ensures this) if (entity_match.action_is_empty) { auto detail = get_request_detail(request); - std::string data = this->binary_sensor_json_(obj, obj->state, detail); + auto data = this->binary_sensor_json_(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); return; } } request->send(404); } -std::string WebServer::binary_sensor_state_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> 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); } -std::string WebServer::binary_sensor_all_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> 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); } -std::string WebServer::binary_sensor_json_(binary_sensor::BinarySensor *obj, bool value, JsonDetail start_config) { +json::SerializationBuffer<> WebServer::binary_sensor_json_(binary_sensor::BinarySensor *obj, bool value, + JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); @@ -852,7 +853,7 @@ void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatc if (request->method() == HTTP_GET && entity_match.action_is_empty) { auto detail = get_request_detail(request); - std::string data = this->fan_json_(obj, detail); + auto data = this->fan_json_(obj, detail); request->send(200, "application/json", data.c_str()); } else if (match.method_equals(ESPHOME_F("toggle"))) { DEFER_ACTION(obj, obj->toggle().perform()); @@ -893,13 +894,13 @@ void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatc } request->send(404); } -std::string WebServer::fan_state_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> WebServer::fan_state_json_generator(WebServer *web_server, void *source) { return web_server->fan_json_((fan::Fan *) (source), DETAIL_STATE); } -std::string WebServer::fan_all_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> WebServer::fan_all_json_generator(WebServer *web_server, void *source) { return web_server->fan_json_((fan::Fan *) (source), DETAIL_ALL); } -std::string WebServer::fan_json_(fan::Fan *obj, JsonDetail start_config) { +json::SerializationBuffer<> WebServer::fan_json_(fan::Fan *obj, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); @@ -933,7 +934,7 @@ void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMa if (request->method() == HTTP_GET && entity_match.action_is_empty) { auto detail = get_request_detail(request); - std::string data = this->light_json_(obj, detail); + auto data = this->light_json_(obj, detail); request->send(200, "application/json", data.c_str()); } else if (match.method_equals(ESPHOME_F("toggle"))) { DEFER_ACTION(obj, obj->toggle().perform()); @@ -972,13 +973,13 @@ void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMa } request->send(404); } -std::string WebServer::light_state_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> WebServer::light_state_json_generator(WebServer *web_server, void *source) { return web_server->light_json_((light::LightState *) (source), DETAIL_STATE); } -std::string WebServer::light_all_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> WebServer::light_all_json_generator(WebServer *web_server, void *source) { return web_server->light_json_((light::LightState *) (source), DETAIL_ALL); } -std::string WebServer::light_json_(light::LightState *obj, JsonDetail start_config) { +json::SerializationBuffer<> WebServer::light_json_(light::LightState *obj, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); @@ -1012,7 +1013,7 @@ void WebServer::handle_cover_request(AsyncWebServerRequest *request, const UrlMa if (request->method() == HTTP_GET && entity_match.action_is_empty) { auto detail = get_request_detail(request); - std::string data = this->cover_json_(obj, detail); + auto data = this->cover_json_(obj, detail); request->send(200, "application/json", data.c_str()); return; } @@ -1060,13 +1061,13 @@ void WebServer::handle_cover_request(AsyncWebServerRequest *request, const UrlMa } request->send(404); } -std::string WebServer::cover_state_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> WebServer::cover_state_json_generator(WebServer *web_server, void *source) { return web_server->cover_json_((cover::Cover *) (source), DETAIL_STATE); } -std::string WebServer::cover_all_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> WebServer::cover_all_json_generator(WebServer *web_server, void *source) { return web_server->cover_json_((cover::Cover *) (source), DETAIL_ALL); } -std::string WebServer::cover_json_(cover::Cover *obj, JsonDetail start_config) { +json::SerializationBuffer<> WebServer::cover_json_(cover::Cover *obj, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); @@ -1101,7 +1102,7 @@ void WebServer::handle_number_request(AsyncWebServerRequest *request, const UrlM if (request->method() == HTTP_GET && entity_match.action_is_empty) { auto detail = get_request_detail(request); - std::string data = this->number_json_(obj, obj->state, detail); + auto data = this->number_json_(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); return; } @@ -1120,13 +1121,13 @@ void WebServer::handle_number_request(AsyncWebServerRequest *request, const UrlM request->send(404); } -std::string WebServer::number_state_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> WebServer::number_state_json_generator(WebServer *web_server, void *source) { 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) { +json::SerializationBuffer<> WebServer::number_all_json_generator(WebServer *web_server, void *source) { 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) { +json::SerializationBuffer<> WebServer::number_json_(number::Number *obj, float value, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); @@ -1168,7 +1169,7 @@ void WebServer::handle_date_request(AsyncWebServerRequest *request, const UrlMat continue; if (request->method() == HTTP_GET && entity_match.action_is_empty) { auto detail = get_request_detail(request); - std::string data = this->date_json_(obj, detail); + auto data = this->date_json_(obj, detail); request->send(200, "application/json", data.c_str()); return; } @@ -1194,13 +1195,13 @@ void WebServer::handle_date_request(AsyncWebServerRequest *request, const UrlMat request->send(404); } -std::string WebServer::date_state_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> WebServer::date_state_json_generator(WebServer *web_server, void *source) { return web_server->date_json_((datetime::DateEntity *) (source), DETAIL_STATE); } -std::string WebServer::date_all_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> WebServer::date_all_json_generator(WebServer *web_server, void *source) { return web_server->date_json_((datetime::DateEntity *) (source), DETAIL_ALL); } -std::string WebServer::date_json_(datetime::DateEntity *obj, JsonDetail start_config) { +json::SerializationBuffer<> WebServer::date_json_(datetime::DateEntity *obj, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); @@ -1229,7 +1230,7 @@ void WebServer::handle_time_request(AsyncWebServerRequest *request, const UrlMat continue; if (request->method() == HTTP_GET && entity_match.action_is_empty) { auto detail = get_request_detail(request); - std::string data = this->time_json_(obj, detail); + auto data = this->time_json_(obj, detail); request->send(200, "application/json", data.c_str()); return; } @@ -1254,13 +1255,13 @@ void WebServer::handle_time_request(AsyncWebServerRequest *request, const UrlMat } request->send(404); } -std::string WebServer::time_state_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> WebServer::time_state_json_generator(WebServer *web_server, void *source) { return web_server->time_json_((datetime::TimeEntity *) (source), DETAIL_STATE); } -std::string WebServer::time_all_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> WebServer::time_all_json_generator(WebServer *web_server, void *source) { return web_server->time_json_((datetime::TimeEntity *) (source), DETAIL_ALL); } -std::string WebServer::time_json_(datetime::TimeEntity *obj, JsonDetail start_config) { +json::SerializationBuffer<> WebServer::time_json_(datetime::TimeEntity *obj, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); @@ -1289,7 +1290,7 @@ void WebServer::handle_datetime_request(AsyncWebServerRequest *request, const Ur continue; if (request->method() == HTTP_GET && entity_match.action_is_empty) { auto detail = get_request_detail(request); - std::string data = this->datetime_json_(obj, detail); + auto data = this->datetime_json_(obj, detail); request->send(200, "application/json", data.c_str()); return; } @@ -1314,13 +1315,13 @@ void WebServer::handle_datetime_request(AsyncWebServerRequest *request, const Ur } request->send(404); } -std::string WebServer::datetime_state_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> WebServer::datetime_state_json_generator(WebServer *web_server, void *source) { return web_server->datetime_json_((datetime::DateTimeEntity *) (source), DETAIL_STATE); } -std::string WebServer::datetime_all_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> WebServer::datetime_all_json_generator(WebServer *web_server, void *source) { return web_server->datetime_json_((datetime::DateTimeEntity *) (source), DETAIL_ALL); } -std::string WebServer::datetime_json_(datetime::DateTimeEntity *obj, JsonDetail start_config) { +json::SerializationBuffer<> WebServer::datetime_json_(datetime::DateTimeEntity *obj, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); @@ -1351,7 +1352,7 @@ void WebServer::handle_text_request(AsyncWebServerRequest *request, const UrlMat if (request->method() == HTTP_GET && entity_match.action_is_empty) { auto detail = get_request_detail(request); - std::string data = this->text_json_(obj, obj->state, detail); + auto data = this->text_json_(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); return; } @@ -1370,13 +1371,13 @@ void WebServer::handle_text_request(AsyncWebServerRequest *request, const UrlMat request->send(404); } -std::string WebServer::text_state_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> WebServer::text_state_json_generator(WebServer *web_server, void *source) { 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) { +json::SerializationBuffer<> WebServer::text_all_json_generator(WebServer *web_server, void *source) { 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) { +json::SerializationBuffer<> WebServer::text_json_(text::Text *obj, const std::string &value, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); @@ -1408,7 +1409,7 @@ void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlM if (request->method() == HTTP_GET && entity_match.action_is_empty) { auto detail = get_request_detail(request); - std::string data = this->select_json_(obj, obj->has_state() ? obj->current_option() : StringRef(), detail); + auto data = this->select_json_(obj, obj->has_state() ? obj->current_option() : StringRef(), detail); request->send(200, "application/json", data.c_str()); return; } @@ -1427,15 +1428,15 @@ void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlM } request->send(404); } -std::string WebServer::select_state_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> 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() : StringRef(), DETAIL_STATE); } -std::string WebServer::select_all_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> 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() : StringRef(), DETAIL_ALL); } -std::string WebServer::select_json_(select::Select *obj, StringRef value, JsonDetail start_config) { +json::SerializationBuffer<> WebServer::select_json_(select::Select *obj, StringRef value, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); @@ -1467,7 +1468,7 @@ void WebServer::handle_climate_request(AsyncWebServerRequest *request, const Url if (request->method() == HTTP_GET && entity_match.action_is_empty) { auto detail = get_request_detail(request); - std::string data = this->climate_json_(obj, detail); + auto data = this->climate_json_(obj, detail); request->send(200, "application/json", data.c_str()); return; } @@ -1500,15 +1501,15 @@ void WebServer::handle_climate_request(AsyncWebServerRequest *request, const Url } request->send(404); } -std::string WebServer::climate_state_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> 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); } -std::string WebServer::climate_all_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> 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); } -std::string WebServer::climate_json_(climate::Climate *obj, JsonDetail start_config) { +json::SerializationBuffer<> 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(); @@ -1641,7 +1642,7 @@ void WebServer::handle_lock_request(AsyncWebServerRequest *request, const UrlMat if (request->method() == HTTP_GET && entity_match.action_is_empty) { auto detail = get_request_detail(request); - std::string data = this->lock_json_(obj, obj->state, detail); + auto data = this->lock_json_(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); return; } @@ -1666,13 +1667,13 @@ void WebServer::handle_lock_request(AsyncWebServerRequest *request, const UrlMat } request->send(404); } -std::string WebServer::lock_state_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> WebServer::lock_state_json_generator(WebServer *web_server, void *source) { 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) { +json::SerializationBuffer<> WebServer::lock_all_json_generator(WebServer *web_server, void *source) { 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) { +json::SerializationBuffer<> WebServer::lock_json_(lock::Lock *obj, lock::LockState value, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); @@ -1700,7 +1701,7 @@ void WebServer::handle_valve_request(AsyncWebServerRequest *request, const UrlMa if (request->method() == HTTP_GET && entity_match.action_is_empty) { auto detail = get_request_detail(request); - std::string data = this->valve_json_(obj, detail); + auto data = this->valve_json_(obj, detail); request->send(200, "application/json", data.c_str()); return; } @@ -1746,13 +1747,13 @@ void WebServer::handle_valve_request(AsyncWebServerRequest *request, const UrlMa } request->send(404); } -std::string WebServer::valve_state_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> WebServer::valve_state_json_generator(WebServer *web_server, void *source) { return web_server->valve_json_((valve::Valve *) (source), DETAIL_STATE); } -std::string WebServer::valve_all_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> WebServer::valve_all_json_generator(WebServer *web_server, void *source) { return web_server->valve_json_((valve::Valve *) (source), DETAIL_ALL); } -std::string WebServer::valve_json_(valve::Valve *obj, JsonDetail start_config) { +json::SerializationBuffer<> WebServer::valve_json_(valve::Valve *obj, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); @@ -1785,7 +1786,7 @@ void WebServer::handle_alarm_control_panel_request(AsyncWebServerRequest *reques if (request->method() == HTTP_GET && entity_match.action_is_empty) { auto detail = get_request_detail(request); - std::string data = this->alarm_control_panel_json_(obj, obj->get_state(), detail); + auto data = this->alarm_control_panel_json_(obj, obj->get_state(), detail); request->send(200, "application/json", data.c_str()); return; } @@ -1825,19 +1826,19 @@ 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) { +json::SerializationBuffer<> 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); } -std::string WebServer::alarm_control_panel_all_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> 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); } -std::string WebServer::alarm_control_panel_json_(alarm_control_panel::AlarmControlPanel *obj, - alarm_control_panel::AlarmControlPanelState value, - JsonDetail start_config) { +json::SerializationBuffer<> 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(); @@ -1866,7 +1867,7 @@ void WebServer::handle_water_heater_request(AsyncWebServerRequest *request, cons if (request->method() == HTTP_GET && entity_match.action_is_empty) { auto detail = get_request_detail(request); - std::string data = this->water_heater_json_(obj, detail); + auto data = this->water_heater_json_(obj, detail); request->send(200, "application/json", data.c_str()); return; } @@ -1902,14 +1903,14 @@ void WebServer::handle_water_heater_request(AsyncWebServerRequest *request, cons request->send(404); } -std::string WebServer::water_heater_state_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> WebServer::water_heater_state_json_generator(WebServer *web_server, void *source) { return web_server->water_heater_json_(static_cast(source), DETAIL_STATE); } -std::string WebServer::water_heater_all_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> WebServer::water_heater_all_json_generator(WebServer *web_server, void *source) { // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson return web_server->water_heater_json_(static_cast(source), DETAIL_ALL); } -std::string WebServer::water_heater_json_(water_heater::WaterHeater *obj, JsonDetail start_config) { +json::SerializationBuffer<> WebServer::water_heater_json_(water_heater::WaterHeater *obj, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); char buf[PSTR_LOCAL_SIZE]; @@ -1971,7 +1972,7 @@ void WebServer::handle_infrared_request(AsyncWebServerRequest *request, const Ur if (request->method() == HTTP_GET && entity_match.action_is_empty) { auto detail = get_request_detail(request); - std::string data = this->infrared_json_(obj, detail); + auto data = this->infrared_json_(obj, detail); request->send(200, ESPHOME_F("application/json"), data.c_str()); return; } @@ -2031,12 +2032,12 @@ void WebServer::handle_infrared_request(AsyncWebServerRequest *request, const Ur request->send(404); } -std::string WebServer::infrared_all_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> WebServer::infrared_all_json_generator(WebServer *web_server, void *source) { // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson return web_server->infrared_json_(static_cast(source), DETAIL_ALL); } -std::string WebServer::infrared_json_(infrared::Infrared *obj, JsonDetail start_config) { +json::SerializationBuffer<> WebServer::infrared_json_(infrared::Infrared *obj, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); @@ -2071,7 +2072,7 @@ void WebServer::handle_event_request(AsyncWebServerRequest *request, const UrlMa // Note: request->method() is always HTTP_GET here (canHandle ensures this) if (entity_match.action_is_empty) { auto detail = get_request_detail(request); - std::string data = this->event_json_(obj, StringRef(), detail); + auto data = this->event_json_(obj, StringRef(), detail); request->send(200, "application/json", data.c_str()); return; } @@ -2081,16 +2082,16 @@ void WebServer::handle_event_request(AsyncWebServerRequest *request, const UrlMa static StringRef get_event_type(event::Event *event) { return event ? event->get_last_event_type() : StringRef(); } -std::string WebServer::event_state_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> 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); } // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson -std::string WebServer::event_all_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> 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); } -std::string WebServer::event_json_(event::Event *obj, StringRef event_type, JsonDetail start_config) { +json::SerializationBuffer<> WebServer::event_json_(event::Event *obj, StringRef event_type, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); @@ -2124,7 +2125,7 @@ void WebServer::handle_update_request(AsyncWebServerRequest *request, const UrlM if (request->method() == HTTP_GET && entity_match.action_is_empty) { auto detail = get_request_detail(request); - std::string data = this->update_json_(obj, detail); + auto data = this->update_json_(obj, detail); request->send(200, "application/json", data.c_str()); return; } @@ -2140,15 +2141,15 @@ void WebServer::handle_update_request(AsyncWebServerRequest *request, const UrlM } request->send(404); } -std::string WebServer::update_state_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> 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); } -std::string WebServer::update_all_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> 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); } -std::string WebServer::update_json_(update::UpdateEntity *obj, JsonDetail start_config) { +json::SerializationBuffer<> 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 6afe618b59..76c1c8b0bd 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -2,6 +2,7 @@ #include "list_entities.h" +#include "esphome/components/json/json_util.h" #include "esphome/components/web_server_base/web_server_base.h" #ifdef USE_WEBSERVER #include "esphome/core/component.h" @@ -103,7 +104,7 @@ enum JsonDetail { DETAIL_ALL, DETAIL_STATE }; can be forgotten. */ #if !defined(USE_ESP32) && defined(USE_ARDUINO) -using message_generator_t = std::string(WebServer *, void *); +using message_generator_t = json::SerializationBuffer<>(WebServer *, void *); class DeferredUpdateEventSourceList; class DeferredUpdateEventSource : public AsyncEventSource { @@ -257,7 +258,7 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { void handle_index_request(AsyncWebServerRequest *request); /// Return the webserver configuration as JSON. - std::string get_config_json(); + json::SerializationBuffer<> get_config_json(); #ifdef USE_WEBSERVER_CSS_INCLUDE /// Handle included css request under '/0.css'. @@ -279,8 +280,8 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { /// Handle a sensor request under '/sensor/'. void handle_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match); - static std::string sensor_state_json_generator(WebServer *web_server, void *source); - static std::string sensor_all_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> sensor_state_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> sensor_all_json_generator(WebServer *web_server, void *source); #endif #ifdef USE_SWITCH @@ -289,8 +290,8 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { /// Handle a switch request under '/switch//'. void handle_switch_request(AsyncWebServerRequest *request, const UrlMatch &match); - static std::string switch_state_json_generator(WebServer *web_server, void *source); - static std::string switch_all_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> switch_state_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> switch_all_json_generator(WebServer *web_server, void *source); #endif #ifdef USE_BUTTON @@ -298,7 +299,7 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { void handle_button_request(AsyncWebServerRequest *request, const UrlMatch &match); // Buttons are stateless, so there is no button_state_json_generator - static std::string button_all_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> button_all_json_generator(WebServer *web_server, void *source); #endif #ifdef USE_BINARY_SENSOR @@ -307,8 +308,8 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { /// Handle a binary sensor request under '/binary_sensor/'. void handle_binary_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match); - 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); + static json::SerializationBuffer<> binary_sensor_state_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> binary_sensor_all_json_generator(WebServer *web_server, void *source); #endif #ifdef USE_FAN @@ -317,8 +318,8 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { /// Handle a fan request under '/fan//'. void handle_fan_request(AsyncWebServerRequest *request, const UrlMatch &match); - static std::string fan_state_json_generator(WebServer *web_server, void *source); - static std::string fan_all_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> fan_state_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> fan_all_json_generator(WebServer *web_server, void *source); #endif #ifdef USE_LIGHT @@ -327,8 +328,8 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { /// Handle a light request under '/light//'. void handle_light_request(AsyncWebServerRequest *request, const UrlMatch &match); - static std::string light_state_json_generator(WebServer *web_server, void *source); - static std::string light_all_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> light_state_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> light_all_json_generator(WebServer *web_server, void *source); #endif #ifdef USE_TEXT_SENSOR @@ -337,8 +338,8 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { /// Handle a text sensor request under '/text_sensor/'. void handle_text_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match); - 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); + static json::SerializationBuffer<> text_sensor_state_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> text_sensor_all_json_generator(WebServer *web_server, void *source); #endif #ifdef USE_COVER @@ -347,8 +348,8 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { /// Handle a cover request under '/cover//'. void handle_cover_request(AsyncWebServerRequest *request, const UrlMatch &match); - static std::string cover_state_json_generator(WebServer *web_server, void *source); - static std::string cover_all_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> cover_state_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> cover_all_json_generator(WebServer *web_server, void *source); #endif #ifdef USE_NUMBER @@ -356,8 +357,8 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { /// Handle a number request under '/number/'. void handle_number_request(AsyncWebServerRequest *request, const UrlMatch &match); - static std::string number_state_json_generator(WebServer *web_server, void *source); - static std::string number_all_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> number_state_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> number_all_json_generator(WebServer *web_server, void *source); #endif #ifdef USE_DATETIME_DATE @@ -365,8 +366,8 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { /// Handle a date request under '/date/'. void handle_date_request(AsyncWebServerRequest *request, const UrlMatch &match); - static std::string date_state_json_generator(WebServer *web_server, void *source); - static std::string date_all_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> date_state_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> date_all_json_generator(WebServer *web_server, void *source); #endif #ifdef USE_DATETIME_TIME @@ -374,8 +375,8 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { /// Handle a time request under '/time/'. void handle_time_request(AsyncWebServerRequest *request, const UrlMatch &match); - static std::string time_state_json_generator(WebServer *web_server, void *source); - static std::string time_all_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> time_state_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> time_all_json_generator(WebServer *web_server, void *source); #endif #ifdef USE_DATETIME_DATETIME @@ -383,8 +384,8 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { /// Handle a datetime request under '/datetime/'. void handle_datetime_request(AsyncWebServerRequest *request, const UrlMatch &match); - static std::string datetime_state_json_generator(WebServer *web_server, void *source); - static std::string datetime_all_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> datetime_state_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> datetime_all_json_generator(WebServer *web_server, void *source); #endif #ifdef USE_TEXT @@ -392,8 +393,8 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { /// Handle a text input request under '/text/'. void handle_text_request(AsyncWebServerRequest *request, const UrlMatch &match); - static std::string text_state_json_generator(WebServer *web_server, void *source); - static std::string text_all_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> text_state_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> text_all_json_generator(WebServer *web_server, void *source); #endif #ifdef USE_SELECT @@ -401,8 +402,8 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { /// Handle a select request under '/select/'. void handle_select_request(AsyncWebServerRequest *request, const UrlMatch &match); - static std::string select_state_json_generator(WebServer *web_server, void *source); - static std::string select_all_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> select_state_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> select_all_json_generator(WebServer *web_server, void *source); #endif #ifdef USE_CLIMATE @@ -410,8 +411,8 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { /// Handle a climate request under '/climate/'. void handle_climate_request(AsyncWebServerRequest *request, const UrlMatch &match); - static std::string climate_state_json_generator(WebServer *web_server, void *source); - static std::string climate_all_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> climate_state_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> climate_all_json_generator(WebServer *web_server, void *source); #endif #ifdef USE_LOCK @@ -420,8 +421,8 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { /// Handle a lock request under '/lock//'. void handle_lock_request(AsyncWebServerRequest *request, const UrlMatch &match); - static std::string lock_state_json_generator(WebServer *web_server, void *source); - static std::string lock_all_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> lock_state_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> lock_all_json_generator(WebServer *web_server, void *source); #endif #ifdef USE_VALVE @@ -430,8 +431,8 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { /// Handle a valve request under '/valve//'. void handle_valve_request(AsyncWebServerRequest *request, const UrlMatch &match); - static std::string valve_state_json_generator(WebServer *web_server, void *source); - static std::string valve_all_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> valve_state_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> valve_all_json_generator(WebServer *web_server, void *source); #endif #ifdef USE_ALARM_CONTROL_PANEL @@ -440,8 +441,8 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { /// Handle a alarm_control_panel request under '/alarm_control_panel/'. void handle_alarm_control_panel_request(AsyncWebServerRequest *request, const UrlMatch &match); - 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); + static json::SerializationBuffer<> alarm_control_panel_state_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> alarm_control_panel_all_json_generator(WebServer *web_server, void *source); #endif #ifdef USE_WATER_HEATER @@ -450,22 +451,22 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { /// Handle a water_heater request under '/water_heater//'. void handle_water_heater_request(AsyncWebServerRequest *request, const UrlMatch &match); - static std::string water_heater_state_json_generator(WebServer *web_server, void *source); - static std::string water_heater_all_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> water_heater_state_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> water_heater_all_json_generator(WebServer *web_server, void *source); #endif #ifdef USE_INFRARED /// Handle an infrared request under '/infrared//transmit'. void handle_infrared_request(AsyncWebServerRequest *request, const UrlMatch &match); - static std::string infrared_all_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> infrared_all_json_generator(WebServer *web_server, void *source); #endif #ifdef USE_EVENT void on_event(event::Event *obj) override; - static std::string event_state_json_generator(WebServer *web_server, void *source); - static std::string event_all_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> event_state_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> event_all_json_generator(WebServer *web_server, void *source); /// Handle a event request under '/event'. void handle_event_request(AsyncWebServerRequest *request, const UrlMatch &match); @@ -477,8 +478,8 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { /// Handle a update request under '/update/'. void handle_update_request(AsyncWebServerRequest *request, const UrlMatch &match); - static std::string update_state_json_generator(WebServer *web_server, void *source); - static std::string update_all_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> update_state_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> update_all_json_generator(WebServer *web_server, void *source); #endif /// Override the web handler's canHandle method. @@ -586,71 +587,74 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { private: #ifdef USE_SENSOR - std::string sensor_json_(sensor::Sensor *obj, float value, JsonDetail start_config); + json::SerializationBuffer<> 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); + json::SerializationBuffer<> switch_json_(switch_::Switch *obj, bool value, JsonDetail start_config); #endif #ifdef USE_BUTTON - std::string button_json_(button::Button *obj, JsonDetail start_config); + json::SerializationBuffer<> 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); + json::SerializationBuffer<> 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); + json::SerializationBuffer<> fan_json_(fan::Fan *obj, JsonDetail start_config); #endif #ifdef USE_LIGHT - std::string light_json_(light::LightState *obj, JsonDetail start_config); + json::SerializationBuffer<> 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); + json::SerializationBuffer<> 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); + json::SerializationBuffer<> cover_json_(cover::Cover *obj, JsonDetail start_config); #endif #ifdef USE_NUMBER - std::string number_json_(number::Number *obj, float value, JsonDetail start_config); + json::SerializationBuffer<> 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); + json::SerializationBuffer<> date_json_(datetime::DateEntity *obj, JsonDetail start_config); #endif #ifdef USE_DATETIME_TIME - std::string time_json_(datetime::TimeEntity *obj, JsonDetail start_config); + json::SerializationBuffer<> time_json_(datetime::TimeEntity *obj, JsonDetail start_config); #endif #ifdef USE_DATETIME_DATETIME - std::string datetime_json_(datetime::DateTimeEntity *obj, JsonDetail start_config); + json::SerializationBuffer<> 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); + json::SerializationBuffer<> text_json_(text::Text *obj, const std::string &value, JsonDetail start_config); #endif #ifdef USE_SELECT - std::string select_json_(select::Select *obj, StringRef value, JsonDetail start_config); + json::SerializationBuffer<> select_json_(select::Select *obj, StringRef value, JsonDetail start_config); #endif #ifdef USE_CLIMATE - std::string climate_json_(climate::Climate *obj, JsonDetail start_config); + json::SerializationBuffer<> 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); + json::SerializationBuffer<> 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); + json::SerializationBuffer<> 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); + json::SerializationBuffer<> 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, StringRef event_type, JsonDetail start_config); + json::SerializationBuffer<> event_json_(event::Event *obj, StringRef event_type, JsonDetail start_config); #endif #ifdef USE_WATER_HEATER - std::string water_heater_json_(water_heater::WaterHeater *obj, JsonDetail start_config); + json::SerializationBuffer<> water_heater_json_(water_heater::WaterHeater *obj, JsonDetail start_config); #endif #ifdef USE_INFRARED - std::string infrared_json_(infrared::Infrared *obj, JsonDetail start_config); + json::SerializationBuffer<> infrared_json_(infrared::Infrared *obj, JsonDetail start_config); #endif #ifdef USE_UPDATE - std::string update_json_(update::UpdateEntity *obj, JsonDetail start_config); + json::SerializationBuffer<> update_json_(update::UpdateEntity *obj, JsonDetail start_config); #endif }; diff --git a/esphome/components/web_server_idf/web_server_idf.cpp b/esphome/components/web_server_idf/web_server_idf.cpp index 1798159e7f..4034a22586 100644 --- a/esphome/components/web_server_idf/web_server_idf.cpp +++ b/esphome/components/web_server_idf/web_server_idf.cpp @@ -563,7 +563,7 @@ AsyncEventSourceResponse::AsyncEventSourceResponse(const AsyncWebServerRequest * // Configure reconnect timeout and send config // this should always go through since the tcp send buffer is empty on connect - std::string message = ws->get_config_json(); + auto message = ws->get_config_json(); this->try_send_nodefer(message.c_str(), "ping", millis(), 30000); #ifdef USE_WEBSERVER_SORTING @@ -617,7 +617,7 @@ void AsyncEventSourceResponse::deq_push_back_with_dedup_(void *source, message_g void AsyncEventSourceResponse::process_deferred_queue_() { while (!deferred_queue_.empty()) { DeferredEvent &de = deferred_queue_.front(); - std::string message = de.message_generator_(web_server_, de.source_); + auto message = de.message_generator_(web_server_, de.source_); if (this->try_send_nodefer(message.c_str(), "state")) { // O(n) but memory efficiency is more important than speed here which is why std::vector was chosen deferred_queue_.erase(deferred_queue_.begin()); @@ -854,7 +854,7 @@ void AsyncEventSourceResponse::deferrable_send_state(void *source, const char *e // trying to send first deq_push_back_with_dedup_(source, message_generator); } else { - std::string message = message_generator(web_server_, source); + auto message = message_generator(web_server_, source); if (!this->try_send_nodefer(message.c_str(), "state")) { deq_push_back_with_dedup_(source, message_generator); } diff --git a/esphome/components/web_server_idf/web_server_idf.h b/esphome/components/web_server_idf/web_server_idf.h index 74601ffda8..76ddfa35fd 100644 --- a/esphome/components/web_server_idf/web_server_idf.h +++ b/esphome/components/web_server_idf/web_server_idf.h @@ -16,6 +16,7 @@ #include #ifdef USE_WEBSERVER +#include "esphome/components/json/json_util.h" #include "esphome/components/web_server/list_entities.h" #endif @@ -250,7 +251,7 @@ class AsyncWebHandler { class AsyncEventSource; class AsyncEventSourceResponse; -using message_generator_t = std::string(esphome::web_server::WebServer *, void *); +using message_generator_t = json::SerializationBuffer<>(esphome::web_server::WebServer *, void *); /* This class holds a pointer to the source component that wants to publish a state event, and a pointer to a function diff --git a/tests/components/json/common.yaml b/tests/components/json/common.yaml index c36c7f2a5a..c4bf6c3831 100644 --- a/tests/components/json/common.yaml +++ b/tests/components/json/common.yaml @@ -4,15 +4,16 @@ interval: - interval: 60s then: - lambda: |- - // Test build_json - std::string json_str = esphome::json::build_json([](JsonObject root) { + // Test build_json - returns SerializationBuffer, use auto to avoid heap allocation + auto json_buf = esphome::json::build_json([](JsonObject root) { root["sensor"] = "temperature"; root["value"] = 23.5; root["unit"] = "°C"; }); - ESP_LOGD("test", "Built JSON: %s", json_str.c_str()); + ESP_LOGD("test", "Built JSON: %s", json_buf.c_str()); - // Test parse_json + // Test parse_json - implicit conversion to std::string for backward compatibility + std::string json_str = json_buf; bool parse_ok = esphome::json::parse_json(json_str, [](JsonObject root) { if (root["sensor"].is() && root["value"].is()) { const char* sensor = root["sensor"]; @@ -26,10 +27,10 @@ interval: }); ESP_LOGD("test", "Parse result (JSON syntax only): %s", parse_ok ? "success" : "failed"); - // Test JsonBuilder class + // Test JsonBuilder class - returns SerializationBuffer esphome::json::JsonBuilder builder; JsonObject obj = builder.root(); obj["test"] = "direct_builder"; obj["count"] = 42; - std::string result = builder.serialize(); + auto result = builder.serialize(); ESP_LOGD("test", "JsonBuilder result: %s", result.c_str()); From 17a810b939ba7ab0019fa65149e2db2622d2466c Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 19 Feb 2026 15:14:48 -0500 Subject: [PATCH 114/197] [wifi] Sync output_power with PHY max TX power to prevent brownout (#14118) Co-authored-by: Claude Opus 4.6 --- esphome/components/wifi/__init__.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index e865de8663..afceec6c54 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -1,4 +1,5 @@ import logging +import math from esphome import automation from esphome.automation import Condition @@ -493,6 +494,13 @@ async def to_code(config): cg.add(var.set_passive_scan(True)) if CONF_OUTPUT_POWER in config: cg.add(var.set_output_power(config[CONF_OUTPUT_POWER])) + if CORE.is_esp32: + # Set PHY max TX power to match output_power so calibration also uses + # reduced power. This prevents brownout during PHY init on marginal + # power supplies, which is critical for OTA updates with rollback enabled. + # Kconfig range is 10-20, ESPHome allows 8.5-20.5 + phy_tx_power = max(10, min(20, math.ceil(config[CONF_OUTPUT_POWER]))) + add_idf_sdkconfig_option("CONFIG_ESP_PHY_MAX_WIFI_TX_POWER", phy_tx_power) # enable_on_boot defaults to true in C++ - only set if false if not config[CONF_ENABLE_ON_BOOT]: cg.add(var.set_enable_on_boot(False)) From cceb109303f16a8723b11e3e9bf9a26f985be197 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 19 Feb 2026 17:48:18 -0600 Subject: [PATCH 115/197] [uart] Always call pin setup for UART0 default pins on ESP-IDF (#14130) --- .../uart/uart_component_esp_idf.cpp | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/esphome/components/uart/uart_component_esp_idf.cpp b/esphome/components/uart/uart_component_esp_idf.cpp index 6c242220a6..ea7a09fee6 100644 --- a/esphome/components/uart/uart_component_esp_idf.cpp +++ b/esphome/components/uart/uart_component_esp_idf.cpp @@ -19,6 +19,13 @@ namespace esphome::uart { static const char *const TAG = "uart.idf"; +/// Check if a pin number matches one of the default UART0 GPIO pins. +/// These pins may have residual state from the boot console that requires +/// explicit reset before UART reconfiguration (ESP-IDF issue #17459). +static constexpr bool is_default_uart0_pin(int8_t pin_num) { + return pin_num == U0TXD_GPIO_NUM || pin_num == U0RXD_GPIO_NUM; +} + uart_config_t IDFUARTComponent::get_config_() { uart_parity_t parity = UART_PARITY_DISABLE; if (this->parity_ == UART_CONFIG_PARITY_EVEN) { @@ -150,20 +157,26 @@ void IDFUARTComponent::load_settings(bool dump_config) { // Commit 9ed617fb17 removed gpio_func_sel() calls from uart_set_pin(), which breaks // UART on default UART0 pins that may have residual state from boot console. // Reset these pins before configuring UART to ensure they're in a clean state. - if (tx == U0TXD_GPIO_NUM || tx == U0RXD_GPIO_NUM) { + if (is_default_uart0_pin(tx)) { gpio_reset_pin(static_cast(tx)); } - if (rx == U0TXD_GPIO_NUM || rx == U0RXD_GPIO_NUM) { + if (is_default_uart0_pin(rx)) { gpio_reset_pin(static_cast(rx)); } - // Setup pins after reset to preserve open drain/pullup/pulldown flags + // Setup pins after reset to configure GPIO direction and pull resistors. + // For UART0 default pins, setup() must always be called because gpio_reset_pin() + // above sets GPIO_MODE_DISABLE which disables the input buffer. Without setup(), + // uart_set_pin() on ESP-IDF 5.4.2+ does not re-enable the input buffer for + // IOMUX-connected pins, so the RX pin cannot receive data (see issue #10132). + // For other pins, only call setup() if pull or open-drain flags are set to avoid + // disturbing the default pin state which breaks some external components (#11823). auto setup_pin_if_needed = [](InternalGPIOPin *pin) { if (!pin) { return; } const auto mask = gpio::Flags::FLAG_OPEN_DRAIN | gpio::Flags::FLAG_PULLUP | gpio::Flags::FLAG_PULLDOWN; - if ((pin->get_flags() & mask) != gpio::Flags::FLAG_NONE) { + if (is_default_uart0_pin(pin->get_pin()) || (pin->get_flags() & mask) != gpio::Flags::FLAG_NONE) { pin->setup(); } }; From 1b4de55efda47b59e1572f432ef40b932311da38 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 19 Feb 2026 19:12:37 -0500 Subject: [PATCH 116/197] [pulse_counter] Fix PCNT glitch filter calculation off by 1000x (#14132) Co-authored-by: Claude Opus 4.6 --- esphome/components/pulse_counter/pulse_counter_sensor.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/pulse_counter/pulse_counter_sensor.cpp b/esphome/components/pulse_counter/pulse_counter_sensor.cpp index ef4cc980f6..5e62c0a410 100644 --- a/esphome/components/pulse_counter/pulse_counter_sensor.cpp +++ b/esphome/components/pulse_counter/pulse_counter_sensor.cpp @@ -117,7 +117,7 @@ bool HwPulseCounterStorage::pulse_counter_setup(InternalGPIOPin *pin) { } if (this->filter_us != 0) { - uint32_t max_glitch_ns = PCNT_LL_MAX_GLITCH_WIDTH * 1000000u / (uint32_t) esp_clk_apb_freq(); + uint32_t max_glitch_ns = PCNT_LL_MAX_GLITCH_WIDTH * 1000u / ((uint32_t) esp_clk_apb_freq() / 1000000u); pcnt_glitch_filter_config_t filter_config = { .max_glitch_ns = std::min(this->filter_us * 1000u, max_glitch_ns), }; From d29288547ec2d5b9e68b61c2186522efaad041b7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 19 Feb 2026 18:54:33 -0600 Subject: [PATCH 117/197] [core] Use constexpr for PROGMEM arrays (#14127) Co-authored-by: Claude Opus 4.6 --- esphome/cpp_generator.py | 2 +- tests/unit_tests/test_cpp_generator.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/cpp_generator.py b/esphome/cpp_generator.py index 83f2d6cf81..fe666bdd6e 100644 --- a/esphome/cpp_generator.py +++ b/esphome/cpp_generator.py @@ -424,7 +424,7 @@ class ProgmemAssignmentExpression(AssignmentExpression): super().__init__(type_, "", name, rhs) def __str__(self): - return f"static const {self.type} {self.name}[] PROGMEM = {self.rhs}" + return f"static constexpr {self.type} {self.name}[] PROGMEM = {self.rhs}" class StaticConstAssignmentExpression(AssignmentExpression): diff --git a/tests/unit_tests/test_cpp_generator.py b/tests/unit_tests/test_cpp_generator.py index 8755e6e2a1..049d21027f 100644 --- a/tests/unit_tests/test_cpp_generator.py +++ b/tests/unit_tests/test_cpp_generator.py @@ -325,7 +325,7 @@ class TestStatements: ), ( cg.ProgmemAssignmentExpression(ct.uint16, "foo", "bar"), - 'static const uint16_t foo[] PROGMEM = "bar"', + 'static constexpr uint16_t foo[] PROGMEM = "bar"', ), ), ) From 94712b3961dd962d0a43636301c5edccd08121b2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 19 Feb 2026 18:54:44 -0600 Subject: [PATCH 118/197] [esp8266][web_server] Use constexpr for PROGMEM arrays in codegen (#14128) Co-authored-by: Claude Opus 4.6 --- esphome/components/esp8266/gpio.py | 4 ++-- esphome/components/web_server/__init__.py | 6 ++---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/esphome/components/esp8266/gpio.py b/esphome/components/esp8266/gpio.py index 2e8d6496bc..43508afaf9 100644 --- a/esphome/components/esp8266/gpio.py +++ b/esphome/components/esp8266/gpio.py @@ -202,11 +202,11 @@ async def add_pin_initial_states_array(): cg.add_global( cg.RawExpression( - f"const uint8_t ESPHOME_ESP8266_GPIO_INITIAL_MODE[16] PROGMEM = {{{initial_modes_s}}}" + f"constexpr uint8_t ESPHOME_ESP8266_GPIO_INITIAL_MODE[16] PROGMEM = {{{initial_modes_s}}}" ) ) cg.add_global( cg.RawExpression( - f"const uint8_t ESPHOME_ESP8266_GPIO_INITIAL_LEVEL[16] PROGMEM = {{{initial_levels_s}}}" + f"constexpr uint8_t ESPHOME_ESP8266_GPIO_INITIAL_LEVEL[16] PROGMEM = {{{initial_levels_s}}}" ) ) diff --git a/esphome/components/web_server/__init__.py b/esphome/components/web_server/__init__.py index 294a5e0a15..9305a2de61 100644 --- a/esphome/components/web_server/__init__.py +++ b/esphome/components/web_server/__init__.py @@ -280,10 +280,8 @@ def add_resource_as_progmem( content_encoded = gzip.compress(content_encoded) content_encoded_size = len(content_encoded) bytes_as_int = ", ".join(str(x) for x in content_encoded) - uint8_t = f"const uint8_t ESPHOME_WEBSERVER_{resource_name}[{content_encoded_size}] PROGMEM = {{{bytes_as_int}}}" - size_t = ( - f"const size_t ESPHOME_WEBSERVER_{resource_name}_SIZE = {content_encoded_size}" - ) + uint8_t = f"constexpr uint8_t ESPHOME_WEBSERVER_{resource_name}[{content_encoded_size}] PROGMEM = {{{bytes_as_int}}}" + size_t = f"constexpr size_t ESPHOME_WEBSERVER_{resource_name}_SIZE = {content_encoded_size}" cg.add_global(cg.RawExpression(uint8_t)) cg.add_global(cg.RawExpression(size_t)) From c1265a9490711813a6036b8a772dd36e3e871089 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 19 Feb 2026 18:54:57 -0600 Subject: [PATCH 119/197] [core] Use constexpr for hand-written PROGMEM arrays in C++ (#14129) Co-authored-by: Claude Opus 4.6 --- .../components/api/api_frame_helper_noise.cpp | 2 +- esphome/components/bme680/bme680.cpp | 8 +++--- .../components/captive_portal/captive_index.h | 4 +-- esphome/components/display/display.cpp | 6 ++-- esphome/components/ili9xxx/ili9xxx_init.h | 28 +++++++++---------- esphome/components/max7219/max7219.cpp | 2 +- esphome/components/max7219digit/max7219font.h | 2 +- .../components/ssd1306_base/ssd1306_base.cpp | 2 +- esphome/components/st7735/st7735.cpp | 2 +- esphome/components/tm1621/tm1621.cpp | 2 +- esphome/components/tm1637/tm1637.cpp | 2 +- esphome/components/tm1638/sevenseg.h | 2 +- .../components/web_server/server_index_v2.h | 4 +-- .../components/web_server/server_index_v3.h | 4 +-- 14 files changed, 35 insertions(+), 35 deletions(-) diff --git a/esphome/components/api/api_frame_helper_noise.cpp b/esphome/components/api/api_frame_helper_noise.cpp index 492988128a..2aad732f7f 100644 --- a/esphome/components/api/api_frame_helper_noise.cpp +++ b/esphome/components/api/api_frame_helper_noise.cpp @@ -19,7 +19,7 @@ namespace esphome::api { static const char *const TAG = "api.noise"; #ifdef USE_ESP8266 -static const char PROLOGUE_INIT[] PROGMEM = "NoiseAPIInit"; +static constexpr char PROLOGUE_INIT[] PROGMEM = "NoiseAPIInit"; #else static const char *const PROLOGUE_INIT = "NoiseAPIInit"; #endif diff --git a/esphome/components/bme680/bme680.cpp b/esphome/components/bme680/bme680.cpp index 5e52c84b3d..e3cd80de00 100644 --- a/esphome/components/bme680/bme680.cpp +++ b/esphome/components/bme680/bme680.cpp @@ -22,11 +22,11 @@ static const uint8_t BME680_REGISTER_CHIPID = 0xD0; static const uint8_t BME680_REGISTER_FIELD0 = 0x1D; -const float BME680_GAS_LOOKUP_TABLE_1[16] PROGMEM = {0.0, 0.0, 0.0, 0.0, 0.0, -1.0, 0.0, -0.8, - 0.0, 0.0, -0.2, -0.5, 0.0, -1.0, 0.0, 0.0}; +constexpr float BME680_GAS_LOOKUP_TABLE_1[16] PROGMEM = {0.0, 0.0, 0.0, 0.0, 0.0, -1.0, 0.0, -0.8, + 0.0, 0.0, -0.2, -0.5, 0.0, -1.0, 0.0, 0.0}; -const float BME680_GAS_LOOKUP_TABLE_2[16] PROGMEM = {0.0, 0.0, 0.0, 0.0, 0.1, 0.7, 0.0, -0.8, - -0.1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0}; +constexpr float BME680_GAS_LOOKUP_TABLE_2[16] PROGMEM = {0.0, 0.0, 0.0, 0.0, 0.1, 0.7, 0.0, -0.8, + -0.1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0}; [[maybe_unused]] static const char *oversampling_to_str(BME680Oversampling oversampling) { switch (oversampling) { diff --git a/esphome/components/captive_portal/captive_index.h b/esphome/components/captive_portal/captive_index.h index 645ebb7a2f..a81edc1900 100644 --- a/esphome/components/captive_portal/captive_index.h +++ b/esphome/components/captive_portal/captive_index.h @@ -6,7 +6,7 @@ namespace esphome::captive_portal { #ifdef USE_CAPTIVE_PORTAL_GZIP -const uint8_t INDEX_GZ[] PROGMEM = { +constexpr uint8_t INDEX_GZ[] PROGMEM = { 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x95, 0x16, 0x6b, 0x8f, 0xdb, 0x36, 0xf2, 0x7b, 0x7e, 0x05, 0x8f, 0x49, 0xbb, 0x52, 0xb3, 0x7a, 0x7a, 0xed, 0x6c, 0x24, 0x51, 0x45, 0x9a, 0xbb, 0xa2, 0x05, 0x9a, 0x36, 0xc0, 0x6e, 0x73, 0x1f, 0x82, 0x00, 0x4b, 0x53, 0x23, 0x8b, 0x31, 0x45, 0xea, 0x48, 0xca, 0x8f, 0x18, 0xbe, 0xdf, @@ -86,7 +86,7 @@ const uint8_t INDEX_GZ[] PROGMEM = { 0xfc, 0xda, 0xd1, 0xf8, 0xe9, 0xa3, 0xe1, 0xa6, 0xfb, 0x1f, 0x53, 0x58, 0x46, 0xb2, 0xf9, 0x0a, 0x00, 0x00}; #else // Brotli (default, smaller) -const uint8_t INDEX_BR[] PROGMEM = { +constexpr uint8_t INDEX_BR[] PROGMEM = { 0x1b, 0xf8, 0x0a, 0x00, 0x64, 0x5a, 0xd3, 0xfa, 0xe7, 0xf3, 0x62, 0xd8, 0x06, 0x1b, 0xe9, 0x6a, 0x8a, 0x81, 0x2b, 0xb5, 0x49, 0x14, 0x37, 0xdc, 0x9e, 0x1a, 0xcb, 0x56, 0x87, 0xfb, 0xff, 0xf7, 0x73, 0x75, 0x12, 0x0a, 0xd6, 0x48, 0x84, 0xc6, 0x21, 0xa4, 0x6d, 0xb5, 0x71, 0xef, 0x13, 0xbe, 0x4e, 0x54, 0xf1, 0x64, 0x8f, 0x3f, 0xcc, 0x9a, 0x78, diff --git a/esphome/components/display/display.cpp b/esphome/components/display/display.cpp index 53a087803c..2bd7d03600 100644 --- a/esphome/components/display/display.cpp +++ b/esphome/components/display/display.cpp @@ -810,9 +810,9 @@ bool Display::clamp_y_(int y, int h, int &min_y, int &max_y) { return min_y < max_y; } -const uint8_t TESTCARD_FONT[3][8] PROGMEM = {{0x41, 0x7F, 0x7F, 0x09, 0x19, 0x7F, 0x66, 0x00}, // 'R' - {0x1C, 0x3E, 0x63, 0x41, 0x51, 0x73, 0x72, 0x00}, // 'G' - {0x41, 0x7F, 0x7F, 0x49, 0x49, 0x7F, 0x36, 0x00}}; // 'B' +constexpr uint8_t TESTCARD_FONT[3][8] PROGMEM = {{0x41, 0x7F, 0x7F, 0x09, 0x19, 0x7F, 0x66, 0x00}, // 'R' + {0x1C, 0x3E, 0x63, 0x41, 0x51, 0x73, 0x72, 0x00}, // 'G' + {0x41, 0x7F, 0x7F, 0x49, 0x49, 0x7F, 0x36, 0x00}}; // 'B' void Display::test_card() { int w = get_width(), h = get_height(), image_w, image_h; diff --git a/esphome/components/ili9xxx/ili9xxx_init.h b/esphome/components/ili9xxx/ili9xxx_init.h index 7b176ed57a..f0c6a94a65 100644 --- a/esphome/components/ili9xxx/ili9xxx_init.h +++ b/esphome/components/ili9xxx/ili9xxx_init.h @@ -7,7 +7,7 @@ namespace esphome { namespace ili9xxx { // clang-format off -static const uint8_t PROGMEM INITCMD_M5STACK[] = { +static constexpr uint8_t PROGMEM INITCMD_M5STACK[] = { 0xEF, 3, 0x03, 0x80, 0x02, 0xCF, 3, 0x00, 0xC1, 0x30, 0xED, 4, 0x64, 0x03, 0x12, 0x81, @@ -37,7 +37,7 @@ static const uint8_t PROGMEM INITCMD_M5STACK[] = { 0x00 // End of list }; -static const uint8_t PROGMEM INITCMD_M5CORE[] = { +static constexpr uint8_t PROGMEM INITCMD_M5CORE[] = { ILI9XXX_SETEXTC, 3, 0xFF,0x93,0x42, // Turn on the external command ILI9XXX_PWCTR1 , 2, 0x12, 0x12, ILI9XXX_PWCTR2 , 1, 0x03, @@ -56,7 +56,7 @@ static const uint8_t PROGMEM INITCMD_M5CORE[] = { -static const uint8_t PROGMEM INITCMD_ILI9341[] = { +static constexpr uint8_t PROGMEM INITCMD_ILI9341[] = { 0xEF, 3, 0x03, 0x80, 0x02, 0xCF, 3, 0x00, 0xC1, 0x30, 0xED, 4, 0x64, 0x03, 0x12, 0x81, @@ -86,7 +86,7 @@ static const uint8_t PROGMEM INITCMD_ILI9341[] = { 0x00 // End of list }; -static const uint8_t PROGMEM INITCMD_ILI9481[] = { +static constexpr uint8_t PROGMEM INITCMD_ILI9481[] = { ILI9XXX_SLPOUT , 0x80, // Exit sleep mode ILI9XXX_PWSET , 3, 0x07, 0x41, 0x1D, ILI9XXX_VMCTR , 3, 0x00, 0x1C, 0x1F, @@ -105,7 +105,7 @@ static const uint8_t PROGMEM INITCMD_ILI9481[] = { 0x00 // end }; -static const uint8_t PROGMEM INITCMD_ILI9481_18[] = { +static constexpr uint8_t PROGMEM INITCMD_ILI9481_18[] = { ILI9XXX_SLPOUT , 0x80, // Exit sleep mode ILI9XXX_PWSET , 3, 0x07, 0x41, 0x1D, ILI9XXX_VMCTR , 3, 0x00, 0x1C, 0x1F, @@ -124,7 +124,7 @@ static const uint8_t PROGMEM INITCMD_ILI9481_18[] = { 0x00 // end }; -static const uint8_t PROGMEM INITCMD_ILI9486[] = { +static constexpr uint8_t PROGMEM INITCMD_ILI9486[] = { ILI9XXX_SLPOUT, 0x80, ILI9XXX_PIXFMT, 1, 0x55, ILI9XXX_PWCTR3, 1, 0x44, @@ -173,7 +173,7 @@ static const uint8_t INITCMD_WAVESHARE_RES_3_5[] = { 0x00 // End of list }; -static const uint8_t PROGMEM INITCMD_ILI9488_A[] = { +static constexpr uint8_t PROGMEM INITCMD_ILI9488_A[] = { ILI9XXX_GMCTRP1,15, 0x00, 0x03, 0x09, 0x08, 0x16, 0x0A, 0x3F, 0x78, 0x4C, 0x09, 0x0A, 0x08, 0x16, 0x1A, 0x0F, ILI9XXX_GMCTRN1,15, 0x00, 0x16, 0x19, 0x03, 0x0F, 0x05, 0x32, 0x45, 0x46, 0x04, 0x0E, 0x0D, 0x35, 0x37, 0x0F, @@ -206,7 +206,7 @@ static const uint8_t PROGMEM INITCMD_ILI9488_A[] = { 0x00 // end }; -static const uint8_t PROGMEM INITCMD_ST7796[] = { +static constexpr uint8_t PROGMEM INITCMD_ST7796[] = { // This ST7796S initilization routine was copied from https://github.com/prenticedavid/Adafruit_ST7796S_kbv/blob/master/Adafruit_ST7796S_kbv.cpp ILI9XXX_SWRESET, 0x80, // Soft reset, then delay 150 ms ILI9XXX_CSCON, 1, 0xC3, // ?? Unlock Manufacturer @@ -226,7 +226,7 @@ static const uint8_t PROGMEM INITCMD_ST7796[] = { 0x00 // End of list }; -static const uint8_t PROGMEM INITCMD_S3BOX[] = { +static constexpr uint8_t PROGMEM INITCMD_S3BOX[] = { 0xEF, 3, 0x03, 0x80, 0x02, 0xCF, 3, 0x00, 0xC1, 0x30, 0xED, 4, 0x64, 0x03, 0x12, 0x81, @@ -256,7 +256,7 @@ static const uint8_t PROGMEM INITCMD_S3BOX[] = { 0x00 // End of list }; -static const uint8_t PROGMEM INITCMD_S3BOXLITE[] = { +static constexpr uint8_t PROGMEM INITCMD_S3BOXLITE[] = { 0xEF, 3, 0x03, 0x80, 0x02, 0xCF, 3, 0x00, 0xC1, 0x30, 0xED, 4, 0x64, 0x03, 0x12, 0x81, @@ -286,7 +286,7 @@ static const uint8_t PROGMEM INITCMD_S3BOXLITE[] = { 0x00 // End of list }; -static const uint8_t PROGMEM INITCMD_ST7789V[] = { +static constexpr uint8_t PROGMEM INITCMD_ST7789V[] = { ILI9XXX_SLPOUT , 0x80, // Exit Sleep ILI9XXX_DISPON , 0x80, // Display on ILI9XXX_MADCTL , 1, 0x08, // Memory Access Control, BGR @@ -313,7 +313,7 @@ static const uint8_t PROGMEM INITCMD_ST7789V[] = { 0x00 // End of list }; -static const uint8_t PROGMEM INITCMD_GC9A01A[] = { +static constexpr uint8_t PROGMEM INITCMD_GC9A01A[] = { 0xEF, 0, 0xEB, 1, 0x14, // ? 0xFE, 0, @@ -367,7 +367,7 @@ static const uint8_t PROGMEM INITCMD_GC9A01A[] = { 0x00 // End of list }; -static const uint8_t PROGMEM INITCMD_GC9D01N[] = { +static constexpr uint8_t PROGMEM INITCMD_GC9D01N[] = { // Enable Inter_command 0xFE, 0, // Inter Register Enable 1 (FEh) 0xEF, 0, // Inter Register Enable 2 (EFh) @@ -426,7 +426,7 @@ static const uint8_t PROGMEM INITCMD_GC9D01N[] = { 0x00 // End of list }; -static const uint8_t PROGMEM INITCMD_ST7735[] = { +static constexpr uint8_t PROGMEM INITCMD_ST7735[] = { ILI9XXX_SWRESET, 0, // Soft reset, then delay 10ms ILI9XXX_DELAY(10), ILI9XXX_SLPOUT , 0, // Exit Sleep, delay diff --git a/esphome/components/max7219/max7219.cpp b/esphome/components/max7219/max7219.cpp index d701e6fc86..bec62ea005 100644 --- a/esphome/components/max7219/max7219.cpp +++ b/esphome/components/max7219/max7219.cpp @@ -15,7 +15,7 @@ static const uint8_t MAX7219_REGISTER_SHUTDOWN = 0x0C; static const uint8_t MAX7219_REGISTER_TEST = 0x0F; static const uint8_t MAX7219_UNKNOWN_CHAR = 0b11111111; -const uint8_t MAX7219_ASCII_TO_RAW[95] PROGMEM = { +constexpr uint8_t MAX7219_ASCII_TO_RAW[95] PROGMEM = { 0b00000000, // ' ', ord 0x20 0b10110000, // '!', ord 0x21 0b00100010, // '"', ord 0x22 diff --git a/esphome/components/max7219digit/max7219font.h b/esphome/components/max7219digit/max7219font.h index 22d64d1ecd..53674dc60f 100644 --- a/esphome/components/max7219digit/max7219font.h +++ b/esphome/components/max7219digit/max7219font.h @@ -7,7 +7,7 @@ namespace max7219digit { // bit patterns for the CP437 font -const uint8_t MAX7219_DOT_MATRIX_FONT[256][8] PROGMEM = { +constexpr uint8_t MAX7219_DOT_MATRIX_FONT[256][8] PROGMEM = { {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 0x00 {0x7E, 0x81, 0x95, 0xB1, 0xB1, 0x95, 0x81, 0x7E}, // 0x01 {0x7E, 0xFF, 0xEB, 0xCF, 0xCF, 0xEB, 0xFF, 0x7E}, // 0x02 diff --git a/esphome/components/ssd1306_base/ssd1306_base.cpp b/esphome/components/ssd1306_base/ssd1306_base.cpp index be99bd93da..5bd83ec8a8 100644 --- a/esphome/components/ssd1306_base/ssd1306_base.cpp +++ b/esphome/components/ssd1306_base/ssd1306_base.cpp @@ -49,7 +49,7 @@ struct ModelDimensions { uint8_t width; uint8_t height; }; -static const ModelDimensions MODEL_DIMS[] PROGMEM = { +static constexpr ModelDimensions MODEL_DIMS[] PROGMEM = { {128, 32}, // SSD1306_MODEL_128_32 {128, 64}, // SSD1306_MODEL_128_64 {96, 16}, // SSD1306_MODEL_96_16 diff --git a/esphome/components/st7735/st7735.cpp b/esphome/components/st7735/st7735.cpp index 1a74b5ce1e..58459b79bb 100644 --- a/esphome/components/st7735/st7735.cpp +++ b/esphome/components/st7735/st7735.cpp @@ -68,7 +68,7 @@ static const uint8_t ST7735_GMCTRP1 = 0xE0; static const uint8_t ST7735_GMCTRN1 = 0xE1; // clang-format off -static const uint8_t PROGMEM +static constexpr uint8_t PROGMEM BCMD[] = { // Init commands for 7735B screens 18, // 18 commands in list: ST77XX_SWRESET, ST_CMD_DELAY, // 1: Software reset, no args, w/delay diff --git a/esphome/components/tm1621/tm1621.cpp b/esphome/components/tm1621/tm1621.cpp index 6859973857..c82d306460 100644 --- a/esphome/components/tm1621/tm1621.cpp +++ b/esphome/components/tm1621/tm1621.cpp @@ -23,7 +23,7 @@ enum Tm1621Device { TM1621_USER, TM1621_POWR316D, TM1621_THR316D }; const uint8_t TM1621_COMMANDS[] = {TM1621_SYS_EN, TM1621_LCD_ON, TM1621_BIAS, TM1621_TIMER_DIS, TM1621_WDT_DIS, TM1621_TONE_OFF, TM1621_IRQ_DIS}; -const char TM1621_KCHAR[] PROGMEM = {"0|1|2|3|4|5|6|7|8|9|-| "}; +constexpr char TM1621_KCHAR[] PROGMEM = {"0|1|2|3|4|5|6|7|8|9|-| "}; // 0 1 2 3 4 5 6 7 8 9 - off const uint8_t TM1621_DIGIT_ROW[2][12] = {{0x5F, 0x50, 0x3D, 0x79, 0x72, 0x6B, 0x6F, 0x51, 0x7F, 0x7B, 0x20, 0x00}, {0xF5, 0x05, 0xB6, 0x97, 0x47, 0xD3, 0xF3, 0x85, 0xF7, 0xD7, 0x02, 0x00}}; diff --git a/esphome/components/tm1637/tm1637.cpp b/esphome/components/tm1637/tm1637.cpp index 49da01472f..f9c876f40c 100644 --- a/esphome/components/tm1637/tm1637.cpp +++ b/esphome/components/tm1637/tm1637.cpp @@ -27,7 +27,7 @@ const uint8_t TM1637_DATA_FIXED_ADDR = 0x04; //!< Fixed address // --- // D X // XABCDEFG -const uint8_t TM1637_ASCII_TO_RAW[] PROGMEM = { +constexpr uint8_t TM1637_ASCII_TO_RAW[] PROGMEM = { 0b00000000, // ' ', ord 0x20 0b10110000, // '!', ord 0x21 0b00100010, // '"', ord 0x22 diff --git a/esphome/components/tm1638/sevenseg.h b/esphome/components/tm1638/sevenseg.h index e20a55a69f..a4c16c7422 100644 --- a/esphome/components/tm1638/sevenseg.h +++ b/esphome/components/tm1638/sevenseg.h @@ -4,7 +4,7 @@ namespace esphome { namespace tm1638 { namespace TM1638Translation { -const unsigned char SEVEN_SEG[] PROGMEM = { +constexpr unsigned char SEVEN_SEG[] PROGMEM = { 0x00, /* (space) */ 0x86, /* ! */ 0x22, /* " */ diff --git a/esphome/components/web_server/server_index_v2.h b/esphome/components/web_server/server_index_v2.h index cc37db6a6b..b5dac9ae4c 100644 --- a/esphome/components/web_server/server_index_v2.h +++ b/esphome/components/web_server/server_index_v2.h @@ -9,7 +9,7 @@ namespace esphome::web_server { #ifdef USE_WEBSERVER_GZIP -const uint8_t INDEX_GZ[] PROGMEM = { +constexpr uint8_t INDEX_GZ[] PROGMEM = { 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xed, 0x7d, 0xd9, 0x72, 0xdb, 0x48, 0xb6, 0xe0, 0xf3, 0xd4, 0x57, 0x40, 0x28, 0xb5, 0x8c, 0x2c, 0x26, 0xc1, 0x45, 0x92, 0x2d, 0x83, 0x4a, 0xb2, 0x65, 0xd9, 0xd5, 0x76, 0x97, 0xb7, 0xb6, 0xec, 0xda, 0x58, 0x6c, 0x09, 0x02, 0x92, 0x44, 0x96, 0x41, 0x80, 0x05, 0x24, 0xb5, 0x14, 0x89, @@ -697,7 +697,7 @@ const uint8_t INDEX_GZ[] PROGMEM = { 0x56, 0x78, 0xff, 0xff, 0x01, 0xa2, 0x89, 0x8c, 0x0d, 0xc4, 0x97, 0x00, 0x00}; #else // Brotli (default, smaller) -const uint8_t INDEX_BR[] PROGMEM = { +constexpr uint8_t INDEX_BR[] PROGMEM = { 0x1b, 0xc3, 0x97, 0x11, 0x55, 0xb5, 0x65, 0x2c, 0x8a, 0x8a, 0x55, 0x0b, 0xd0, 0xba, 0x80, 0x1b, 0x32, 0xb0, 0x81, 0x4f, 0x27, 0x63, 0xf1, 0x7e, 0x88, 0xe3, 0xd8, 0x52, 0x84, 0x55, 0xe8, 0x35, 0x5b, 0x2b, 0x82, 0xe1, 0xed, 0x1f, 0xfd, 0xde, 0x63, 0x38, 0x3a, 0x71, 0x78, 0xb0, 0x42, 0x17, 0x15, 0x54, 0x23, 0xe1, 0xaa, 0x28, 0x11, 0x94, 0x23, diff --git a/esphome/components/web_server/server_index_v3.h b/esphome/components/web_server/server_index_v3.h index bd47071dce..1f61b19fb5 100644 --- a/esphome/components/web_server/server_index_v3.h +++ b/esphome/components/web_server/server_index_v3.h @@ -9,7 +9,7 @@ namespace esphome::web_server { #ifdef USE_WEBSERVER_GZIP -const uint8_t INDEX_GZ[] PROGMEM = { +constexpr uint8_t INDEX_GZ[] PROGMEM = { 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xcc, 0xbd, 0x7b, 0x7f, 0x1a, 0xb9, 0xb2, 0x28, 0xfa, 0xf7, 0x3d, 0x9f, 0xc2, 0xee, 0x9d, 0xf1, 0xb4, 0x8c, 0x68, 0x03, 0x36, 0x8e, 0xd3, 0x58, 0xe6, 0xe4, 0x39, 0xc9, 0x3c, 0x92, 0x4c, 0x9c, 0x64, 0x26, 0xc3, 0xb0, 0x33, 0xa2, 0x11, 0xa0, 0xa4, 0x91, 0x98, 0x96, 0x88, 0xed, 0x01, @@ -4104,7 +4104,7 @@ const uint8_t INDEX_GZ[] PROGMEM = { 0x37, 0x7a, 0x03, 0x00}; #else // Brotli (default, smaller) -const uint8_t INDEX_BR[] PROGMEM = { +constexpr uint8_t INDEX_BR[] PROGMEM = { 0x5b, 0x36, 0x7a, 0x53, 0xc2, 0x36, 0x06, 0x5a, 0x1f, 0xd4, 0x4e, 0x00, 0xb3, 0xd6, 0xea, 0xff, 0x0a, 0xab, 0x51, 0x94, 0xb1, 0xe6, 0xb0, 0x2e, 0x61, 0xbb, 0x1a, 0x70, 0x3b, 0xd8, 0x06, 0xfd, 0x7d, 0x2f, 0x1a, 0x00, 0x55, 0x35, 0xe3, 0xa8, 0x1c, 0x62, 0xca, 0xd3, 0xb4, 0x00, 0xdb, 0x5e, 0x43, 0xa7, 0x14, 0x08, 0xa4, 0x51, 0x99, 0x96, 0xb6, From afbc45bf32fe8146183d0d44fc16213cdebddbdb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 19 Feb 2026 20:35:42 -0600 Subject: [PATCH 120/197] [e131] Drain all queued packets per loop iteration (#14133) --- esphome/components/e131/e131.cpp | 26 +++++--------------------- esphome/components/e131/e131.h | 9 +++++++++ 2 files changed, 14 insertions(+), 21 deletions(-) diff --git a/esphome/components/e131/e131.cpp b/esphome/components/e131/e131.cpp index 941927122c..a7a695c167 100644 --- a/esphome/components/e131/e131.cpp +++ b/esphome/components/e131/e131.cpp @@ -70,27 +70,12 @@ void E131Component::loop() { E131Packet packet; int universe = 0; uint8_t buf[1460]; + ssize_t len; -#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) - ssize_t len = this->socket_->read(buf, sizeof(buf)); - if (len == -1) { - return; - } - - if (!this->packet_(buf, (size_t) len, universe, packet)) { - ESP_LOGV(TAG, "Invalid packet received of size %d.", (int) len); - return; - } - - if (!this->process_(universe, packet)) { - ESP_LOGV(TAG, "Ignored packet for %d universe of size %d.", universe, packet.count); - } -#elif defined(USE_SOCKET_IMPL_LWIP_TCP) - while (auto packet_size = this->udp_.parsePacket()) { - auto len = this->udp_.read(buf, sizeof(buf)); - if (len <= 0) - continue; - + // Drain all queued packets so multi-universe frames are applied + // atomically before the light writes. Without this, each universe + // packet would trigger a separate full-strip write causing tearing. + while ((len = this->read_(buf, sizeof(buf))) > 0) { if (!this->packet_(buf, (size_t) len, universe, packet)) { ESP_LOGV(TAG, "Invalid packet received of size %d.", (int) len); continue; @@ -100,7 +85,6 @@ void E131Component::loop() { ESP_LOGV(TAG, "Ignored packet for %d universe of size %d.", universe, packet.count); } } -#endif } void E131Component::add_effect(E131AddressableLightEffect *light_effect) { diff --git a/esphome/components/e131/e131.h b/esphome/components/e131/e131.h index fee447b678..8f0b808946 100644 --- a/esphome/components/e131/e131.h +++ b/esphome/components/e131/e131.h @@ -46,6 +46,15 @@ class E131Component : public esphome::Component { void set_method(E131ListenMethod listen_method) { this->listen_method_ = listen_method; } protected: + inline ssize_t read_(uint8_t *buf, size_t len) { +#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) + return this->socket_->read(buf, len); +#elif defined(USE_SOCKET_IMPL_LWIP_TCP) + if (!this->udp_.parsePacket()) + return -1; + return this->udp_.read(buf, len); +#endif + } bool packet_(const uint8_t *data, size_t len, int &universe, E131Packet &packet); bool process_(int universe, const E131Packet &packet); bool join_igmp_groups_(); From 7a2a149061c3b9480a3d1900f3e8cad1f1ad75ac Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 19 Feb 2026 21:43:29 -0500 Subject: [PATCH 121/197] [esp32] Bump ESP-IDF to 5.5.3 (#14122) Co-authored-by: Claude Opus 4.6 --- .clang-tidy.hash | 2 +- esphome/components/esp32/__init__.py | 17 +++++++++++++---- esphome/components/esp32_ble/__init__.py | 15 +++++++++------ platformio.ini | 4 ++-- 4 files changed, 25 insertions(+), 13 deletions(-) diff --git a/.clang-tidy.hash b/.clang-tidy.hash index d6d401ee66..df584fa716 100644 --- a/.clang-tidy.hash +++ b/.clang-tidy.hash @@ -1 +1 @@ -ce05c28e9dc0b12c4f6e7454986ffea5123ac974a949da841be698c535f2083e +3258307fa645ba77307e502075c02c4d710e92c48250839db3526d36a9655444 diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index b1b3f0dc16..cdde1c4ed5 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -643,7 +643,7 @@ ARDUINO_PLATFORM_VERSION_LOOKUP = { # These versions correspond to pioarduino/esp-idf releases # See: https://github.com/pioarduino/esp-idf/releases ARDUINO_IDF_VERSION_LOOKUP = { - cv.Version(3, 3, 7): cv.Version(5, 5, 2), + cv.Version(3, 3, 7): cv.Version(5, 5, 3), cv.Version(3, 3, 6): cv.Version(5, 5, 2), cv.Version(3, 3, 5): cv.Version(5, 5, 2), cv.Version(3, 3, 4): cv.Version(5, 5, 1), @@ -662,11 +662,12 @@ ARDUINO_IDF_VERSION_LOOKUP = { # The default/recommended esp-idf framework version # - https://github.com/espressif/esp-idf/releases ESP_IDF_FRAMEWORK_VERSION_LOOKUP = { - "recommended": cv.Version(5, 5, 2), - "latest": cv.Version(5, 5, 2), - "dev": cv.Version(5, 5, 2), + "recommended": cv.Version(5, 5, 3), + "latest": cv.Version(5, 5, 3), + "dev": cv.Version(5, 5, 3), } ESP_IDF_PLATFORM_VERSION_LOOKUP = { + cv.Version(5, 5, 3): cv.Version(55, 3, 37), cv.Version(5, 5, 2): cv.Version(55, 3, 37), cv.Version(5, 5, 1): cv.Version(55, 3, 31, "2"), cv.Version(5, 5, 0): cv.Version(55, 3, 31, "2"), @@ -1471,6 +1472,14 @@ async def to_code(config): f"CONFIG_ESPTOOLPY_FLASHSIZE_{config[CONF_FLASH_SIZE]}", True ) + # ESP32-P4: ESP-IDF 5.5.3 changed the default of ESP32P4_SELECTS_REV_LESS_V3 + # from y to n. PlatformIO uses sections.ld.in (for rev <3) or + # sections.rev3.ld.in (for rev >=3) based on board definition. + # Set the sdkconfig option to match the board's revision. + if variant == VARIANT_ESP32P4: + is_rev3 = "_r3" in config[CONF_BOARD] + add_idf_sdkconfig_option("CONFIG_ESP32P4_SELECTS_REV_LESS_V3", not is_rev3) + # Set minimum chip revision for ESP32 variant # Setting this to 3.0 or higher reduces flash size by excluding workaround code, # and for PSRAM users saves significant IRAM by keeping C library functions in ROM. diff --git a/esphome/components/esp32_ble/__init__.py b/esphome/components/esp32_ble/__init__.py index d2020ada22..80fcf051b9 100644 --- a/esphome/components/esp32_ble/__init__.py +++ b/esphome/components/esp32_ble/__init__.py @@ -413,18 +413,21 @@ def final_validation(config): add_idf_sdkconfig_option("CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID", True) add_idf_sdkconfig_option("CONFIG_ESP_HOSTED_BLUEDROID_HCI_VHCI", True) - # Check if BLE Server is needed - has_ble_server = "esp32_ble_server" in full_config - # Check if BLE Client is needed (via esp32_ble_tracker or esp32_ble_client) has_ble_client = ( "esp32_ble_tracker" in full_config or "esp32_ble_client" in full_config ) - # ESP-IDF BLE stack requires GATT Server to be enabled when GATT Client is enabled - # This is an internal dependency in the Bluedroid stack (tested ESP-IDF 5.4.2-5.5.1) + # Always enable GATTS: ESP-IDF 5.5.2.260206 has a bug in gatt_main.c where a + # GATT_TRACE_DEBUG references 'msg_len' outside the GATTS_INCLUDED/GATTC_INCLUDED + # guard, causing a compile error when both are disabled. + # Additionally, when GATT Client is enabled, GATT Server must also be enabled + # as an internal dependency in the Bluedroid stack. # See: https://github.com/espressif/esp-idf/issues/17724 - add_idf_sdkconfig_option("CONFIG_BT_GATTS_ENABLE", has_ble_server or has_ble_client) + # TODO: Revert to conditional once the gatt_main.c bug is fixed upstream: + # has_ble_server = "esp32_ble_server" in full_config + # add_idf_sdkconfig_option("CONFIG_BT_GATTS_ENABLE", has_ble_server or has_ble_client) + add_idf_sdkconfig_option("CONFIG_BT_GATTS_ENABLE", True) add_idf_sdkconfig_option("CONFIG_BT_GATTC_ENABLE", has_ble_client) # Handle max_connections: check for deprecated location in esp32_ble_tracker diff --git a/platformio.ini b/platformio.ini index 09b3d8722d..fdd6a36428 100644 --- a/platformio.ini +++ b/platformio.ini @@ -136,7 +136,7 @@ extends = common:arduino platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.37/platform-espressif32.zip platform_packages = pioarduino/framework-arduinoespressif32@https://github.com/espressif/arduino-esp32/releases/download/3.3.7/esp32-core-3.3.7.tar.xz - pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.5.2/esp-idf-v5.5.2.tar.xz + pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.5.3/esp-idf-v5.5.3.tar.xz framework = arduino, espidf ; Arduino as an ESP-IDF component lib_deps = @@ -171,7 +171,7 @@ extra_scripts = post:esphome/components/esp32/post_build.py.script extends = common:idf platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.37/platform-espressif32.zip platform_packages = - pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.5.2/esp-idf-v5.5.2.tar.xz + pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.5.3/esp-idf-v5.5.3.tar.xz framework = espidf lib_deps = From b67b2cc3ab86ebbc7cfb3d0a893bf96bd50ef9a6 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 19 Feb 2026 21:56:20 -0500 Subject: [PATCH 122/197] [ld2450] Add frame header synchronization to fix initialization regression (#14135) Co-authored-by: Claude Opus 4.6 Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- esphome/components/ld2450/ld2450.cpp | 22 ++- esphome/components/ld2450/ld2450.h | 1 + tests/components/ld2450/common.h | 61 +++++++ tests/components/ld2450/ld2450_readline.cpp | 181 ++++++++++++++++++++ 4 files changed, 263 insertions(+), 2 deletions(-) create mode 100644 tests/components/ld2450/common.h create mode 100644 tests/components/ld2450/ld2450_readline.cpp diff --git a/esphome/components/ld2450/ld2450.cpp b/esphome/components/ld2450/ld2450.cpp index 1ea5c18271..2af45235a3 100644 --- a/esphome/components/ld2450/ld2450.cpp +++ b/esphome/components/ld2450/ld2450.cpp @@ -769,15 +769,33 @@ void LD2450Component::readline_(int readch) { return; // No data available } + // Frame header synchronization: verify first 4 bytes match a known frame header. + // This prevents the parser from accumulating mid-frame data after losing sync + // (e.g. after module restart or UART noise). + if (this->buffer_pos_ < HEADER_FOOTER_SIZE) { + const uint8_t byte = static_cast(readch); + // Verify header bytes match the frame type established by byte 0 + if (this->buffer_pos_ > 0) { + const uint8_t *expected = (this->buffer_data_[0] == DATA_FRAME_HEADER[0]) ? DATA_FRAME_HEADER : CMD_FRAME_HEADER; + if (byte != expected[this->buffer_pos_]) { + this->buffer_pos_ = 0; // Reset and fall through to check if this byte starts a new frame + } + } + // First byte must match start of a data or command frame header + if (this->buffer_pos_ == 0 && byte != DATA_FRAME_HEADER[0] && byte != CMD_FRAME_HEADER[0]) { + return; + } + } + if (this->buffer_pos_ < MAX_LINE_LENGTH - 1) { this->buffer_data_[this->buffer_pos_++] = readch; this->buffer_data_[this->buffer_pos_] = 0; } else { - // We should never get here, but just in case... ESP_LOGW(TAG, "Max command length exceeded; ignoring"); this->buffer_pos_ = 0; + return; } - if (this->buffer_pos_ < 4) { + if (this->buffer_pos_ < HEADER_FOOTER_SIZE) { return; // Not enough data to process yet } if (this->buffer_data_[this->buffer_pos_ - 2] == DATA_FRAME_FOOTER[0] && diff --git a/esphome/components/ld2450/ld2450.h b/esphome/components/ld2450/ld2450.h index fe69cd81d0..44e5912b2a 100644 --- a/esphome/components/ld2450/ld2450.h +++ b/esphome/components/ld2450/ld2450.h @@ -1,5 +1,6 @@ #pragma once +#include "esphome/core/automation.h" #include "esphome/core/defines.h" #include "esphome/core/component.h" #ifdef USE_SENSOR diff --git a/tests/components/ld2450/common.h b/tests/components/ld2450/common.h new file mode 100644 index 0000000000..d5ffbe1295 --- /dev/null +++ b/tests/components/ld2450/common.h @@ -0,0 +1,61 @@ +#pragma once +#include +#include +#include +#include +#include +#include "esphome/components/ld2450/ld2450.h" +#include "esphome/components/uart/uart_component.h" + +namespace esphome::ld2450::testing { + +// Mock UART component to satisfy UARTDevice parent requirement. +class MockUARTComponent : public uart::UARTComponent { + public: + void write_array(const uint8_t *data, size_t len) override {} + MOCK_METHOD(bool, read_array, (uint8_t * data, size_t len), (override)); + MOCK_METHOD(bool, peek_byte, (uint8_t * data), (override)); + MOCK_METHOD(size_t, available, (), (override)); + MOCK_METHOD(void, flush, (), (override)); + MOCK_METHOD(void, check_logger_conflict, (), (override)); +}; + +// Expose protected members for testing. +class TestableLD2450 : public LD2450Component { + public: + using LD2450Component::buffer_data_; + using LD2450Component::buffer_pos_; + using LD2450Component::readline_; + + void feed(const std::vector &data) { + for (uint8_t byte : data) { + this->readline_(byte); + } + } +}; + +// LD2450 periodic data frame: header (4) + 3 targets * 8 bytes + footer (2) = 30 bytes +// All-zero targets means no presence detected. +inline std::vector make_periodic_frame(uint8_t fill = 0x00) { + std::vector frame = {0xAA, 0xFF, 0x03, 0x00}; // DATA_FRAME_HEADER + for (int i = 0; i < 24; i++) { + frame.push_back(fill); // 3 targets * 8 bytes + } + frame.push_back(0x55); // DATA_FRAME_FOOTER + frame.push_back(0xCC); + return frame; +} + +// LD2450 command ACK frame for CMD_ENABLE_CONF (0xFF), successful. +// header (4) + length (2) + command (2) + result (2) + footer (4) = 14 bytes +inline std::vector make_ack_frame() { + return { + 0xFD, 0xFC, 0xFB, 0xFA, // CMD_FRAME_HEADER + 0x04, 0x00, // length = 4 + 0xFF, 0x01, // command = enable_conf, status = success + 0x00, 0x00, // result = ok + 0x04, 0x03, 0x02, 0x01 // CMD_FRAME_FOOTER + }; +} + +} // namespace esphome::ld2450::testing diff --git a/tests/components/ld2450/ld2450_readline.cpp b/tests/components/ld2450/ld2450_readline.cpp new file mode 100644 index 0000000000..68b1dd6881 --- /dev/null +++ b/tests/components/ld2450/ld2450_readline.cpp @@ -0,0 +1,181 @@ +#include "common.h" + +namespace esphome::ld2450::testing { + +class LD2450ReadlineTest : public ::testing::Test { + protected: + void SetUp() override { + this->ld2450_.set_uart_parent(&this->mock_uart_); + // Ensure clean state + ASSERT_EQ(this->ld2450_.buffer_pos_, 0); + } + + MockUARTComponent mock_uart_; + TestableLD2450 ld2450_; +}; + +// --- Good data tests --- + +TEST_F(LD2450ReadlineTest, ValidPeriodicFrame) { + auto frame = make_periodic_frame(); + this->ld2450_.feed(frame); + // After a complete valid frame, buffer should be reset + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); +} + +TEST_F(LD2450ReadlineTest, ValidCommandAckFrame) { + auto frame = make_ack_frame(); + this->ld2450_.feed(frame); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); +} + +TEST_F(LD2450ReadlineTest, BackToBackPeriodicFrames) { + auto frame = make_periodic_frame(); + for (int i = 0; i < 5; i++) { + this->ld2450_.feed(frame); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0) << "Frame " << i << " not processed"; + } +} + +TEST_F(LD2450ReadlineTest, BackToBackMixedFrames) { + auto periodic = make_periodic_frame(); + auto ack = make_ack_frame(); + this->ld2450_.feed(periodic); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); + this->ld2450_.feed(ack); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); + this->ld2450_.feed(periodic); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); +} + +// --- Garbage rejection tests --- + +TEST_F(LD2450ReadlineTest, GarbageDiscarded) { + // Feed bytes that don't match any header start byte + std::vector garbage = {0x01, 0x02, 0x03, 0x42, 0x99, 0x00, 0xFF, 0x7F}; + this->ld2450_.feed(garbage); + // Header sync should discard all of these + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); +} + +TEST_F(LD2450ReadlineTest, GarbageThenValidFrame) { + std::vector garbage = {0x01, 0x02, 0x03, 0x42, 0x99}; + this->ld2450_.feed(garbage); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); + + auto frame = make_periodic_frame(); + this->ld2450_.feed(frame); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); +} + +// --- Header synchronization tests --- + +TEST_F(LD2450ReadlineTest, PartialDataHeaderThenMismatch) { + // Start of a data frame header, then invalid byte + this->ld2450_.feed({0xAA, 0xFF, 0x42}); // 0x42 doesn't match DATA_FRAME_HEADER[2] (0x03) + // Parser should have reset + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); +} + +TEST_F(LD2450ReadlineTest, PartialCmdHeaderThenMismatch) { + // Start of a command frame header, then invalid byte + this->ld2450_.feed({0xFD, 0xFC, 0xFB, 0x42}); // 0x42 doesn't match CMD_FRAME_HEADER[3] (0xFA) + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); +} + +TEST_F(LD2450ReadlineTest, PartialHeaderThenValidFrame) { + // Partial header that fails, then a complete valid frame + this->ld2450_.feed({0xAA, 0xFF, 0x42}); // Fails at byte 3 + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); + + auto frame = make_periodic_frame(); + this->ld2450_.feed(frame); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); +} + +TEST_F(LD2450ReadlineTest, HeaderMismatchRecoveryOnNewHeaderByte) { + // Start data header, mismatch at byte 2, but mismatch byte is start of command header + this->ld2450_.feed({0xAA, 0xFF}); + EXPECT_EQ(this->ld2450_.buffer_pos_, 2); // Accumulating header + + this->ld2450_.feed({0xFD}); // Doesn't match DATA_FRAME_HEADER[2]=0x03, but IS CMD_FRAME_HEADER[0] + // Parser should reset and start new frame with 0xFD + EXPECT_EQ(this->ld2450_.buffer_pos_, 1); + EXPECT_EQ(this->ld2450_.buffer_data_[0], 0xFD); +} + +// --- Mid-frame / overflow recovery tests --- + +TEST_F(LD2450ReadlineTest, MidFrameDataRecovery) { + // Simulate starting mid-frame: feed the tail end of a periodic frame (no valid header) + // These bytes would be part of target data in a real frame + std::vector mid_frame = {0x10, 0x20, 0x30, 0x40, 0x55, 0xCC}; + this->ld2450_.feed(mid_frame); + // All discarded (none match header start bytes) + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); + + // Now feed a valid frame + auto frame = make_periodic_frame(); + this->ld2450_.feed(frame); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); +} + +TEST_F(LD2450ReadlineTest, OverflowRecovery) { + // Feed a valid data frame header followed by enough filler to cause overflow. + // Header (4) + 36 filler = 40 bytes in buffer. The 41st byte triggers overflow. + std::vector overflow_data = {0xAA, 0xFF, 0x03, 0x00}; // Valid header + for (int i = 0; i < 37; i++) { + overflow_data.push_back(0x11); // Filler that won't match any footer + } + // 41 bytes total: 40 stored, 41st triggers overflow and resets buffer_pos_ to 0 + this->ld2450_.feed(overflow_data); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); + + // Feed a valid frame and verify recovery + auto frame = make_periodic_frame(); + this->ld2450_.feed(frame); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); +} + +TEST_F(LD2450ReadlineTest, RepeatedOverflowDoesNotLoop) { + // Simulate the bug scenario: repeated overflows should not prevent recovery. + // Feed 3 rounds of overflow-inducing data. + for (int round = 0; round < 3; round++) { + std::vector overflow_data = {0xAA, 0xFF, 0x03, 0x00}; + for (int i = 0; i < 37; i++) { + overflow_data.push_back(0x22); + } + this->ld2450_.feed(overflow_data); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0) << "Overflow round " << round; + } + + // Parser should still recover and process a valid frame + auto frame = make_periodic_frame(); + this->ld2450_.feed(frame); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); +} + +TEST_F(LD2450ReadlineTest, SimulatedRestartGarbageThenFrames) { + // Simulate LD2450 restart: burst of garbage bytes (partial frames, noise) + // followed by normal periodic data. + // Partial periodic frame (as if we started reading mid-frame), a stale footer, and more garbage + std::vector restart_noise = { + 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, // mid-frame data + 0x55, 0xCC, // stale footer bytes + 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, // more garbage + }; + + this->ld2450_.feed(restart_noise); + // All garbage should be discarded + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); + + // Now the LD2450 starts sending valid frames + auto frame = make_periodic_frame(); + this->ld2450_.feed(frame); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); + + this->ld2450_.feed(frame); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); +} + +} // namespace esphome::ld2450::testing From a2f0607c1e860d1173c80e0a763fda99e71e69f6 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 19 Feb 2026 22:04:38 -0500 Subject: [PATCH 123/197] [ld2410] Add frame header synchronization to readline_() (#14136) Co-authored-by: Claude Opus 4.6 --- esphome/components/ld2410/ld2410.cpp | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/esphome/components/ld2410/ld2410.cpp b/esphome/components/ld2410/ld2410.cpp index 95a04f768a..a3c2193d67 100644 --- a/esphome/components/ld2410/ld2410.cpp +++ b/esphome/components/ld2410/ld2410.cpp @@ -601,15 +601,33 @@ void LD2410Component::readline_(int readch) { return; // No data available } + // Frame header synchronization: verify first 4 bytes match a known frame header. + // This prevents the parser from getting stuck in an overflow loop after losing sync + // (e.g. after module restart or UART noise). + if (this->buffer_pos_ < HEADER_FOOTER_SIZE) { + const uint8_t byte = static_cast(readch); + // Verify header bytes match the frame type established by byte 0 + if (this->buffer_pos_ > 0) { + const uint8_t *expected = (this->buffer_data_[0] == DATA_FRAME_HEADER[0]) ? DATA_FRAME_HEADER : CMD_FRAME_HEADER; + if (byte != expected[this->buffer_pos_]) { + this->buffer_pos_ = 0; // Reset and fall through to check if this byte starts a new frame + } + } + // First byte must match start of a data or command frame header + if (this->buffer_pos_ == 0 && byte != DATA_FRAME_HEADER[0] && byte != CMD_FRAME_HEADER[0]) { + return; + } + } + if (this->buffer_pos_ < MAX_LINE_LENGTH - 1) { this->buffer_data_[this->buffer_pos_++] = readch; this->buffer_data_[this->buffer_pos_] = 0; } else { - // We should never get here, but just in case... ESP_LOGW(TAG, "Max command length exceeded; ignoring"); this->buffer_pos_ = 0; + return; } - if (this->buffer_pos_ < 4) { + if (this->buffer_pos_ < HEADER_FOOTER_SIZE) { return; // Not enough data to process yet } if (ld2410::validate_header_footer(DATA_FRAME_FOOTER, &this->buffer_data_[this->buffer_pos_ - 4])) { From 5af871acce9c33284d483620131056af2ad48071 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 19 Feb 2026 22:36:12 -0500 Subject: [PATCH 124/197] [ld2420] Increase MAX_LINE_LENGTH to allow footer-based resync (#14137) Co-authored-by: Claude Opus 4.6 --- esphome/components/ld2420/ld2420.h | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/esphome/components/ld2420/ld2420.h b/esphome/components/ld2420/ld2420.h index 02250c5911..358793fe64 100644 --- a/esphome/components/ld2420/ld2420.h +++ b/esphome/components/ld2420/ld2420.h @@ -21,7 +21,9 @@ namespace esphome::ld2420 { static constexpr uint8_t CALIBRATE_SAMPLES = 64; -static constexpr uint8_t MAX_LINE_LENGTH = 46; // Max characters for serial buffer +// Energy frame is 45 bytes; +1 for null terminator, +4 so that a frame footer always lands +// inside the buffer during footer-based resynchronization after losing sync. +static constexpr uint8_t MAX_LINE_LENGTH = 50; static constexpr uint8_t TOTAL_GATES = 16; enum OpMode : uint8_t { From efe54e3b5e3d3ddefee703048ff37bb4ec9b0b5f Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 19 Feb 2026 23:25:25 -0500 Subject: [PATCH 125/197] [ld2410/ld2450] Replace header sync with buffer size increase for frame resync (#14138) Co-authored-by: Claude Opus 4.6 --- esphome/components/ld2410/ld2410.cpp | 19 +-- esphome/components/ld2410/ld2410.h | 6 +- esphome/components/ld2450/ld2450.cpp | 19 +-- esphome/components/ld2450/ld2450.h | 8 +- tests/components/ld2450/ld2450_readline.cpp | 158 ++++++++------------ 5 files changed, 72 insertions(+), 138 deletions(-) diff --git a/esphome/components/ld2410/ld2410.cpp b/esphome/components/ld2410/ld2410.cpp index a3c2193d67..f8f782f804 100644 --- a/esphome/components/ld2410/ld2410.cpp +++ b/esphome/components/ld2410/ld2410.cpp @@ -601,28 +601,11 @@ void LD2410Component::readline_(int readch) { return; // No data available } - // Frame header synchronization: verify first 4 bytes match a known frame header. - // This prevents the parser from getting stuck in an overflow loop after losing sync - // (e.g. after module restart or UART noise). - if (this->buffer_pos_ < HEADER_FOOTER_SIZE) { - const uint8_t byte = static_cast(readch); - // Verify header bytes match the frame type established by byte 0 - if (this->buffer_pos_ > 0) { - const uint8_t *expected = (this->buffer_data_[0] == DATA_FRAME_HEADER[0]) ? DATA_FRAME_HEADER : CMD_FRAME_HEADER; - if (byte != expected[this->buffer_pos_]) { - this->buffer_pos_ = 0; // Reset and fall through to check if this byte starts a new frame - } - } - // First byte must match start of a data or command frame header - if (this->buffer_pos_ == 0 && byte != DATA_FRAME_HEADER[0] && byte != CMD_FRAME_HEADER[0]) { - return; - } - } - if (this->buffer_pos_ < MAX_LINE_LENGTH - 1) { this->buffer_data_[this->buffer_pos_++] = readch; this->buffer_data_[this->buffer_pos_] = 0; } else { + // We should never get here, but just in case... ESP_LOGW(TAG, "Max command length exceeded; ignoring"); this->buffer_pos_ = 0; return; diff --git a/esphome/components/ld2410/ld2410.h b/esphome/components/ld2410/ld2410.h index efe585fb76..687ed21d1d 100644 --- a/esphome/components/ld2410/ld2410.h +++ b/esphome/components/ld2410/ld2410.h @@ -33,8 +33,10 @@ namespace esphome::ld2410 { using namespace ld24xx; -static constexpr uint8_t MAX_LINE_LENGTH = 46; // Max characters for serial buffer -static constexpr uint8_t TOTAL_GATES = 9; // Total number of gates supported by the LD2410 +// Engineering data frame is 45 bytes; +1 for null terminator, +4 so that a frame footer always +// lands inside the buffer during footer-based resynchronization after losing sync. +static constexpr uint8_t MAX_LINE_LENGTH = 50; +static constexpr uint8_t TOTAL_GATES = 9; // Total number of gates supported by the LD2410 class LD2410Component : public Component, public uart::UARTDevice { #ifdef USE_BINARY_SENSOR diff --git a/esphome/components/ld2450/ld2450.cpp b/esphome/components/ld2450/ld2450.cpp index 2af45235a3..d30c164769 100644 --- a/esphome/components/ld2450/ld2450.cpp +++ b/esphome/components/ld2450/ld2450.cpp @@ -769,28 +769,11 @@ void LD2450Component::readline_(int readch) { return; // No data available } - // Frame header synchronization: verify first 4 bytes match a known frame header. - // This prevents the parser from accumulating mid-frame data after losing sync - // (e.g. after module restart or UART noise). - if (this->buffer_pos_ < HEADER_FOOTER_SIZE) { - const uint8_t byte = static_cast(readch); - // Verify header bytes match the frame type established by byte 0 - if (this->buffer_pos_ > 0) { - const uint8_t *expected = (this->buffer_data_[0] == DATA_FRAME_HEADER[0]) ? DATA_FRAME_HEADER : CMD_FRAME_HEADER; - if (byte != expected[this->buffer_pos_]) { - this->buffer_pos_ = 0; // Reset and fall through to check if this byte starts a new frame - } - } - // First byte must match start of a data or command frame header - if (this->buffer_pos_ == 0 && byte != DATA_FRAME_HEADER[0] && byte != CMD_FRAME_HEADER[0]) { - return; - } - } - if (this->buffer_pos_ < MAX_LINE_LENGTH - 1) { this->buffer_data_[this->buffer_pos_++] = readch; this->buffer_data_[this->buffer_pos_] = 0; } else { + // We should never get here, but just in case... ESP_LOGW(TAG, "Max command length exceeded; ignoring"); this->buffer_pos_ = 0; return; diff --git a/esphome/components/ld2450/ld2450.h b/esphome/components/ld2450/ld2450.h index 44e5912b2a..30f96c0a9c 100644 --- a/esphome/components/ld2450/ld2450.h +++ b/esphome/components/ld2450/ld2450.h @@ -38,9 +38,11 @@ using namespace ld24xx; // Constants static constexpr uint8_t DEFAULT_PRESENCE_TIMEOUT = 5; // Timeout to reset presense status 5 sec. -static constexpr uint8_t MAX_LINE_LENGTH = 41; // Max characters for serial buffer -static constexpr uint8_t MAX_TARGETS = 3; // Max 3 Targets in LD2450 -static constexpr uint8_t MAX_ZONES = 3; // Max 3 Zones in LD2450 +// Zone query response is 40 bytes; +1 for null terminator, +4 so that a frame footer always +// lands inside the buffer during footer-based resynchronization after losing sync. +static constexpr uint8_t MAX_LINE_LENGTH = 45; +static constexpr uint8_t MAX_TARGETS = 3; // Max 3 Targets in LD2450 +static constexpr uint8_t MAX_ZONES = 3; // Max 3 Zones in LD2450 enum Direction : uint8_t { DIRECTION_APPROACHING = 0, diff --git a/tests/components/ld2450/ld2450_readline.cpp b/tests/components/ld2450/ld2450_readline.cpp index 68b1dd6881..cb97f633bf 100644 --- a/tests/components/ld2450/ld2450_readline.cpp +++ b/tests/components/ld2450/ld2450_readline.cpp @@ -48,19 +48,39 @@ TEST_F(LD2450ReadlineTest, BackToBackMixedFrames) { EXPECT_EQ(this->ld2450_.buffer_pos_, 0); } -// --- Garbage rejection tests --- - -TEST_F(LD2450ReadlineTest, GarbageDiscarded) { - // Feed bytes that don't match any header start byte - std::vector garbage = {0x01, 0x02, 0x03, 0x42, 0x99, 0x00, 0xFF, 0x7F}; - this->ld2450_.feed(garbage); - // Header sync should discard all of these - EXPECT_EQ(this->ld2450_.buffer_pos_, 0); -} +// --- Garbage then valid frame tests --- TEST_F(LD2450ReadlineTest, GarbageThenValidFrame) { + // Garbage bytes accumulate in the buffer but don't match any footer. + // A valid frame follows; its footer resets the buffer and resyncs. std::vector garbage = {0x01, 0x02, 0x03, 0x42, 0x99}; this->ld2450_.feed(garbage); + EXPECT_GT(this->ld2450_.buffer_pos_, 0); // Garbage accumulated + + auto frame = make_periodic_frame(); + this->ld2450_.feed(frame); + // Footer from the valid frame resyncs the parser + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); +} + +// --- Footer-based resynchronization tests --- + +TEST_F(LD2450ReadlineTest, FooterInGarbageResyncs) { + // Garbage containing a periodic frame footer (0x55 0xCC) triggers + // a buffer reset, allowing the next frame to be parsed cleanly. + std::vector garbage_with_footer = {0x01, 0x02, 0x03, 0x04, 0x55, 0xCC}; + this->ld2450_.feed(garbage_with_footer); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); // Footer reset the buffer + + auto frame = make_periodic_frame(); + this->ld2450_.feed(frame); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); +} + +TEST_F(LD2450ReadlineTest, CmdFooterInGarbageResyncs) { + // Garbage containing a command frame footer (04 03 02 01) also resyncs. + std::vector garbage_with_footer = {0x10, 0x20, 0x30, 0x40, 0x04, 0x03, 0x02, 0x01}; + this->ld2450_.feed(garbage_with_footer); EXPECT_EQ(this->ld2450_.buffer_pos_, 0); auto frame = make_periodic_frame(); @@ -68,112 +88,56 @@ TEST_F(LD2450ReadlineTest, GarbageThenValidFrame) { EXPECT_EQ(this->ld2450_.buffer_pos_, 0); } -// --- Header synchronization tests --- +// --- Overflow recovery tests --- -TEST_F(LD2450ReadlineTest, PartialDataHeaderThenMismatch) { - // Start of a data frame header, then invalid byte - this->ld2450_.feed({0xAA, 0xFF, 0x42}); // 0x42 doesn't match DATA_FRAME_HEADER[2] (0x03) - // Parser should have reset - EXPECT_EQ(this->ld2450_.buffer_pos_, 0); -} - -TEST_F(LD2450ReadlineTest, PartialCmdHeaderThenMismatch) { - // Start of a command frame header, then invalid byte - this->ld2450_.feed({0xFD, 0xFC, 0xFB, 0x42}); // 0x42 doesn't match CMD_FRAME_HEADER[3] (0xFA) - EXPECT_EQ(this->ld2450_.buffer_pos_, 0); -} - -TEST_F(LD2450ReadlineTest, PartialHeaderThenValidFrame) { - // Partial header that fails, then a complete valid frame - this->ld2450_.feed({0xAA, 0xFF, 0x42}); // Fails at byte 3 - EXPECT_EQ(this->ld2450_.buffer_pos_, 0); - - auto frame = make_periodic_frame(); - this->ld2450_.feed(frame); - EXPECT_EQ(this->ld2450_.buffer_pos_, 0); -} - -TEST_F(LD2450ReadlineTest, HeaderMismatchRecoveryOnNewHeaderByte) { - // Start data header, mismatch at byte 2, but mismatch byte is start of command header - this->ld2450_.feed({0xAA, 0xFF}); - EXPECT_EQ(this->ld2450_.buffer_pos_, 2); // Accumulating header - - this->ld2450_.feed({0xFD}); // Doesn't match DATA_FRAME_HEADER[2]=0x03, but IS CMD_FRAME_HEADER[0] - // Parser should reset and start new frame with 0xFD - EXPECT_EQ(this->ld2450_.buffer_pos_, 1); - EXPECT_EQ(this->ld2450_.buffer_data_[0], 0xFD); -} - -// --- Mid-frame / overflow recovery tests --- - -TEST_F(LD2450ReadlineTest, MidFrameDataRecovery) { - // Simulate starting mid-frame: feed the tail end of a periodic frame (no valid header) - // These bytes would be part of target data in a real frame - std::vector mid_frame = {0x10, 0x20, 0x30, 0x40, 0x55, 0xCC}; - this->ld2450_.feed(mid_frame); - // All discarded (none match header start bytes) - EXPECT_EQ(this->ld2450_.buffer_pos_, 0); - - // Now feed a valid frame - auto frame = make_periodic_frame(); - this->ld2450_.feed(frame); - EXPECT_EQ(this->ld2450_.buffer_pos_, 0); -} - -TEST_F(LD2450ReadlineTest, OverflowRecovery) { - // Feed a valid data frame header followed by enough filler to cause overflow. - // Header (4) + 36 filler = 40 bytes in buffer. The 41st byte triggers overflow. - std::vector overflow_data = {0xAA, 0xFF, 0x03, 0x00}; // Valid header - for (int i = 0; i < 37; i++) { - overflow_data.push_back(0x11); // Filler that won't match any footer - } - // 41 bytes total: 40 stored, 41st triggers overflow and resets buffer_pos_ to 0 +TEST_F(LD2450ReadlineTest, OverflowResetsBuffer) { + // Fill the buffer to capacity with filler that won't match any footer. + // MAX_LINE_LENGTH is 45, usable is 44. The 45th byte triggers overflow. + std::vector overflow_data(MAX_LINE_LENGTH, 0x11); + this->ld2450_.feed(overflow_data); + // After overflow, buffer_pos_ resets to 0 (via the < 4 early return path) + EXPECT_LT(this->ld2450_.buffer_pos_, 4); +} + +TEST_F(LD2450ReadlineTest, OverflowThenValidFrame) { + // Overflow, then a valid frame should be processed. + std::vector overflow_data(MAX_LINE_LENGTH, 0x11); this->ld2450_.feed(overflow_data); - EXPECT_EQ(this->ld2450_.buffer_pos_, 0); - // Feed a valid frame and verify recovery auto frame = make_periodic_frame(); this->ld2450_.feed(frame); EXPECT_EQ(this->ld2450_.buffer_pos_, 0); } -TEST_F(LD2450ReadlineTest, RepeatedOverflowDoesNotLoop) { - // Simulate the bug scenario: repeated overflows should not prevent recovery. - // Feed 3 rounds of overflow-inducing data. - for (int round = 0; round < 3; round++) { - std::vector overflow_data = {0xAA, 0xFF, 0x03, 0x00}; - for (int i = 0; i < 37; i++) { - overflow_data.push_back(0x22); - } - this->ld2450_.feed(overflow_data); - EXPECT_EQ(this->ld2450_.buffer_pos_, 0) << "Overflow round " << round; - } - - // Parser should still recover and process a valid frame +TEST_F(LD2450ReadlineTest, BufferLargeEnoughForDesyncedFooter) { + // The key fix: the buffer (45) is large enough that a desynced periodic frame's + // footer (at most 30 bytes into the stream) will land inside the buffer before overflow. + // Simulate starting 10 bytes into a periodic frame, then a full frame follows. + std::vector mid_frame = {0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39}; + // Then a complete periodic frame whose footer will land at position 40 (10 + 30), + // well within the buffer size of 45. auto frame = make_periodic_frame(); - this->ld2450_.feed(frame); + mid_frame.insert(mid_frame.end(), frame.begin(), frame.end()); + + this->ld2450_.feed(mid_frame); + // The footer from the frame should have triggered a reset EXPECT_EQ(this->ld2450_.buffer_pos_, 0); } -TEST_F(LD2450ReadlineTest, SimulatedRestartGarbageThenFrames) { - // Simulate LD2450 restart: burst of garbage bytes (partial frames, noise) - // followed by normal periodic data. - // Partial periodic frame (as if we started reading mid-frame), a stale footer, and more garbage +TEST_F(LD2450ReadlineTest, SimulatedRestartThenFrames) { + // Simulate LD2450 restart: burst of garbage followed by valid periodic frames. + // The garbage + first frame should fit in the buffer so the footer resyncs. std::vector restart_noise = { - 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, // mid-frame data - 0x55, 0xCC, // stale footer bytes - 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, // more garbage + 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, // 8 bytes of mid-frame data }; + auto frame = make_periodic_frame(); + // 8 garbage + 30 frame = 38 bytes, well within buffer of 45 + restart_noise.insert(restart_noise.end(), frame.begin(), frame.end()); this->ld2450_.feed(restart_noise); - // All garbage should be discarded - EXPECT_EQ(this->ld2450_.buffer_pos_, 0); - - // Now the LD2450 starts sending valid frames - auto frame = make_periodic_frame(); - this->ld2450_.feed(frame); EXPECT_EQ(this->ld2450_.buffer_pos_, 0); + // Subsequent frames should work normally this->ld2450_.feed(frame); EXPECT_EQ(this->ld2450_.buffer_pos_, 0); } From efe8a6c8ebfd3e831b9757356db7f08fc331472e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Mart=C3=ADn?= Date: Tue, 17 Feb 2026 18:45:21 +0100 Subject: [PATCH 126/197] [esp32_ble_server] fix infinitely large characteristic value (#14011) --- .../components/esp32_ble_server/__init__.py | 2 +- .../esp32_ble_server/ble_characteristic.cpp | 28 +++++++++++++++---- .../esp32_ble_server/ble_characteristic.h | 1 - 3 files changed, 24 insertions(+), 7 deletions(-) diff --git a/esphome/components/esp32_ble_server/__init__.py b/esphome/components/esp32_ble_server/__init__.py index a7e2522fac..cb494ed1bc 100644 --- a/esphome/components/esp32_ble_server/__init__.py +++ b/esphome/components/esp32_ble_server/__init__.py @@ -527,7 +527,7 @@ async def to_code_characteristic(service_var, char_conf): action_conf, char_conf[CONF_CHAR_VALUE_ACTION_ID_], cg.TemplateArguments(), - {}, + [], ) cg.add(value_action.play()) else: diff --git a/esphome/components/esp32_ble_server/ble_characteristic.cpp b/esphome/components/esp32_ble_server/ble_characteristic.cpp index 0482848ea0..a1b1ff94bb 100644 --- a/esphome/components/esp32_ble_server/ble_characteristic.cpp +++ b/esphome/components/esp32_ble_server/ble_characteristic.cpp @@ -246,9 +246,27 @@ void BLECharacteristic::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt if (this->handle_ != param->write.handle) break; + esp_gatt_status_t status = ESP_GATT_OK; + if (param->write.is_prep) { - this->value_.insert(this->value_.end(), param->write.value, param->write.value + param->write.len); - this->write_event_ = true; + const size_t offset = param->write.offset; + const size_t write_len = param->write.len; + const size_t new_size = offset + write_len; + // Clean the buffer on the first prepared write event + if (offset == 0) { + this->value_.clear(); + } + + if (offset != this->value_.size()) { + status = ESP_GATT_INVALID_OFFSET; + } else if (new_size > ESP_GATT_MAX_ATTR_LEN) { + status = ESP_GATT_INVALID_ATTR_LEN; + } else { + if (this->value_.size() < new_size) { + this->value_.resize(new_size); + } + memcpy(this->value_.data() + offset, param->write.value, write_len); + } } else { this->set_value(ByteBuffer::wrap(param->write.value, param->write.len)); } @@ -263,7 +281,7 @@ void BLECharacteristic::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt memcpy(response.attr_value.value, param->write.value, param->write.len); esp_err_t err = - esp_ble_gatts_send_response(gatts_if, param->write.conn_id, param->write.trans_id, ESP_GATT_OK, &response); + esp_ble_gatts_send_response(gatts_if, param->write.conn_id, param->write.trans_id, status, &response); if (err != ESP_OK) { ESP_LOGE(TAG, "esp_ble_gatts_send_response failed: %d", err); @@ -280,9 +298,9 @@ void BLECharacteristic::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt } case ESP_GATTS_EXEC_WRITE_EVT: { - if (!this->write_event_) + // BLE stack will guarantee that ESP_GATTS_EXEC_WRITE_EVT is only received after prepared writes + if (this->value_.empty()) break; - this->write_event_ = false; if (param->exec_write.exec_write_flag == ESP_GATT_PREP_WRITE_EXEC) { if (this->on_write_callback_) { (*this->on_write_callback_)(this->value_, param->exec_write.conn_id); diff --git a/esphome/components/esp32_ble_server/ble_characteristic.h b/esphome/components/esp32_ble_server/ble_characteristic.h index b913915789..c2cdb1660c 100644 --- a/esphome/components/esp32_ble_server/ble_characteristic.h +++ b/esphome/components/esp32_ble_server/ble_characteristic.h @@ -77,7 +77,6 @@ class BLECharacteristic { } protected: - bool write_event_{false}; BLEService *service_{}; ESPBTUUID uuid_; esp_gatt_char_prop_t properties_; From 8c0cc3a2d8bfc978865e940b03e30262f9331ead Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 19:01:00 -0600 Subject: [PATCH 127/197] [udp] Register socket consumption for CONFIG_LWIP_MAX_SOCKETS (#14068) --- esphome/components/udp/__init__.py | 63 ++++++++++++++++++------------ 1 file changed, 39 insertions(+), 24 deletions(-) diff --git a/esphome/components/udp/__init__.py b/esphome/components/udp/__init__.py index bfaa5f2516..c9586d0b95 100644 --- a/esphome/components/udp/__init__.py +++ b/esphome/components/udp/__init__.py @@ -14,6 +14,7 @@ import esphome.config_validation as cv from esphome.const import CONF_DATA, CONF_ID, CONF_PORT, CONF_TRIGGER_ID from esphome.core import ID from esphome.cpp_generator import MockObj +from esphome.types import ConfigType CODEOWNERS = ["@clydebarrow"] DEPENDENCIES = ["network"] @@ -65,33 +66,47 @@ RELOCATED = { ) } -CONFIG_SCHEMA = cv.COMPONENT_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(UDPComponent), - cv.Optional(CONF_PORT, default=18511): cv.Any( - cv.port, - cv.Schema( + +def _consume_udp_sockets(config: ConfigType) -> ConfigType: + """Register socket needs for UDP component.""" + from esphome.components import socket + + # UDP uses up to 2 sockets: 1 broadcast + 1 listen + # Whether each is used depends on code generation, so register worst case + socket.consume_sockets(2, "udp")(config) + return config + + +CONFIG_SCHEMA = cv.All( + cv.COMPONENT_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(UDPComponent), + cv.Optional(CONF_PORT, default=18511): cv.Any( + cv.port, + cv.Schema( + { + cv.Required(CONF_LISTEN_PORT): cv.port, + cv.Required(CONF_BROADCAST_PORT): cv.port, + } + ), + ), + cv.Optional( + CONF_LISTEN_ADDRESS, default="255.255.255.255" + ): cv.ipv4address_multi_broadcast, + cv.Optional(CONF_ADDRESSES, default=["255.255.255.255"]): cv.ensure_list( + cv.ipv4address, + ), + cv.Optional(CONF_ON_RECEIVE): automation.validate_automation( { - cv.Required(CONF_LISTEN_PORT): cv.port, - cv.Required(CONF_BROADCAST_PORT): cv.port, + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + Trigger.template(trigger_args) + ), } ), - ), - cv.Optional( - CONF_LISTEN_ADDRESS, default="255.255.255.255" - ): cv.ipv4address_multi_broadcast, - cv.Optional(CONF_ADDRESSES, default=["255.255.255.255"]): cv.ensure_list( - cv.ipv4address, - ), - cv.Optional(CONF_ON_RECEIVE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - Trigger.template(trigger_args) - ), - } - ), - } -).extend(RELOCATED) + } + ).extend(RELOCATED), + _consume_udp_sockets, +) async def register_udp_client(var, config): From e4aa23abaac53320b89d99ecd39efe9e60cbe7bd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 19:01:37 -0600 Subject: [PATCH 128/197] [web_server] Double socket allocation to prevent connection exhaustion (#14067) --- esphome/components/web_server/__init__.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/esphome/components/web_server/__init__.py b/esphome/components/web_server/__init__.py index 8b02a6baee..294a5e0a15 100644 --- a/esphome/components/web_server/__init__.py +++ b/esphome/components/web_server/__init__.py @@ -144,9 +144,10 @@ def _consume_web_server_sockets(config: ConfigType) -> ConfigType: """Register socket needs for web_server component.""" from esphome.components import socket - # Web server needs 1 listening socket + typically 2 concurrent client connections - # (browser makes 2 connections for page + event stream) - sockets_needed = 3 + # Web server needs 1 listening socket + typically 5 concurrent client connections + # (browser opens connections for page resources, SSE event stream, and POST + # requests for entity control which may linger before closing) + sockets_needed = 6 socket.consume_sockets(sockets_needed, "web_server")(config) return config From 887172d663c5f7bd1ffc4a4ac35f99eecccf2a6c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 19:08:53 -0600 Subject: [PATCH 129/197] [pulse_counter] Fix compilation on ESP32-C6/C5/H2/P4 (#14070) --- esphome/components/pulse_counter/pulse_counter_sensor.cpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/esphome/components/pulse_counter/pulse_counter_sensor.cpp b/esphome/components/pulse_counter/pulse_counter_sensor.cpp index 8ac5a28d8f..ef4cc980f6 100644 --- a/esphome/components/pulse_counter/pulse_counter_sensor.cpp +++ b/esphome/components/pulse_counter/pulse_counter_sensor.cpp @@ -2,7 +2,7 @@ #include "esphome/core/log.h" #ifdef HAS_PCNT -#include +#include #include #endif @@ -117,9 +117,7 @@ bool HwPulseCounterStorage::pulse_counter_setup(InternalGPIOPin *pin) { } if (this->filter_us != 0) { - uint32_t apb_freq; - esp_clk_tree_src_get_freq_hz(SOC_MOD_CLK_APB, ESP_CLK_TREE_SRC_FREQ_PRECISION_CACHED, &apb_freq); - uint32_t max_glitch_ns = PCNT_LL_MAX_GLITCH_WIDTH * 1000000u / apb_freq; + uint32_t max_glitch_ns = PCNT_LL_MAX_GLITCH_WIDTH * 1000000u / (uint32_t) esp_clk_apb_freq(); pcnt_glitch_filter_config_t filter_config = { .max_glitch_ns = std::min(this->filter_us * 1000u, max_glitch_ns), }; From cb8b14e64b6c4fbd01f96e75106f0b2ed452ffcc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 21:00:24 -0600 Subject: [PATCH 130/197] [web_server] Fix water_heater JSON key names and move traits to DETAIL_ALL (#14064) --- esphome/components/web_server/web_server.cpp | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index dfd602be6b..c894d32a4b 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -1913,6 +1913,9 @@ std::string WebServer::water_heater_json_(water_heater::WaterHeater *obj, JsonDe JsonArray modes = root[ESPHOME_F("modes")].to(); for (auto m : traits.get_supported_modes()) modes.add(PSTR_LOCAL(water_heater::water_heater_mode_to_string(m))); + root[ESPHOME_F("min_temp")] = traits.get_min_temperature(); + root[ESPHOME_F("max_temp")] = traits.get_max_temperature(); + root[ESPHOME_F("step")] = traits.get_target_temperature_step(); this->add_sorting_info_(root, obj); } @@ -1935,10 +1938,6 @@ std::string WebServer::water_heater_json_(water_heater::WaterHeater *obj, JsonDe root[ESPHOME_F("target_temperature")] = target; } - root[ESPHOME_F("min_temperature")] = traits.get_min_temperature(); - root[ESPHOME_F("max_temperature")] = traits.get_max_temperature(); - root[ESPHOME_F("step")] = traits.get_target_temperature_step(); - if (traits.get_supports_away_mode()) { root[ESPHOME_F("away")] = obj->is_away(); } From 2491b4f85c1825b7dd8bc4273eafca643cf8c120 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 21:32:37 -0600 Subject: [PATCH 131/197] [ld2420] Use constexpr for compile-time constants (#14079) --- esphome/components/ld2420/ld2420.cpp | 118 +++++++++++++-------------- esphome/components/ld2420/ld2420.h | 6 +- 2 files changed, 62 insertions(+), 62 deletions(-) diff --git a/esphome/components/ld2420/ld2420.cpp b/esphome/components/ld2420/ld2420.cpp index 69b69f4a61..cf78a1a460 100644 --- a/esphome/components/ld2420/ld2420.cpp +++ b/esphome/components/ld2420/ld2420.cpp @@ -63,73 +63,73 @@ namespace esphome::ld2420 { static const char *const TAG = "ld2420"; // Local const's -static const uint16_t REFRESH_RATE_MS = 1000; +static constexpr uint16_t REFRESH_RATE_MS = 1000; // Command sets -static const uint16_t CMD_DISABLE_CONF = 0x00FE; -static const uint16_t CMD_ENABLE_CONF = 0x00FF; -static const uint16_t CMD_PARM_HIGH_TRESH = 0x0012; -static const uint16_t CMD_PARM_LOW_TRESH = 0x0021; -static const uint16_t CMD_PROTOCOL_VER = 0x0002; -static const uint16_t CMD_READ_ABD_PARAM = 0x0008; -static const uint16_t CMD_READ_REG_ADDR = 0x0020; -static const uint16_t CMD_READ_REGISTER = 0x0002; -static const uint16_t CMD_READ_SERIAL_NUM = 0x0011; -static const uint16_t CMD_READ_SYS_PARAM = 0x0013; -static const uint16_t CMD_READ_VERSION = 0x0000; -static const uint16_t CMD_RESTART = 0x0068; -static const uint16_t CMD_SYSTEM_MODE = 0x0000; -static const uint16_t CMD_SYSTEM_MODE_GR = 0x0003; -static const uint16_t CMD_SYSTEM_MODE_MTT = 0x0001; -static const uint16_t CMD_SYSTEM_MODE_SIMPLE = 0x0064; -static const uint16_t CMD_SYSTEM_MODE_DEBUG = 0x0000; -static const uint16_t CMD_SYSTEM_MODE_ENERGY = 0x0004; -static const uint16_t CMD_SYSTEM_MODE_VS = 0x0002; -static const uint16_t CMD_WRITE_ABD_PARAM = 0x0007; -static const uint16_t CMD_WRITE_REGISTER = 0x0001; -static const uint16_t CMD_WRITE_SYS_PARAM = 0x0012; +static constexpr uint16_t CMD_DISABLE_CONF = 0x00FE; +static constexpr uint16_t CMD_ENABLE_CONF = 0x00FF; +static constexpr uint16_t CMD_PARM_HIGH_TRESH = 0x0012; +static constexpr uint16_t CMD_PARM_LOW_TRESH = 0x0021; +static constexpr uint16_t CMD_PROTOCOL_VER = 0x0002; +static constexpr uint16_t CMD_READ_ABD_PARAM = 0x0008; +static constexpr uint16_t CMD_READ_REG_ADDR = 0x0020; +static constexpr uint16_t CMD_READ_REGISTER = 0x0002; +static constexpr uint16_t CMD_READ_SERIAL_NUM = 0x0011; +static constexpr uint16_t CMD_READ_SYS_PARAM = 0x0013; +static constexpr uint16_t CMD_READ_VERSION = 0x0000; +static constexpr uint16_t CMD_RESTART = 0x0068; +static constexpr uint16_t CMD_SYSTEM_MODE = 0x0000; +static constexpr uint16_t CMD_SYSTEM_MODE_GR = 0x0003; +static constexpr uint16_t CMD_SYSTEM_MODE_MTT = 0x0001; +static constexpr uint16_t CMD_SYSTEM_MODE_SIMPLE = 0x0064; +static constexpr uint16_t CMD_SYSTEM_MODE_DEBUG = 0x0000; +static constexpr uint16_t CMD_SYSTEM_MODE_ENERGY = 0x0004; +static constexpr uint16_t CMD_SYSTEM_MODE_VS = 0x0002; +static constexpr uint16_t CMD_WRITE_ABD_PARAM = 0x0007; +static constexpr uint16_t CMD_WRITE_REGISTER = 0x0001; +static constexpr uint16_t CMD_WRITE_SYS_PARAM = 0x0012; -static const uint8_t CMD_ABD_DATA_REPLY_SIZE = 0x04; -static const uint8_t CMD_ABD_DATA_REPLY_START = 0x0A; -static const uint8_t CMD_MAX_BYTES = 0x64; -static const uint8_t CMD_REG_DATA_REPLY_SIZE = 0x02; +static constexpr uint8_t CMD_ABD_DATA_REPLY_SIZE = 0x04; +static constexpr uint8_t CMD_ABD_DATA_REPLY_START = 0x0A; +static constexpr uint8_t CMD_MAX_BYTES = 0x64; +static constexpr uint8_t CMD_REG_DATA_REPLY_SIZE = 0x02; -static const uint8_t LD2420_ERROR_NONE = 0x00; -static const uint8_t LD2420_ERROR_TIMEOUT = 0x02; -static const uint8_t LD2420_ERROR_UNKNOWN = 0x01; +static constexpr uint8_t LD2420_ERROR_NONE = 0x00; +static constexpr uint8_t LD2420_ERROR_TIMEOUT = 0x02; +static constexpr uint8_t LD2420_ERROR_UNKNOWN = 0x01; // Register address values -static const uint16_t CMD_MIN_GATE_REG = 0x0000; -static const uint16_t CMD_MAX_GATE_REG = 0x0001; -static const uint16_t CMD_TIMEOUT_REG = 0x0004; -static const uint16_t CMD_GATE_MOVE_THRESH[TOTAL_GATES] = {0x0010, 0x0011, 0x0012, 0x0013, 0x0014, 0x0015, - 0x0016, 0x0017, 0x0018, 0x0019, 0x001A, 0x001B, - 0x001C, 0x001D, 0x001E, 0x001F}; -static const uint16_t CMD_GATE_STILL_THRESH[TOTAL_GATES] = {0x0020, 0x0021, 0x0022, 0x0023, 0x0024, 0x0025, - 0x0026, 0x0027, 0x0028, 0x0029, 0x002A, 0x002B, - 0x002C, 0x002D, 0x002E, 0x002F}; -static const uint32_t FACTORY_MOVE_THRESH[TOTAL_GATES] = {60000, 30000, 400, 250, 250, 250, 250, 250, - 250, 250, 250, 250, 250, 250, 250, 250}; -static const uint32_t FACTORY_STILL_THRESH[TOTAL_GATES] = {40000, 20000, 200, 200, 200, 200, 200, 150, - 150, 100, 100, 100, 100, 100, 100, 100}; -static const uint16_t FACTORY_TIMEOUT = 120; -static const uint16_t FACTORY_MIN_GATE = 1; -static const uint16_t FACTORY_MAX_GATE = 12; +static constexpr uint16_t CMD_MIN_GATE_REG = 0x0000; +static constexpr uint16_t CMD_MAX_GATE_REG = 0x0001; +static constexpr uint16_t CMD_TIMEOUT_REG = 0x0004; +static constexpr uint16_t CMD_GATE_MOVE_THRESH[TOTAL_GATES] = {0x0010, 0x0011, 0x0012, 0x0013, 0x0014, 0x0015, + 0x0016, 0x0017, 0x0018, 0x0019, 0x001A, 0x001B, + 0x001C, 0x001D, 0x001E, 0x001F}; +static constexpr uint16_t CMD_GATE_STILL_THRESH[TOTAL_GATES] = {0x0020, 0x0021, 0x0022, 0x0023, 0x0024, 0x0025, + 0x0026, 0x0027, 0x0028, 0x0029, 0x002A, 0x002B, + 0x002C, 0x002D, 0x002E, 0x002F}; +static constexpr uint32_t FACTORY_MOVE_THRESH[TOTAL_GATES] = {60000, 30000, 400, 250, 250, 250, 250, 250, + 250, 250, 250, 250, 250, 250, 250, 250}; +static constexpr uint32_t FACTORY_STILL_THRESH[TOTAL_GATES] = {40000, 20000, 200, 200, 200, 200, 200, 150, + 150, 100, 100, 100, 100, 100, 100, 100}; +static constexpr uint16_t FACTORY_TIMEOUT = 120; +static constexpr uint16_t FACTORY_MIN_GATE = 1; +static constexpr uint16_t FACTORY_MAX_GATE = 12; // COMMAND_BYTE Header & Footer -static const uint32_t CMD_FRAME_FOOTER = 0x01020304; -static const uint32_t CMD_FRAME_HEADER = 0xFAFBFCFD; -static const uint32_t DEBUG_FRAME_FOOTER = 0xFAFBFCFD; -static const uint32_t DEBUG_FRAME_HEADER = 0x1410BFAA; -static const uint32_t ENERGY_FRAME_FOOTER = 0xF5F6F7F8; -static const uint32_t ENERGY_FRAME_HEADER = 0xF1F2F3F4; -static const int CALIBRATE_VERSION_MIN = 154; -static const uint8_t CMD_FRAME_COMMAND = 6; -static const uint8_t CMD_FRAME_DATA_LENGTH = 4; -static const uint8_t CMD_FRAME_STATUS = 7; -static const uint8_t CMD_ERROR_WORD = 8; -static const uint8_t ENERGY_SENSOR_START = 9; -static const uint8_t CALIBRATE_REPORT_INTERVAL = 4; +static constexpr uint32_t CMD_FRAME_FOOTER = 0x01020304; +static constexpr uint32_t CMD_FRAME_HEADER = 0xFAFBFCFD; +static constexpr uint32_t DEBUG_FRAME_FOOTER = 0xFAFBFCFD; +static constexpr uint32_t DEBUG_FRAME_HEADER = 0x1410BFAA; +static constexpr uint32_t ENERGY_FRAME_FOOTER = 0xF5F6F7F8; +static constexpr uint32_t ENERGY_FRAME_HEADER = 0xF1F2F3F4; +static constexpr int CALIBRATE_VERSION_MIN = 154; +static constexpr uint8_t CMD_FRAME_COMMAND = 6; +static constexpr uint8_t CMD_FRAME_DATA_LENGTH = 4; +static constexpr uint8_t CMD_FRAME_STATUS = 7; +static constexpr uint8_t CMD_ERROR_WORD = 8; +static constexpr uint8_t ENERGY_SENSOR_START = 9; +static constexpr uint8_t CALIBRATE_REPORT_INTERVAL = 4; static const char *const OP_NORMAL_MODE_STRING = "Normal"; static const char *const OP_SIMPLE_MODE_STRING = "Simple"; diff --git a/esphome/components/ld2420/ld2420.h b/esphome/components/ld2420/ld2420.h index 6d81f86497..02250c5911 100644 --- a/esphome/components/ld2420/ld2420.h +++ b/esphome/components/ld2420/ld2420.h @@ -20,9 +20,9 @@ namespace esphome::ld2420 { -static const uint8_t CALIBRATE_SAMPLES = 64; -static const uint8_t MAX_LINE_LENGTH = 46; // Max characters for serial buffer -static const uint8_t TOTAL_GATES = 16; +static constexpr uint8_t CALIBRATE_SAMPLES = 64; +static constexpr uint8_t MAX_LINE_LENGTH = 46; // Max characters for serial buffer +static constexpr uint8_t TOTAL_GATES = 16; enum OpMode : uint8_t { OP_NORMAL_MODE = 1, From 25b14f995309a7dfbb4857d53f8c9a67aa8a7308 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 19 Feb 2026 09:27:05 -0600 Subject: [PATCH 132/197] [e131] Fix E1.31 on ESP8266 and RP2040 by restoring WiFiUDP support (#14086) --- esphome/components/e131/e131.cpp | 31 ++++++++++++++++++++++++- esphome/components/e131/e131.h | 8 +++++++ esphome/components/e131/e131_packet.cpp | 2 ++ 3 files changed, 40 insertions(+), 1 deletion(-) diff --git a/esphome/components/e131/e131.cpp b/esphome/components/e131/e131.cpp index 4187857901..941927122c 100644 --- a/esphome/components/e131/e131.cpp +++ b/esphome/components/e131/e131.cpp @@ -14,12 +14,17 @@ static const int PORT = 5568; E131Component::E131Component() {} E131Component::~E131Component() { +#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) if (this->socket_) { this->socket_->close(); } +#elif defined(USE_SOCKET_IMPL_LWIP_TCP) + this->udp_.stop(); +#endif } void E131Component::setup() { +#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) this->socket_ = socket::socket_ip(SOCK_DGRAM, IPPROTO_IP); int enable = 1; @@ -50,6 +55,13 @@ void E131Component::setup() { this->mark_failed(); return; } +#elif defined(USE_SOCKET_IMPL_LWIP_TCP) + if (!this->udp_.begin(PORT)) { + ESP_LOGW(TAG, "Cannot bind E1.31 to port %d.", PORT); + this->mark_failed(); + return; + } +#endif join_igmp_groups_(); } @@ -59,19 +71,36 @@ void E131Component::loop() { int universe = 0; uint8_t buf[1460]; +#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) ssize_t len = this->socket_->read(buf, sizeof(buf)); if (len == -1) { return; } if (!this->packet_(buf, (size_t) len, universe, packet)) { - ESP_LOGV(TAG, "Invalid packet received of size %zd.", len); + ESP_LOGV(TAG, "Invalid packet received of size %d.", (int) len); return; } if (!this->process_(universe, packet)) { ESP_LOGV(TAG, "Ignored packet for %d universe of size %d.", universe, packet.count); } +#elif defined(USE_SOCKET_IMPL_LWIP_TCP) + while (auto packet_size = this->udp_.parsePacket()) { + auto len = this->udp_.read(buf, sizeof(buf)); + if (len <= 0) + continue; + + if (!this->packet_(buf, (size_t) len, universe, packet)) { + ESP_LOGV(TAG, "Invalid packet received of size %d.", (int) len); + continue; + } + + if (!this->process_(universe, packet)) { + ESP_LOGV(TAG, "Ignored packet for %d universe of size %d.", universe, packet.count); + } + } +#endif } void E131Component::add_effect(E131AddressableLightEffect *light_effect) { diff --git a/esphome/components/e131/e131.h b/esphome/components/e131/e131.h index d4b272eae2..72da9ddebe 100644 --- a/esphome/components/e131/e131.h +++ b/esphome/components/e131/e131.h @@ -1,7 +1,11 @@ #pragma once #include "esphome/core/defines.h" #ifdef USE_NETWORK +#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) #include "esphome/components/socket/socket.h" +#elif defined(USE_SOCKET_IMPL_LWIP_TCP) +#include +#endif #include "esphome/core/component.h" #include @@ -45,7 +49,11 @@ class E131Component : public esphome::Component { void leave_(int universe); E131ListenMethod listen_method_{E131_MULTICAST}; +#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) std::unique_ptr socket_; +#elif defined(USE_SOCKET_IMPL_LWIP_TCP) + WiFiUDP udp_; +#endif std::vector light_effects_; std::map universe_consumers_; }; diff --git a/esphome/components/e131/e131_packet.cpp b/esphome/components/e131/e131_packet.cpp index ed081e5758..aa5c740454 100644 --- a/esphome/components/e131/e131_packet.cpp +++ b/esphome/components/e131/e131_packet.cpp @@ -62,8 +62,10 @@ const size_t E131_MIN_PACKET_SIZE = reinterpret_cast(&((E131RawPacket *) bool E131Component::join_igmp_groups_() { if (listen_method_ != E131_MULTICAST) return false; +#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) if (this->socket_ == nullptr) return false; +#endif for (auto universe : universe_consumers_) { if (!universe.second) From 2d2178c90a7d8357a1b51cd51c02b6f800d70559 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 19 Feb 2026 11:00:34 -0500 Subject: [PATCH 133/197] [socket] Fix IPv6 compilation error on host platform (#14101) Co-authored-by: Claude Opus 4.6 --- esphome/components/socket/socket.cpp | 10 ++++++++-- tests/components/socket/common.yaml | 11 +++++++++++ tests/components/socket/test-ipv6.esp32-idf.yaml | 4 ++++ tests/components/socket/test-ipv6.host.yaml | 4 ++++ tests/components/socket/test.esp32-idf.yaml | 1 + tests/components/socket/test.host.yaml | 3 +++ 6 files changed, 31 insertions(+), 2 deletions(-) create mode 100644 tests/components/socket/common.yaml create mode 100644 tests/components/socket/test-ipv6.esp32-idf.yaml create mode 100644 tests/components/socket/test-ipv6.host.yaml create mode 100644 tests/components/socket/test.esp32-idf.yaml create mode 100644 tests/components/socket/test.host.yaml diff --git a/esphome/components/socket/socket.cpp b/esphome/components/socket/socket.cpp index 2fcc162ead..6154c497e0 100644 --- a/esphome/components/socket/socket.cpp +++ b/esphome/components/socket/socket.cpp @@ -59,8 +59,14 @@ size_t format_sockaddr_to(const struct sockaddr *addr_ptr, socklen_t len, std::s #if USE_NETWORK_IPV6 else if (addr_ptr->sa_family == AF_INET6 && len >= sizeof(sockaddr_in6)) { const auto *addr = reinterpret_cast(addr_ptr); -#ifndef USE_SOCKET_IMPL_LWIP_TCP - // Format IPv4-mapped IPv6 addresses as regular IPv4 (not supported on ESP8266 raw TCP) +#ifdef USE_HOST + // Format IPv4-mapped IPv6 addresses as regular IPv4 (POSIX layout, no LWIP union) + if (IN6_IS_ADDR_V4MAPPED(&addr->sin6_addr) && + esphome_inet_ntop4(&addr->sin6_addr.s6_addr[12], buf.data(), buf.size()) != nullptr) { + return strlen(buf.data()); + } +#elif !defined(USE_SOCKET_IMPL_LWIP_TCP) + // Format IPv4-mapped IPv6 addresses as regular IPv4 (LWIP layout) if (addr->sin6_addr.un.u32_addr[0] == 0 && addr->sin6_addr.un.u32_addr[1] == 0 && addr->sin6_addr.un.u32_addr[2] == htonl(0xFFFF) && esphome_inet_ntop4(&addr->sin6_addr.un.u32_addr[3], buf.data(), buf.size()) != nullptr) { diff --git a/tests/components/socket/common.yaml b/tests/components/socket/common.yaml new file mode 100644 index 0000000000..aaf49f1611 --- /dev/null +++ b/tests/components/socket/common.yaml @@ -0,0 +1,11 @@ +substitutions: + network_enable_ipv6: "false" + +socket: + +wifi: + ssid: MySSID + password: password1 + +network: + enable_ipv6: ${network_enable_ipv6} diff --git a/tests/components/socket/test-ipv6.esp32-idf.yaml b/tests/components/socket/test-ipv6.esp32-idf.yaml new file mode 100644 index 0000000000..da1324b17e --- /dev/null +++ b/tests/components/socket/test-ipv6.esp32-idf.yaml @@ -0,0 +1,4 @@ +substitutions: + network_enable_ipv6: "true" + +<<: !include common.yaml diff --git a/tests/components/socket/test-ipv6.host.yaml b/tests/components/socket/test-ipv6.host.yaml new file mode 100644 index 0000000000..fdd52c574e --- /dev/null +++ b/tests/components/socket/test-ipv6.host.yaml @@ -0,0 +1,4 @@ +socket: + +network: + enable_ipv6: true diff --git a/tests/components/socket/test.esp32-idf.yaml b/tests/components/socket/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/socket/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/socket/test.host.yaml b/tests/components/socket/test.host.yaml new file mode 100644 index 0000000000..e0c5d7cea3 --- /dev/null +++ b/tests/components/socket/test.host.yaml @@ -0,0 +1,3 @@ +socket: + +network: From a343ff19895c0699671766f7ee8a0b0ec81bd321 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 19 Feb 2026 12:00:05 -0500 Subject: [PATCH 134/197] [ethernet] Improve clk_mode deprecation warning with actionable YAML (#14104) Co-authored-by: Claude Opus 4.6 --- esphome/components/ethernet/__init__.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py index 52f5f44d41..935d2004d4 100644 --- a/esphome/components/ethernet/__init__.py +++ b/esphome/components/ethernet/__init__.py @@ -218,12 +218,19 @@ def _validate(config): ) elif config[CONF_TYPE] != "OPENETH": if CONF_CLK_MODE in config: + mode, pin = CLK_MODES_DEPRECATED[config[CONF_CLK_MODE]] LOGGER.warning( - "[ethernet] The 'clk_mode' option is deprecated and will be removed in ESPHome 2026.1. " - "Please update your configuration to use 'clk' instead." + "[ethernet] The 'clk_mode' option is deprecated. " + "Please replace 'clk_mode: %s' with:\n" + " clk:\n" + " mode: %s\n" + " pin: %s\n" + "Removal scheduled for 2026.7.0.", + config[CONF_CLK_MODE], + mode, + pin, ) - mode = CLK_MODES_DEPRECATED[config[CONF_CLK_MODE]] - config[CONF_CLK] = CLK_SCHEMA({CONF_MODE: mode[0], CONF_PIN: mode[1]}) + config[CONF_CLK] = CLK_SCHEMA({CONF_MODE: mode, CONF_PIN: pin}) del config[CONF_CLK_MODE] elif CONF_CLK not in config: raise cv.Invalid("'clk' is a required option for [ethernet].") From ac76fc44098f2a7adb74b212093c9353ff0c8912 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 19 Feb 2026 12:25:26 -0500 Subject: [PATCH 135/197] [pulse_counter] Fix build failure when use_pcnt is false (#14111) Co-authored-by: Claude Opus 4.6 --- .../components/pulse_counter/pulse_counter_sensor.h | 4 ++-- .../pulse_counter/test-no-pcnt.esp32-idf.yaml | 10 ++++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) create mode 100644 tests/components/pulse_counter/test-no-pcnt.esp32-idf.yaml diff --git a/esphome/components/pulse_counter/pulse_counter_sensor.h b/esphome/components/pulse_counter/pulse_counter_sensor.h index a7913d5d66..7a68858099 100644 --- a/esphome/components/pulse_counter/pulse_counter_sensor.h +++ b/esphome/components/pulse_counter/pulse_counter_sensor.h @@ -8,10 +8,10 @@ #if defined(USE_ESP32) #include -#ifdef SOC_PCNT_SUPPORTED +#if defined(SOC_PCNT_SUPPORTED) && __has_include() #include #define HAS_PCNT -#endif // SOC_PCNT_SUPPORTED +#endif // defined(SOC_PCNT_SUPPORTED) && __has_include() #endif // USE_ESP32 namespace esphome { diff --git a/tests/components/pulse_counter/test-no-pcnt.esp32-idf.yaml b/tests/components/pulse_counter/test-no-pcnt.esp32-idf.yaml new file mode 100644 index 0000000000..cd15cc781d --- /dev/null +++ b/tests/components/pulse_counter/test-no-pcnt.esp32-idf.yaml @@ -0,0 +1,10 @@ +sensor: + - platform: pulse_counter + name: Pulse Counter + pin: 4 + use_pcnt: false + count_mode: + rising_edge: INCREMENT + falling_edge: DECREMENT + internal_filter: 13us + update_interval: 15s From d78496321e2a65208fe83d7ea93d7107914419b0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 19 Feb 2026 11:41:00 -0600 Subject: [PATCH 136/197] [esp32_ble] Enable CONFIG_BT_RELEASE_IRAM on ESP32-C2 (#14109) Co-authored-by: Claude Opus 4.6 --- esphome/components/esp32_ble/__init__.py | 10 ++++++++++ tests/components/esp32_ble/test.esp32-c2-idf.yaml | 5 +++++ 2 files changed, 15 insertions(+) create mode 100644 tests/components/esp32_ble/test.esp32-c2-idf.yaml diff --git a/esphome/components/esp32_ble/__init__.py b/esphome/components/esp32_ble/__init__.py index dcc3ce71cf..d2020ada22 100644 --- a/esphome/components/esp32_ble/__init__.py +++ b/esphome/components/esp32_ble/__init__.py @@ -9,6 +9,7 @@ from esphome import automation import esphome.codegen as cg from esphome.components import socket from esphome.components.esp32 import add_idf_sdkconfig_option, const, get_esp32_variant +from esphome.components.esp32.const import VARIANT_ESP32C2 import esphome.config_validation as cv from esphome.const import ( CONF_ENABLE_ON_BOOT, @@ -387,6 +388,15 @@ def final_validation(config): f"Name '{name}' is too long, maximum length is {max_length} characters" ) + # ESP32-C2 has very limited RAM (~272KB). Without releasing BLE IRAM, + # esp_bt_controller_init fails with ESP_ERR_NO_MEM. + # CONFIG_BT_RELEASE_IRAM changes the memory layout so IRAM and DRAM share + # space more flexibly, giving the BT controller enough contiguous memory. + # This requires CONFIG_ESP_SYSTEM_PMP_IDRAM_SPLIT to be disabled. + if get_esp32_variant() == VARIANT_ESP32C2: + add_idf_sdkconfig_option("CONFIG_BT_RELEASE_IRAM", True) + add_idf_sdkconfig_option("CONFIG_ESP_SYSTEM_PMP_IDRAM_SPLIT", False) + # Set GATT Client/Server sdkconfig options based on which components are loaded full_config = fv.full_config.get() diff --git a/tests/components/esp32_ble/test.esp32-c2-idf.yaml b/tests/components/esp32_ble/test.esp32-c2-idf.yaml new file mode 100644 index 0000000000..f8defaf28f --- /dev/null +++ b/tests/components/esp32_ble/test.esp32-c2-idf.yaml @@ -0,0 +1,5 @@ +<<: !include common.yaml + +esp32_ble: + io_capability: keyboard_only + disable_bt_logs: false From 0fc09462ff73e82b380389c3ae1f885947fc70f5 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 19 Feb 2026 12:45:04 -0500 Subject: [PATCH 137/197] [safe_mode] Log brownout as reset reason on OTA rollback (#14113) Co-authored-by: Claude Opus 4.6 --- esphome/components/safe_mode/safe_mode.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/esphome/components/safe_mode/safe_mode.cpp b/esphome/components/safe_mode/safe_mode.cpp index f32511531a..6cae4bf9d5 100644 --- a/esphome/components/safe_mode/safe_mode.cpp +++ b/esphome/components/safe_mode/safe_mode.cpp @@ -11,6 +11,7 @@ #if defined(USE_ESP32) && defined(USE_OTA_ROLLBACK) #include +#include #endif namespace esphome::safe_mode { @@ -54,6 +55,10 @@ void SafeModeComponent::dump_config() { "OTA rollback detected! Rolled back from partition '%s'\n" "The device reset before the boot was marked successful", last_invalid->label); + if (esp_reset_reason() == ESP_RST_BROWNOUT) { + ESP_LOGW(TAG, "Last reset was due to brownout - check your power supply!\n" + "See https://esphome.io/guides/faq.html#brownout-detector-was-triggered"); + } } #endif } From f412ab4f8b954ca7f81125237df452832decaa56 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 19 Feb 2026 15:14:48 -0500 Subject: [PATCH 138/197] [wifi] Sync output_power with PHY max TX power to prevent brownout (#14118) Co-authored-by: Claude Opus 4.6 --- esphome/components/wifi/__init__.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index e865de8663..afceec6c54 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -1,4 +1,5 @@ import logging +import math from esphome import automation from esphome.automation import Condition @@ -493,6 +494,13 @@ async def to_code(config): cg.add(var.set_passive_scan(True)) if CONF_OUTPUT_POWER in config: cg.add(var.set_output_power(config[CONF_OUTPUT_POWER])) + if CORE.is_esp32: + # Set PHY max TX power to match output_power so calibration also uses + # reduced power. This prevents brownout during PHY init on marginal + # power supplies, which is critical for OTA updates with rollback enabled. + # Kconfig range is 10-20, ESPHome allows 8.5-20.5 + phy_tx_power = max(10, min(20, math.ceil(config[CONF_OUTPUT_POWER]))) + add_idf_sdkconfig_option("CONFIG_ESP_PHY_MAX_WIFI_TX_POWER", phy_tx_power) # enable_on_boot defaults to true in C++ - only set if false if not config[CONF_ENABLE_ON_BOOT]: cg.add(var.set_enable_on_boot(False)) From 7bdeb32a8a1ca43e07b64e80daa0afa13bc68206 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 19 Feb 2026 17:48:18 -0600 Subject: [PATCH 139/197] [uart] Always call pin setup for UART0 default pins on ESP-IDF (#14130) --- .../uart/uart_component_esp_idf.cpp | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/esphome/components/uart/uart_component_esp_idf.cpp b/esphome/components/uart/uart_component_esp_idf.cpp index 6c242220a6..ea7a09fee6 100644 --- a/esphome/components/uart/uart_component_esp_idf.cpp +++ b/esphome/components/uart/uart_component_esp_idf.cpp @@ -19,6 +19,13 @@ namespace esphome::uart { static const char *const TAG = "uart.idf"; +/// Check if a pin number matches one of the default UART0 GPIO pins. +/// These pins may have residual state from the boot console that requires +/// explicit reset before UART reconfiguration (ESP-IDF issue #17459). +static constexpr bool is_default_uart0_pin(int8_t pin_num) { + return pin_num == U0TXD_GPIO_NUM || pin_num == U0RXD_GPIO_NUM; +} + uart_config_t IDFUARTComponent::get_config_() { uart_parity_t parity = UART_PARITY_DISABLE; if (this->parity_ == UART_CONFIG_PARITY_EVEN) { @@ -150,20 +157,26 @@ void IDFUARTComponent::load_settings(bool dump_config) { // Commit 9ed617fb17 removed gpio_func_sel() calls from uart_set_pin(), which breaks // UART on default UART0 pins that may have residual state from boot console. // Reset these pins before configuring UART to ensure they're in a clean state. - if (tx == U0TXD_GPIO_NUM || tx == U0RXD_GPIO_NUM) { + if (is_default_uart0_pin(tx)) { gpio_reset_pin(static_cast(tx)); } - if (rx == U0TXD_GPIO_NUM || rx == U0RXD_GPIO_NUM) { + if (is_default_uart0_pin(rx)) { gpio_reset_pin(static_cast(rx)); } - // Setup pins after reset to preserve open drain/pullup/pulldown flags + // Setup pins after reset to configure GPIO direction and pull resistors. + // For UART0 default pins, setup() must always be called because gpio_reset_pin() + // above sets GPIO_MODE_DISABLE which disables the input buffer. Without setup(), + // uart_set_pin() on ESP-IDF 5.4.2+ does not re-enable the input buffer for + // IOMUX-connected pins, so the RX pin cannot receive data (see issue #10132). + // For other pins, only call setup() if pull or open-drain flags are set to avoid + // disturbing the default pin state which breaks some external components (#11823). auto setup_pin_if_needed = [](InternalGPIOPin *pin) { if (!pin) { return; } const auto mask = gpio::Flags::FLAG_OPEN_DRAIN | gpio::Flags::FLAG_PULLUP | gpio::Flags::FLAG_PULLDOWN; - if ((pin->get_flags() & mask) != gpio::Flags::FLAG_NONE) { + if (is_default_uart0_pin(pin->get_pin()) || (pin->get_flags() & mask) != gpio::Flags::FLAG_NONE) { pin->setup(); } }; From e7e1acc0a2fb1e1c3c5ce7fc448cca2050bb2d17 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 19 Feb 2026 19:12:37 -0500 Subject: [PATCH 140/197] [pulse_counter] Fix PCNT glitch filter calculation off by 1000x (#14132) Co-authored-by: Claude Opus 4.6 --- esphome/components/pulse_counter/pulse_counter_sensor.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/pulse_counter/pulse_counter_sensor.cpp b/esphome/components/pulse_counter/pulse_counter_sensor.cpp index ef4cc980f6..5e62c0a410 100644 --- a/esphome/components/pulse_counter/pulse_counter_sensor.cpp +++ b/esphome/components/pulse_counter/pulse_counter_sensor.cpp @@ -117,7 +117,7 @@ bool HwPulseCounterStorage::pulse_counter_setup(InternalGPIOPin *pin) { } if (this->filter_us != 0) { - uint32_t max_glitch_ns = PCNT_LL_MAX_GLITCH_WIDTH * 1000000u / (uint32_t) esp_clk_apb_freq(); + uint32_t max_glitch_ns = PCNT_LL_MAX_GLITCH_WIDTH * 1000u / ((uint32_t) esp_clk_apb_freq() / 1000000u); pcnt_glitch_filter_config_t filter_config = { .max_glitch_ns = std::min(this->filter_us * 1000u, max_glitch_ns), }; From d19c1b689af4dbbe85e24f02580e586f6025ea73 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 19 Feb 2026 21:56:20 -0500 Subject: [PATCH 141/197] [ld2450] Add frame header synchronization to fix initialization regression (#14135) Co-authored-by: Claude Opus 4.6 Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- esphome/components/ld2450/ld2450.cpp | 22 ++- esphome/components/ld2450/ld2450.h | 1 + tests/components/ld2450/common.h | 61 +++++++ tests/components/ld2450/ld2450_readline.cpp | 181 ++++++++++++++++++++ 4 files changed, 263 insertions(+), 2 deletions(-) create mode 100644 tests/components/ld2450/common.h create mode 100644 tests/components/ld2450/ld2450_readline.cpp diff --git a/esphome/components/ld2450/ld2450.cpp b/esphome/components/ld2450/ld2450.cpp index 1ea5c18271..2af45235a3 100644 --- a/esphome/components/ld2450/ld2450.cpp +++ b/esphome/components/ld2450/ld2450.cpp @@ -769,15 +769,33 @@ void LD2450Component::readline_(int readch) { return; // No data available } + // Frame header synchronization: verify first 4 bytes match a known frame header. + // This prevents the parser from accumulating mid-frame data after losing sync + // (e.g. after module restart or UART noise). + if (this->buffer_pos_ < HEADER_FOOTER_SIZE) { + const uint8_t byte = static_cast(readch); + // Verify header bytes match the frame type established by byte 0 + if (this->buffer_pos_ > 0) { + const uint8_t *expected = (this->buffer_data_[0] == DATA_FRAME_HEADER[0]) ? DATA_FRAME_HEADER : CMD_FRAME_HEADER; + if (byte != expected[this->buffer_pos_]) { + this->buffer_pos_ = 0; // Reset and fall through to check if this byte starts a new frame + } + } + // First byte must match start of a data or command frame header + if (this->buffer_pos_ == 0 && byte != DATA_FRAME_HEADER[0] && byte != CMD_FRAME_HEADER[0]) { + return; + } + } + if (this->buffer_pos_ < MAX_LINE_LENGTH - 1) { this->buffer_data_[this->buffer_pos_++] = readch; this->buffer_data_[this->buffer_pos_] = 0; } else { - // We should never get here, but just in case... ESP_LOGW(TAG, "Max command length exceeded; ignoring"); this->buffer_pos_ = 0; + return; } - if (this->buffer_pos_ < 4) { + if (this->buffer_pos_ < HEADER_FOOTER_SIZE) { return; // Not enough data to process yet } if (this->buffer_data_[this->buffer_pos_ - 2] == DATA_FRAME_FOOTER[0] && diff --git a/esphome/components/ld2450/ld2450.h b/esphome/components/ld2450/ld2450.h index fe69cd81d0..44e5912b2a 100644 --- a/esphome/components/ld2450/ld2450.h +++ b/esphome/components/ld2450/ld2450.h @@ -1,5 +1,6 @@ #pragma once +#include "esphome/core/automation.h" #include "esphome/core/defines.h" #include "esphome/core/component.h" #ifdef USE_SENSOR diff --git a/tests/components/ld2450/common.h b/tests/components/ld2450/common.h new file mode 100644 index 0000000000..d5ffbe1295 --- /dev/null +++ b/tests/components/ld2450/common.h @@ -0,0 +1,61 @@ +#pragma once +#include +#include +#include +#include +#include +#include "esphome/components/ld2450/ld2450.h" +#include "esphome/components/uart/uart_component.h" + +namespace esphome::ld2450::testing { + +// Mock UART component to satisfy UARTDevice parent requirement. +class MockUARTComponent : public uart::UARTComponent { + public: + void write_array(const uint8_t *data, size_t len) override {} + MOCK_METHOD(bool, read_array, (uint8_t * data, size_t len), (override)); + MOCK_METHOD(bool, peek_byte, (uint8_t * data), (override)); + MOCK_METHOD(size_t, available, (), (override)); + MOCK_METHOD(void, flush, (), (override)); + MOCK_METHOD(void, check_logger_conflict, (), (override)); +}; + +// Expose protected members for testing. +class TestableLD2450 : public LD2450Component { + public: + using LD2450Component::buffer_data_; + using LD2450Component::buffer_pos_; + using LD2450Component::readline_; + + void feed(const std::vector &data) { + for (uint8_t byte : data) { + this->readline_(byte); + } + } +}; + +// LD2450 periodic data frame: header (4) + 3 targets * 8 bytes + footer (2) = 30 bytes +// All-zero targets means no presence detected. +inline std::vector make_periodic_frame(uint8_t fill = 0x00) { + std::vector frame = {0xAA, 0xFF, 0x03, 0x00}; // DATA_FRAME_HEADER + for (int i = 0; i < 24; i++) { + frame.push_back(fill); // 3 targets * 8 bytes + } + frame.push_back(0x55); // DATA_FRAME_FOOTER + frame.push_back(0xCC); + return frame; +} + +// LD2450 command ACK frame for CMD_ENABLE_CONF (0xFF), successful. +// header (4) + length (2) + command (2) + result (2) + footer (4) = 14 bytes +inline std::vector make_ack_frame() { + return { + 0xFD, 0xFC, 0xFB, 0xFA, // CMD_FRAME_HEADER + 0x04, 0x00, // length = 4 + 0xFF, 0x01, // command = enable_conf, status = success + 0x00, 0x00, // result = ok + 0x04, 0x03, 0x02, 0x01 // CMD_FRAME_FOOTER + }; +} + +} // namespace esphome::ld2450::testing diff --git a/tests/components/ld2450/ld2450_readline.cpp b/tests/components/ld2450/ld2450_readline.cpp new file mode 100644 index 0000000000..68b1dd6881 --- /dev/null +++ b/tests/components/ld2450/ld2450_readline.cpp @@ -0,0 +1,181 @@ +#include "common.h" + +namespace esphome::ld2450::testing { + +class LD2450ReadlineTest : public ::testing::Test { + protected: + void SetUp() override { + this->ld2450_.set_uart_parent(&this->mock_uart_); + // Ensure clean state + ASSERT_EQ(this->ld2450_.buffer_pos_, 0); + } + + MockUARTComponent mock_uart_; + TestableLD2450 ld2450_; +}; + +// --- Good data tests --- + +TEST_F(LD2450ReadlineTest, ValidPeriodicFrame) { + auto frame = make_periodic_frame(); + this->ld2450_.feed(frame); + // After a complete valid frame, buffer should be reset + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); +} + +TEST_F(LD2450ReadlineTest, ValidCommandAckFrame) { + auto frame = make_ack_frame(); + this->ld2450_.feed(frame); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); +} + +TEST_F(LD2450ReadlineTest, BackToBackPeriodicFrames) { + auto frame = make_periodic_frame(); + for (int i = 0; i < 5; i++) { + this->ld2450_.feed(frame); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0) << "Frame " << i << " not processed"; + } +} + +TEST_F(LD2450ReadlineTest, BackToBackMixedFrames) { + auto periodic = make_periodic_frame(); + auto ack = make_ack_frame(); + this->ld2450_.feed(periodic); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); + this->ld2450_.feed(ack); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); + this->ld2450_.feed(periodic); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); +} + +// --- Garbage rejection tests --- + +TEST_F(LD2450ReadlineTest, GarbageDiscarded) { + // Feed bytes that don't match any header start byte + std::vector garbage = {0x01, 0x02, 0x03, 0x42, 0x99, 0x00, 0xFF, 0x7F}; + this->ld2450_.feed(garbage); + // Header sync should discard all of these + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); +} + +TEST_F(LD2450ReadlineTest, GarbageThenValidFrame) { + std::vector garbage = {0x01, 0x02, 0x03, 0x42, 0x99}; + this->ld2450_.feed(garbage); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); + + auto frame = make_periodic_frame(); + this->ld2450_.feed(frame); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); +} + +// --- Header synchronization tests --- + +TEST_F(LD2450ReadlineTest, PartialDataHeaderThenMismatch) { + // Start of a data frame header, then invalid byte + this->ld2450_.feed({0xAA, 0xFF, 0x42}); // 0x42 doesn't match DATA_FRAME_HEADER[2] (0x03) + // Parser should have reset + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); +} + +TEST_F(LD2450ReadlineTest, PartialCmdHeaderThenMismatch) { + // Start of a command frame header, then invalid byte + this->ld2450_.feed({0xFD, 0xFC, 0xFB, 0x42}); // 0x42 doesn't match CMD_FRAME_HEADER[3] (0xFA) + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); +} + +TEST_F(LD2450ReadlineTest, PartialHeaderThenValidFrame) { + // Partial header that fails, then a complete valid frame + this->ld2450_.feed({0xAA, 0xFF, 0x42}); // Fails at byte 3 + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); + + auto frame = make_periodic_frame(); + this->ld2450_.feed(frame); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); +} + +TEST_F(LD2450ReadlineTest, HeaderMismatchRecoveryOnNewHeaderByte) { + // Start data header, mismatch at byte 2, but mismatch byte is start of command header + this->ld2450_.feed({0xAA, 0xFF}); + EXPECT_EQ(this->ld2450_.buffer_pos_, 2); // Accumulating header + + this->ld2450_.feed({0xFD}); // Doesn't match DATA_FRAME_HEADER[2]=0x03, but IS CMD_FRAME_HEADER[0] + // Parser should reset and start new frame with 0xFD + EXPECT_EQ(this->ld2450_.buffer_pos_, 1); + EXPECT_EQ(this->ld2450_.buffer_data_[0], 0xFD); +} + +// --- Mid-frame / overflow recovery tests --- + +TEST_F(LD2450ReadlineTest, MidFrameDataRecovery) { + // Simulate starting mid-frame: feed the tail end of a periodic frame (no valid header) + // These bytes would be part of target data in a real frame + std::vector mid_frame = {0x10, 0x20, 0x30, 0x40, 0x55, 0xCC}; + this->ld2450_.feed(mid_frame); + // All discarded (none match header start bytes) + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); + + // Now feed a valid frame + auto frame = make_periodic_frame(); + this->ld2450_.feed(frame); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); +} + +TEST_F(LD2450ReadlineTest, OverflowRecovery) { + // Feed a valid data frame header followed by enough filler to cause overflow. + // Header (4) + 36 filler = 40 bytes in buffer. The 41st byte triggers overflow. + std::vector overflow_data = {0xAA, 0xFF, 0x03, 0x00}; // Valid header + for (int i = 0; i < 37; i++) { + overflow_data.push_back(0x11); // Filler that won't match any footer + } + // 41 bytes total: 40 stored, 41st triggers overflow and resets buffer_pos_ to 0 + this->ld2450_.feed(overflow_data); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); + + // Feed a valid frame and verify recovery + auto frame = make_periodic_frame(); + this->ld2450_.feed(frame); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); +} + +TEST_F(LD2450ReadlineTest, RepeatedOverflowDoesNotLoop) { + // Simulate the bug scenario: repeated overflows should not prevent recovery. + // Feed 3 rounds of overflow-inducing data. + for (int round = 0; round < 3; round++) { + std::vector overflow_data = {0xAA, 0xFF, 0x03, 0x00}; + for (int i = 0; i < 37; i++) { + overflow_data.push_back(0x22); + } + this->ld2450_.feed(overflow_data); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0) << "Overflow round " << round; + } + + // Parser should still recover and process a valid frame + auto frame = make_periodic_frame(); + this->ld2450_.feed(frame); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); +} + +TEST_F(LD2450ReadlineTest, SimulatedRestartGarbageThenFrames) { + // Simulate LD2450 restart: burst of garbage bytes (partial frames, noise) + // followed by normal periodic data. + // Partial periodic frame (as if we started reading mid-frame), a stale footer, and more garbage + std::vector restart_noise = { + 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, // mid-frame data + 0x55, 0xCC, // stale footer bytes + 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, // more garbage + }; + + this->ld2450_.feed(restart_noise); + // All garbage should be discarded + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); + + // Now the LD2450 starts sending valid frames + auto frame = make_periodic_frame(); + this->ld2450_.feed(frame); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); + + this->ld2450_.feed(frame); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); +} + +} // namespace esphome::ld2450::testing From 49afe53a2cfae55feb304695ee1735a5f0e5bc43 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 19 Feb 2026 22:04:38 -0500 Subject: [PATCH 142/197] [ld2410] Add frame header synchronization to readline_() (#14136) Co-authored-by: Claude Opus 4.6 --- esphome/components/ld2410/ld2410.cpp | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/esphome/components/ld2410/ld2410.cpp b/esphome/components/ld2410/ld2410.cpp index 95a04f768a..a3c2193d67 100644 --- a/esphome/components/ld2410/ld2410.cpp +++ b/esphome/components/ld2410/ld2410.cpp @@ -601,15 +601,33 @@ void LD2410Component::readline_(int readch) { return; // No data available } + // Frame header synchronization: verify first 4 bytes match a known frame header. + // This prevents the parser from getting stuck in an overflow loop after losing sync + // (e.g. after module restart or UART noise). + if (this->buffer_pos_ < HEADER_FOOTER_SIZE) { + const uint8_t byte = static_cast(readch); + // Verify header bytes match the frame type established by byte 0 + if (this->buffer_pos_ > 0) { + const uint8_t *expected = (this->buffer_data_[0] == DATA_FRAME_HEADER[0]) ? DATA_FRAME_HEADER : CMD_FRAME_HEADER; + if (byte != expected[this->buffer_pos_]) { + this->buffer_pos_ = 0; // Reset and fall through to check if this byte starts a new frame + } + } + // First byte must match start of a data or command frame header + if (this->buffer_pos_ == 0 && byte != DATA_FRAME_HEADER[0] && byte != CMD_FRAME_HEADER[0]) { + return; + } + } + if (this->buffer_pos_ < MAX_LINE_LENGTH - 1) { this->buffer_data_[this->buffer_pos_++] = readch; this->buffer_data_[this->buffer_pos_] = 0; } else { - // We should never get here, but just in case... ESP_LOGW(TAG, "Max command length exceeded; ignoring"); this->buffer_pos_ = 0; + return; } - if (this->buffer_pos_ < 4) { + if (this->buffer_pos_ < HEADER_FOOTER_SIZE) { return; // Not enough data to process yet } if (ld2410::validate_header_footer(DATA_FRAME_FOOTER, &this->buffer_data_[this->buffer_pos_ - 4])) { From 4c8e0575f94fb660bba85403347b999a424abe92 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 19 Feb 2026 22:36:12 -0500 Subject: [PATCH 143/197] [ld2420] Increase MAX_LINE_LENGTH to allow footer-based resync (#14137) Co-authored-by: Claude Opus 4.6 --- esphome/components/ld2420/ld2420.h | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/esphome/components/ld2420/ld2420.h b/esphome/components/ld2420/ld2420.h index 02250c5911..358793fe64 100644 --- a/esphome/components/ld2420/ld2420.h +++ b/esphome/components/ld2420/ld2420.h @@ -21,7 +21,9 @@ namespace esphome::ld2420 { static constexpr uint8_t CALIBRATE_SAMPLES = 64; -static constexpr uint8_t MAX_LINE_LENGTH = 46; // Max characters for serial buffer +// Energy frame is 45 bytes; +1 for null terminator, +4 so that a frame footer always lands +// inside the buffer during footer-based resynchronization after losing sync. +static constexpr uint8_t MAX_LINE_LENGTH = 50; static constexpr uint8_t TOTAL_GATES = 16; enum OpMode : uint8_t { From 28d510191c150ac3163bfdc80f23738c2cddb908 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 19 Feb 2026 23:25:25 -0500 Subject: [PATCH 144/197] [ld2410/ld2450] Replace header sync with buffer size increase for frame resync (#14138) Co-authored-by: Claude Opus 4.6 --- esphome/components/ld2410/ld2410.cpp | 19 +-- esphome/components/ld2410/ld2410.h | 6 +- esphome/components/ld2450/ld2450.cpp | 19 +-- esphome/components/ld2450/ld2450.h | 8 +- tests/components/ld2450/ld2450_readline.cpp | 158 ++++++++------------ 5 files changed, 72 insertions(+), 138 deletions(-) diff --git a/esphome/components/ld2410/ld2410.cpp b/esphome/components/ld2410/ld2410.cpp index a3c2193d67..f8f782f804 100644 --- a/esphome/components/ld2410/ld2410.cpp +++ b/esphome/components/ld2410/ld2410.cpp @@ -601,28 +601,11 @@ void LD2410Component::readline_(int readch) { return; // No data available } - // Frame header synchronization: verify first 4 bytes match a known frame header. - // This prevents the parser from getting stuck in an overflow loop after losing sync - // (e.g. after module restart or UART noise). - if (this->buffer_pos_ < HEADER_FOOTER_SIZE) { - const uint8_t byte = static_cast(readch); - // Verify header bytes match the frame type established by byte 0 - if (this->buffer_pos_ > 0) { - const uint8_t *expected = (this->buffer_data_[0] == DATA_FRAME_HEADER[0]) ? DATA_FRAME_HEADER : CMD_FRAME_HEADER; - if (byte != expected[this->buffer_pos_]) { - this->buffer_pos_ = 0; // Reset and fall through to check if this byte starts a new frame - } - } - // First byte must match start of a data or command frame header - if (this->buffer_pos_ == 0 && byte != DATA_FRAME_HEADER[0] && byte != CMD_FRAME_HEADER[0]) { - return; - } - } - if (this->buffer_pos_ < MAX_LINE_LENGTH - 1) { this->buffer_data_[this->buffer_pos_++] = readch; this->buffer_data_[this->buffer_pos_] = 0; } else { + // We should never get here, but just in case... ESP_LOGW(TAG, "Max command length exceeded; ignoring"); this->buffer_pos_ = 0; return; diff --git a/esphome/components/ld2410/ld2410.h b/esphome/components/ld2410/ld2410.h index efe585fb76..687ed21d1d 100644 --- a/esphome/components/ld2410/ld2410.h +++ b/esphome/components/ld2410/ld2410.h @@ -33,8 +33,10 @@ namespace esphome::ld2410 { using namespace ld24xx; -static constexpr uint8_t MAX_LINE_LENGTH = 46; // Max characters for serial buffer -static constexpr uint8_t TOTAL_GATES = 9; // Total number of gates supported by the LD2410 +// Engineering data frame is 45 bytes; +1 for null terminator, +4 so that a frame footer always +// lands inside the buffer during footer-based resynchronization after losing sync. +static constexpr uint8_t MAX_LINE_LENGTH = 50; +static constexpr uint8_t TOTAL_GATES = 9; // Total number of gates supported by the LD2410 class LD2410Component : public Component, public uart::UARTDevice { #ifdef USE_BINARY_SENSOR diff --git a/esphome/components/ld2450/ld2450.cpp b/esphome/components/ld2450/ld2450.cpp index 2af45235a3..d30c164769 100644 --- a/esphome/components/ld2450/ld2450.cpp +++ b/esphome/components/ld2450/ld2450.cpp @@ -769,28 +769,11 @@ void LD2450Component::readline_(int readch) { return; // No data available } - // Frame header synchronization: verify first 4 bytes match a known frame header. - // This prevents the parser from accumulating mid-frame data after losing sync - // (e.g. after module restart or UART noise). - if (this->buffer_pos_ < HEADER_FOOTER_SIZE) { - const uint8_t byte = static_cast(readch); - // Verify header bytes match the frame type established by byte 0 - if (this->buffer_pos_ > 0) { - const uint8_t *expected = (this->buffer_data_[0] == DATA_FRAME_HEADER[0]) ? DATA_FRAME_HEADER : CMD_FRAME_HEADER; - if (byte != expected[this->buffer_pos_]) { - this->buffer_pos_ = 0; // Reset and fall through to check if this byte starts a new frame - } - } - // First byte must match start of a data or command frame header - if (this->buffer_pos_ == 0 && byte != DATA_FRAME_HEADER[0] && byte != CMD_FRAME_HEADER[0]) { - return; - } - } - if (this->buffer_pos_ < MAX_LINE_LENGTH - 1) { this->buffer_data_[this->buffer_pos_++] = readch; this->buffer_data_[this->buffer_pos_] = 0; } else { + // We should never get here, but just in case... ESP_LOGW(TAG, "Max command length exceeded; ignoring"); this->buffer_pos_ = 0; return; diff --git a/esphome/components/ld2450/ld2450.h b/esphome/components/ld2450/ld2450.h index 44e5912b2a..30f96c0a9c 100644 --- a/esphome/components/ld2450/ld2450.h +++ b/esphome/components/ld2450/ld2450.h @@ -38,9 +38,11 @@ using namespace ld24xx; // Constants static constexpr uint8_t DEFAULT_PRESENCE_TIMEOUT = 5; // Timeout to reset presense status 5 sec. -static constexpr uint8_t MAX_LINE_LENGTH = 41; // Max characters for serial buffer -static constexpr uint8_t MAX_TARGETS = 3; // Max 3 Targets in LD2450 -static constexpr uint8_t MAX_ZONES = 3; // Max 3 Zones in LD2450 +// Zone query response is 40 bytes; +1 for null terminator, +4 so that a frame footer always +// lands inside the buffer during footer-based resynchronization after losing sync. +static constexpr uint8_t MAX_LINE_LENGTH = 45; +static constexpr uint8_t MAX_TARGETS = 3; // Max 3 Targets in LD2450 +static constexpr uint8_t MAX_ZONES = 3; // Max 3 Zones in LD2450 enum Direction : uint8_t { DIRECTION_APPROACHING = 0, diff --git a/tests/components/ld2450/ld2450_readline.cpp b/tests/components/ld2450/ld2450_readline.cpp index 68b1dd6881..cb97f633bf 100644 --- a/tests/components/ld2450/ld2450_readline.cpp +++ b/tests/components/ld2450/ld2450_readline.cpp @@ -48,19 +48,39 @@ TEST_F(LD2450ReadlineTest, BackToBackMixedFrames) { EXPECT_EQ(this->ld2450_.buffer_pos_, 0); } -// --- Garbage rejection tests --- - -TEST_F(LD2450ReadlineTest, GarbageDiscarded) { - // Feed bytes that don't match any header start byte - std::vector garbage = {0x01, 0x02, 0x03, 0x42, 0x99, 0x00, 0xFF, 0x7F}; - this->ld2450_.feed(garbage); - // Header sync should discard all of these - EXPECT_EQ(this->ld2450_.buffer_pos_, 0); -} +// --- Garbage then valid frame tests --- TEST_F(LD2450ReadlineTest, GarbageThenValidFrame) { + // Garbage bytes accumulate in the buffer but don't match any footer. + // A valid frame follows; its footer resets the buffer and resyncs. std::vector garbage = {0x01, 0x02, 0x03, 0x42, 0x99}; this->ld2450_.feed(garbage); + EXPECT_GT(this->ld2450_.buffer_pos_, 0); // Garbage accumulated + + auto frame = make_periodic_frame(); + this->ld2450_.feed(frame); + // Footer from the valid frame resyncs the parser + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); +} + +// --- Footer-based resynchronization tests --- + +TEST_F(LD2450ReadlineTest, FooterInGarbageResyncs) { + // Garbage containing a periodic frame footer (0x55 0xCC) triggers + // a buffer reset, allowing the next frame to be parsed cleanly. + std::vector garbage_with_footer = {0x01, 0x02, 0x03, 0x04, 0x55, 0xCC}; + this->ld2450_.feed(garbage_with_footer); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); // Footer reset the buffer + + auto frame = make_periodic_frame(); + this->ld2450_.feed(frame); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); +} + +TEST_F(LD2450ReadlineTest, CmdFooterInGarbageResyncs) { + // Garbage containing a command frame footer (04 03 02 01) also resyncs. + std::vector garbage_with_footer = {0x10, 0x20, 0x30, 0x40, 0x04, 0x03, 0x02, 0x01}; + this->ld2450_.feed(garbage_with_footer); EXPECT_EQ(this->ld2450_.buffer_pos_, 0); auto frame = make_periodic_frame(); @@ -68,112 +88,56 @@ TEST_F(LD2450ReadlineTest, GarbageThenValidFrame) { EXPECT_EQ(this->ld2450_.buffer_pos_, 0); } -// --- Header synchronization tests --- +// --- Overflow recovery tests --- -TEST_F(LD2450ReadlineTest, PartialDataHeaderThenMismatch) { - // Start of a data frame header, then invalid byte - this->ld2450_.feed({0xAA, 0xFF, 0x42}); // 0x42 doesn't match DATA_FRAME_HEADER[2] (0x03) - // Parser should have reset - EXPECT_EQ(this->ld2450_.buffer_pos_, 0); -} - -TEST_F(LD2450ReadlineTest, PartialCmdHeaderThenMismatch) { - // Start of a command frame header, then invalid byte - this->ld2450_.feed({0xFD, 0xFC, 0xFB, 0x42}); // 0x42 doesn't match CMD_FRAME_HEADER[3] (0xFA) - EXPECT_EQ(this->ld2450_.buffer_pos_, 0); -} - -TEST_F(LD2450ReadlineTest, PartialHeaderThenValidFrame) { - // Partial header that fails, then a complete valid frame - this->ld2450_.feed({0xAA, 0xFF, 0x42}); // Fails at byte 3 - EXPECT_EQ(this->ld2450_.buffer_pos_, 0); - - auto frame = make_periodic_frame(); - this->ld2450_.feed(frame); - EXPECT_EQ(this->ld2450_.buffer_pos_, 0); -} - -TEST_F(LD2450ReadlineTest, HeaderMismatchRecoveryOnNewHeaderByte) { - // Start data header, mismatch at byte 2, but mismatch byte is start of command header - this->ld2450_.feed({0xAA, 0xFF}); - EXPECT_EQ(this->ld2450_.buffer_pos_, 2); // Accumulating header - - this->ld2450_.feed({0xFD}); // Doesn't match DATA_FRAME_HEADER[2]=0x03, but IS CMD_FRAME_HEADER[0] - // Parser should reset and start new frame with 0xFD - EXPECT_EQ(this->ld2450_.buffer_pos_, 1); - EXPECT_EQ(this->ld2450_.buffer_data_[0], 0xFD); -} - -// --- Mid-frame / overflow recovery tests --- - -TEST_F(LD2450ReadlineTest, MidFrameDataRecovery) { - // Simulate starting mid-frame: feed the tail end of a periodic frame (no valid header) - // These bytes would be part of target data in a real frame - std::vector mid_frame = {0x10, 0x20, 0x30, 0x40, 0x55, 0xCC}; - this->ld2450_.feed(mid_frame); - // All discarded (none match header start bytes) - EXPECT_EQ(this->ld2450_.buffer_pos_, 0); - - // Now feed a valid frame - auto frame = make_periodic_frame(); - this->ld2450_.feed(frame); - EXPECT_EQ(this->ld2450_.buffer_pos_, 0); -} - -TEST_F(LD2450ReadlineTest, OverflowRecovery) { - // Feed a valid data frame header followed by enough filler to cause overflow. - // Header (4) + 36 filler = 40 bytes in buffer. The 41st byte triggers overflow. - std::vector overflow_data = {0xAA, 0xFF, 0x03, 0x00}; // Valid header - for (int i = 0; i < 37; i++) { - overflow_data.push_back(0x11); // Filler that won't match any footer - } - // 41 bytes total: 40 stored, 41st triggers overflow and resets buffer_pos_ to 0 +TEST_F(LD2450ReadlineTest, OverflowResetsBuffer) { + // Fill the buffer to capacity with filler that won't match any footer. + // MAX_LINE_LENGTH is 45, usable is 44. The 45th byte triggers overflow. + std::vector overflow_data(MAX_LINE_LENGTH, 0x11); + this->ld2450_.feed(overflow_data); + // After overflow, buffer_pos_ resets to 0 (via the < 4 early return path) + EXPECT_LT(this->ld2450_.buffer_pos_, 4); +} + +TEST_F(LD2450ReadlineTest, OverflowThenValidFrame) { + // Overflow, then a valid frame should be processed. + std::vector overflow_data(MAX_LINE_LENGTH, 0x11); this->ld2450_.feed(overflow_data); - EXPECT_EQ(this->ld2450_.buffer_pos_, 0); - // Feed a valid frame and verify recovery auto frame = make_periodic_frame(); this->ld2450_.feed(frame); EXPECT_EQ(this->ld2450_.buffer_pos_, 0); } -TEST_F(LD2450ReadlineTest, RepeatedOverflowDoesNotLoop) { - // Simulate the bug scenario: repeated overflows should not prevent recovery. - // Feed 3 rounds of overflow-inducing data. - for (int round = 0; round < 3; round++) { - std::vector overflow_data = {0xAA, 0xFF, 0x03, 0x00}; - for (int i = 0; i < 37; i++) { - overflow_data.push_back(0x22); - } - this->ld2450_.feed(overflow_data); - EXPECT_EQ(this->ld2450_.buffer_pos_, 0) << "Overflow round " << round; - } - - // Parser should still recover and process a valid frame +TEST_F(LD2450ReadlineTest, BufferLargeEnoughForDesyncedFooter) { + // The key fix: the buffer (45) is large enough that a desynced periodic frame's + // footer (at most 30 bytes into the stream) will land inside the buffer before overflow. + // Simulate starting 10 bytes into a periodic frame, then a full frame follows. + std::vector mid_frame = {0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39}; + // Then a complete periodic frame whose footer will land at position 40 (10 + 30), + // well within the buffer size of 45. auto frame = make_periodic_frame(); - this->ld2450_.feed(frame); + mid_frame.insert(mid_frame.end(), frame.begin(), frame.end()); + + this->ld2450_.feed(mid_frame); + // The footer from the frame should have triggered a reset EXPECT_EQ(this->ld2450_.buffer_pos_, 0); } -TEST_F(LD2450ReadlineTest, SimulatedRestartGarbageThenFrames) { - // Simulate LD2450 restart: burst of garbage bytes (partial frames, noise) - // followed by normal periodic data. - // Partial periodic frame (as if we started reading mid-frame), a stale footer, and more garbage +TEST_F(LD2450ReadlineTest, SimulatedRestartThenFrames) { + // Simulate LD2450 restart: burst of garbage followed by valid periodic frames. + // The garbage + first frame should fit in the buffer so the footer resyncs. std::vector restart_noise = { - 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, // mid-frame data - 0x55, 0xCC, // stale footer bytes - 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, // more garbage + 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, // 8 bytes of mid-frame data }; + auto frame = make_periodic_frame(); + // 8 garbage + 30 frame = 38 bytes, well within buffer of 45 + restart_noise.insert(restart_noise.end(), frame.begin(), frame.end()); this->ld2450_.feed(restart_noise); - // All garbage should be discarded - EXPECT_EQ(this->ld2450_.buffer_pos_, 0); - - // Now the LD2450 starts sending valid frames - auto frame = make_periodic_frame(); - this->ld2450_.feed(frame); EXPECT_EQ(this->ld2450_.buffer_pos_, 0); + // Subsequent frames should work normally this->ld2450_.feed(frame); EXPECT_EQ(this->ld2450_.buffer_pos_, 0); } From 8aaf0b8d8546471d5733b7e565a29f2be2d4582e Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 20 Feb 2026 10:17:12 -0500 Subject: [PATCH 145/197] Bump version to 2026.2.1 --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index 38135f9106..d41a79b0dc 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 = 2026.2.0 +PROJECT_NUMBER = 2026.2.1 # 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 9115055e7b..b3c15b1e27 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2026.2.0" +__version__ = "2026.2.1" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From b0a35559b3aa905a46b662b5834689a45b840e33 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 20 Feb 2026 10:19:01 -0600 Subject: [PATCH 146/197] [esp32] Bump ESP-IDF to 5.5.3.1, revert GATTS workaround (#14147) --- .clang-tidy.hash | 2 +- esphome/components/esp32/__init__.py | 25 +++++++++++++++--------- esphome/components/esp32_ble/__init__.py | 15 ++++++-------- platformio.ini | 4 ++-- 4 files changed, 25 insertions(+), 21 deletions(-) diff --git a/.clang-tidy.hash b/.clang-tidy.hash index df584fa716..777c846371 100644 --- a/.clang-tidy.hash +++ b/.clang-tidy.hash @@ -1 +1 @@ -3258307fa645ba77307e502075c02c4d710e92c48250839db3526d36a9655444 +5eb1e5852765114ad06533220d3160b6c23f5ccefc4de41828699de5dfff5ad6 diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index cdde1c4ed5..b6682100f7 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -587,16 +587,22 @@ def _format_framework_arduino_version(ver: cv.Version) -> str: return f"{ARDUINO_FRAMEWORK_PKG}@https://github.com/espressif/arduino-esp32/releases/download/{ver}/{filename}" -def _format_framework_espidf_version(ver: cv.Version, release: str) -> str: +def _format_framework_espidf_version( + ver: cv.Version, release: str | None = None +) -> str: # format the given espidf (https://github.com/pioarduino/esp-idf/releases) version to # a PIO platformio/framework-espidf value if ver == cv.Version(5, 4, 3) or ver >= cv.Version(5, 5, 1): ext = "tar.xz" else: ext = "zip" + # Build version string with dot-separated extra (e.g., "5.5.3.1" not "5.5.3-1") + ver_str = f"{ver.major}.{ver.minor}.{ver.patch}" + if ver.extra: + ver_str += f".{ver.extra}" if release: - return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{str(ver)}.{release}/esp-idf-v{str(ver)}.{ext}" - return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{str(ver)}/esp-idf-v{str(ver)}.{ext}" + return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{ver_str}.{release}/esp-idf-v{ver_str}.{ext}" + return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{ver_str}/esp-idf-v{ver_str}.{ext}" def _is_framework_url(source: str) -> bool: @@ -643,7 +649,7 @@ ARDUINO_PLATFORM_VERSION_LOOKUP = { # These versions correspond to pioarduino/esp-idf releases # See: https://github.com/pioarduino/esp-idf/releases ARDUINO_IDF_VERSION_LOOKUP = { - cv.Version(3, 3, 7): cv.Version(5, 5, 3), + cv.Version(3, 3, 7): cv.Version(5, 5, 3, "1"), cv.Version(3, 3, 6): cv.Version(5, 5, 2), cv.Version(3, 3, 5): cv.Version(5, 5, 2), cv.Version(3, 3, 4): cv.Version(5, 5, 1), @@ -662,11 +668,12 @@ ARDUINO_IDF_VERSION_LOOKUP = { # The default/recommended esp-idf framework version # - https://github.com/espressif/esp-idf/releases ESP_IDF_FRAMEWORK_VERSION_LOOKUP = { - "recommended": cv.Version(5, 5, 3), - "latest": cv.Version(5, 5, 3), - "dev": cv.Version(5, 5, 3), + "recommended": cv.Version(5, 5, 3, "1"), + "latest": cv.Version(5, 5, 3, "1"), + "dev": cv.Version(5, 5, 3, "1"), } ESP_IDF_PLATFORM_VERSION_LOOKUP = { + cv.Version(5, 5, 3, "1"): cv.Version(55, 3, 37), cv.Version(5, 5, 3): cv.Version(55, 3, 37), cv.Version(5, 5, 2): cv.Version(55, 3, 37), cv.Version(5, 5, 1): cv.Version(55, 3, 31, "2"), @@ -730,7 +737,7 @@ def _check_versions(config): platform_lookup = ESP_IDF_PLATFORM_VERSION_LOOKUP.get(version) value[CONF_SOURCE] = value.get( CONF_SOURCE, - _format_framework_espidf_version(version, value.get(CONF_RELEASE, None)), + _format_framework_espidf_version(version, value.get(CONF_RELEASE)), ) if _is_framework_url(value[CONF_SOURCE]): value[CONF_SOURCE] = f"pioarduino/framework-espidf@{value[CONF_SOURCE]}" @@ -1428,7 +1435,7 @@ async def to_code(config): if (idf_ver := ARDUINO_IDF_VERSION_LOOKUP.get(framework_ver)) is not None: cg.add_platformio_option( "platform_packages", - [_format_framework_espidf_version(idf_ver, None)], + [_format_framework_espidf_version(idf_ver)], ) # Use stub package to skip downloading precompiled libs stubs_dir = CORE.relative_build_path("arduino_libs_stub") diff --git a/esphome/components/esp32_ble/__init__.py b/esphome/components/esp32_ble/__init__.py index 80fcf051b9..c0e2f78bde 100644 --- a/esphome/components/esp32_ble/__init__.py +++ b/esphome/components/esp32_ble/__init__.py @@ -418,16 +418,13 @@ def final_validation(config): "esp32_ble_tracker" in full_config or "esp32_ble_client" in full_config ) - # Always enable GATTS: ESP-IDF 5.5.2.260206 has a bug in gatt_main.c where a - # GATT_TRACE_DEBUG references 'msg_len' outside the GATTS_INCLUDED/GATTC_INCLUDED - # guard, causing a compile error when both are disabled. - # Additionally, when GATT Client is enabled, GATT Server must also be enabled - # as an internal dependency in the Bluedroid stack. + # Check if BLE Server is needed + has_ble_server = "esp32_ble_server" in full_config + + # ESP-IDF BLE stack requires GATT Server to be enabled when GATT Client is enabled + # This is an internal dependency in the Bluedroid stack # See: https://github.com/espressif/esp-idf/issues/17724 - # TODO: Revert to conditional once the gatt_main.c bug is fixed upstream: - # has_ble_server = "esp32_ble_server" in full_config - # add_idf_sdkconfig_option("CONFIG_BT_GATTS_ENABLE", has_ble_server or has_ble_client) - add_idf_sdkconfig_option("CONFIG_BT_GATTS_ENABLE", True) + add_idf_sdkconfig_option("CONFIG_BT_GATTS_ENABLE", has_ble_server or has_ble_client) add_idf_sdkconfig_option("CONFIG_BT_GATTC_ENABLE", has_ble_client) # Handle max_connections: check for deprecated location in esp32_ble_tracker diff --git a/platformio.ini b/platformio.ini index fdd6a36428..e35dce2228 100644 --- a/platformio.ini +++ b/platformio.ini @@ -136,7 +136,7 @@ extends = common:arduino platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.37/platform-espressif32.zip platform_packages = pioarduino/framework-arduinoespressif32@https://github.com/espressif/arduino-esp32/releases/download/3.3.7/esp32-core-3.3.7.tar.xz - pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.5.3/esp-idf-v5.5.3.tar.xz + pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.5.3.1/esp-idf-v5.5.3.1.tar.xz framework = arduino, espidf ; Arduino as an ESP-IDF component lib_deps = @@ -171,7 +171,7 @@ extra_scripts = post:esphome/components/esp32/post_build.py.script extends = common:idf platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.37/platform-espressif32.zip platform_packages = - pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.5.3/esp-idf-v5.5.3.tar.xz + pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.5.3.1/esp-idf-v5.5.3.1.tar.xz framework = espidf lib_deps = From 9ce01fc369c5451e1f980e49cea08e51b6a0cc53 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 20 Feb 2026 11:20:05 -0500 Subject: [PATCH 147/197] [esp32] Add engineering_sample option for ESP32-P4 (#14139) Co-authored-by: Claude Opus 4.6 --- esphome/components/esp32/__init__.py | 43 +++++++++++++++++-- esphome/components/esp32/boards.py | 5 ++- script/generate-esp32-boards.py | 15 ++++++- tests/components/esp32/test.esp32-p4-idf.yaml | 1 + 4 files changed, 58 insertions(+), 6 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index b6682100f7..06677006ea 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -87,6 +87,7 @@ IS_TARGET_PLATFORM = True CONF_ASSERTION_LEVEL = "assertion_level" CONF_COMPILER_OPTIMIZATION = "compiler_optimization" CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES = "enable_idf_experimental_features" +CONF_ENGINEERING_SAMPLE = "engineering_sample" CONF_INCLUDE_BUILTIN_IDF_COMPONENTS = "include_builtin_idf_components" CONF_ENABLE_LWIP_ASSERT = "enable_lwip_assert" CONF_ENABLE_OTA_ROLLBACK = "enable_ota_rollback" @@ -785,6 +786,15 @@ def _detect_variant(value): # variant has already been validated against the known set value = value.copy() value[CONF_BOARD] = STANDARD_BOARDS[variant] + if variant == VARIANT_ESP32P4: + engineering_sample = value.get(CONF_ENGINEERING_SAMPLE) + if engineering_sample is None: + _LOGGER.warning( + "No board specified for ESP32-P4. Defaulting to production silicon (rev3). " + "If you have an early engineering sample (pre-rev3), set 'engineering_sample: true'." + ) + elif engineering_sample: + value[CONF_BOARD] = "esp32-p4-evboard" elif board in BOARDS: variant = variant or BOARDS[board][KEY_VARIANT] if variant != BOARDS[board][KEY_VARIANT]: @@ -848,6 +858,30 @@ def final_validate(config): path=[CONF_FRAMEWORK, CONF_ADVANCED, CONF_MINIMUM_CHIP_REVISION], ) ) + if ( + config[CONF_VARIANT] != VARIANT_ESP32P4 + and config.get(CONF_ENGINEERING_SAMPLE) is not None + ): + errs.append( + cv.Invalid( + f"'{CONF_ENGINEERING_SAMPLE}' is only supported on {VARIANT_ESP32P4}", + path=[CONF_ENGINEERING_SAMPLE], + ) + ) + if ( + config[CONF_VARIANT] == VARIANT_ESP32P4 + and config.get(CONF_ENGINEERING_SAMPLE) is not None + ): + board_is_es = BOARDS.get(config[CONF_BOARD], {}).get( + "engineering_sample", False + ) + if config[CONF_ENGINEERING_SAMPLE] != board_is_es: + errs.append( + cv.Invalid( + f"'{CONF_ENGINEERING_SAMPLE}' does not match board '{config[CONF_BOARD]}'", + path=[CONF_ENGINEERING_SAMPLE], + ) + ) if advanced[CONF_EXECUTE_FROM_PSRAM]: if config[CONF_VARIANT] != VARIANT_ESP32S3: errs.append( @@ -1197,6 +1231,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_CPU_FREQUENCY): cv.one_of( *FULL_CPU_FREQUENCIES, upper=True ), + cv.Optional(CONF_ENGINEERING_SAMPLE): cv.boolean, cv.Optional(CONF_FLASH_SIZE, default="4MB"): cv.one_of( *FLASH_SIZES, upper=True ), @@ -1482,10 +1517,12 @@ async def to_code(config): # ESP32-P4: ESP-IDF 5.5.3 changed the default of ESP32P4_SELECTS_REV_LESS_V3 # from y to n. PlatformIO uses sections.ld.in (for rev <3) or # sections.rev3.ld.in (for rev >=3) based on board definition. - # Set the sdkconfig option to match the board's revision. + # Set the sdkconfig option to match the board's chip revision. if variant == VARIANT_ESP32P4: - is_rev3 = "_r3" in config[CONF_BOARD] - add_idf_sdkconfig_option("CONFIG_ESP32P4_SELECTS_REV_LESS_V3", not is_rev3) + is_eng_sample = BOARDS.get(config[CONF_BOARD], {}).get( + "engineering_sample", False + ) + add_idf_sdkconfig_option("CONFIG_ESP32P4_SELECTS_REV_LESS_V3", is_eng_sample) # Set minimum chip revision for ESP32 variant # Setting this to 3.0 or higher reduces flash size by excluding workaround code, diff --git a/esphome/components/esp32/boards.py b/esphome/components/esp32/boards.py index 66367d63ae..2bd08e7c39 100644 --- a/esphome/components/esp32/boards.py +++ b/esphome/components/esp32/boards.py @@ -20,7 +20,7 @@ STANDARD_BOARDS = { VARIANT_ESP32C6: "esp32-c6-devkitm-1", VARIANT_ESP32C61: "esp32-c61-devkitc1-n8r2", VARIANT_ESP32H2: "esp32-h2-devkitm-1", - VARIANT_ESP32P4: "esp32-p4-evboard", + VARIANT_ESP32P4: "esp32-p4_r3-evboard", VARIANT_ESP32S2: "esp32-s2-kaluga-1", VARIANT_ESP32S3: "esp32-s3-devkitc-1", } @@ -1713,10 +1713,12 @@ BOARDS = { "esp32-p4": { "name": "Espressif ESP32-P4 ES (pre rev.300) generic", "variant": VARIANT_ESP32P4, + "engineering_sample": True, }, "esp32-p4-evboard": { "name": "Espressif ESP32-P4 Function EV Board (ES pre rev.300)", "variant": VARIANT_ESP32P4, + "engineering_sample": True, }, "esp32-p4_r3": { "name": "Espressif ESP32-P4 rev.300 generic", @@ -2141,6 +2143,7 @@ BOARDS = { "m5stack-tab5-p4": { "name": "M5STACK Tab5 esp32-p4 Board (ES pre rev.300)", "variant": VARIANT_ESP32P4, + "engineering_sample": True, }, "m5stack-timer-cam": { "name": "M5Stack Timer CAM", diff --git a/script/generate-esp32-boards.py b/script/generate-esp32-boards.py index 81b78b04be..ab4a38ced5 100755 --- a/script/generate-esp32-boards.py +++ b/script/generate-esp32-boards.py @@ -43,10 +43,14 @@ def get_boards(): name = board_info["name"] board = fname.stem variant = mcu.upper() - boards[board] = { + chip_variant = board_info["build"].get("chip_variant", "") + entry = { "name": name, "variant": f"VARIANT_{variant}", } + if chip_variant.endswith("_es"): + entry["engineering_sample"] = True + boards[board] = entry return boards @@ -55,6 +59,12 @@ TEMPLATE = """ "%s": { "variant": %s, },""" +TEMPLATE_ES = """ "%s": { + "name": "%s", + "variant": %s, + "engineering_sample": True, + },""" + def main(check: bool): boards = get_boards() @@ -66,7 +76,8 @@ def main(check: bool): if line == "BOARDS = {": parts.append(line) parts.extend( - TEMPLATE % (board, info["name"], info["variant"]) + (TEMPLATE_ES if info.get("engineering_sample") else TEMPLATE) + % (board, info["name"], info["variant"]) for board, info in sorted(boards.items()) ) parts.append("}") diff --git a/tests/components/esp32/test.esp32-p4-idf.yaml b/tests/components/esp32/test.esp32-p4-idf.yaml index bc054f5aee..fd42fac5a3 100644 --- a/tests/components/esp32/test.esp32-p4-idf.yaml +++ b/tests/components/esp32/test.esp32-p4-idf.yaml @@ -1,5 +1,6 @@ esp32: variant: esp32p4 + engineering_sample: true flash_size: 32MB cpu_frequency: 400MHz framework: From 403235e2d4ec6a27ac4ad35f79b7dc2afb413611 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 20 Feb 2026 11:20:29 -0500 Subject: [PATCH 148/197] [wifi] Add band_mode configuration for ESP32-C5 dual-band WiFi (#14148) Co-authored-by: Claude Opus 4.6 --- esphome/components/wifi/__init__.py | 22 ++++++++++++++++++- esphome/components/wifi/wifi_component.cpp | 16 ++++++++++++++ esphome/components/wifi/wifi_component.h | 13 +++++++++++ .../wifi/wifi_component_esp_idf.cpp | 7 ++++++ tests/components/wifi/test.esp32-c5-idf.yaml | 5 +++++ 5 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 tests/components/wifi/test.esp32-c5-idf.yaml diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index afceec6c54..540d0a0ab1 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -5,7 +5,12 @@ from esphome import automation from esphome.automation import Condition import esphome.codegen as cg from esphome.components.const import CONF_USE_PSRAM -from esphome.components.esp32 import add_idf_sdkconfig_option, const, get_esp32_variant +from esphome.components.esp32 import ( + add_idf_sdkconfig_option, + const, + get_esp32_variant, + only_on_variant, +) from esphome.components.network import ( has_high_performance_networking, ip_address_literal, @@ -64,6 +69,7 @@ _LOGGER = logging.getLogger(__name__) NO_WIFI_VARIANTS = [const.VARIANT_ESP32H2, const.VARIANT_ESP32P4] CONF_SAVE = "save" +CONF_BAND_MODE = "band_mode" CONF_MIN_AUTH_MODE = "min_auth_mode" CONF_POST_CONNECT_ROAMING = "post_connect_roaming" @@ -90,6 +96,13 @@ WIFI_POWER_SAVE_MODES = { "HIGH": WiFiPowerSaveMode.WIFI_POWER_SAVE_HIGH, } +WiFiBandMode = cg.global_ns.enum("wifi_band_mode_t") +WIFI_BAND_MODES = { + "AUTO": WiFiBandMode.WIFI_BAND_MODE_AUTO, + "2.4GHZ": WiFiBandMode.WIFI_BAND_MODE_2G_ONLY, + "5GHZ": WiFiBandMode.WIFI_BAND_MODE_5G_ONLY, +} + WifiMinAuthMode = wifi_ns.enum("WifiMinAuthMode") WIFI_MIN_AUTH_MODES = { "WPA": WifiMinAuthMode.WIFI_MIN_AUTH_MODE_WPA, @@ -353,6 +366,11 @@ CONFIG_SCHEMA = cv.All( cv.SplitDefault(CONF_ENABLE_RRM, esp32=False): cv.All( cv.boolean, cv.only_on_esp32 ), + cv.Optional(CONF_BAND_MODE): cv.All( + cv.enum(WIFI_BAND_MODES, upper=True), + cv.only_on_esp32, + only_on_variant(supported=[const.VARIANT_ESP32C5]), + ), cv.Optional(CONF_PASSIVE_SCAN, default=False): cv.boolean, cv.Optional(CONF_ENABLE_ON_BOOT, default=True): cv.boolean, cv.Optional(CONF_POST_CONNECT_ROAMING, default=True): cv.boolean, @@ -527,6 +545,8 @@ async def to_code(config): cg.add(var.set_btm(config[CONF_ENABLE_BTM])) if config[CONF_ENABLE_RRM]: cg.add(var.set_rrm(config[CONF_ENABLE_RRM])) + if CONF_BAND_MODE in config: + cg.add(var.set_band_mode(config[CONF_BAND_MODE])) if config.get(CONF_USE_PSRAM): add_idf_sdkconfig_option("CONFIG_SPIRAM_TRY_ALLOCATE_WIFI_LWIP", True) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index a2efac8d26..8b3060c7c3 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -1469,6 +1469,22 @@ void WiFiComponent::dump_config() { ESP_LOGCONFIG(TAG, " Disabled"); return; } +#if defined(USE_ESP32) && defined(SOC_WIFI_SUPPORT_5G) + const char *band_mode_s; + switch (this->band_mode_) { + case WIFI_BAND_MODE_2G_ONLY: + band_mode_s = "2.4GHz"; + break; + case WIFI_BAND_MODE_5G_ONLY: + band_mode_s = "5GHz"; + break; + case WIFI_BAND_MODE_AUTO: + default: + band_mode_s = "Auto"; + break; + } + ESP_LOGCONFIG(TAG, " Band Mode: %s", band_mode_s); +#endif if (this->is_connected()) { this->print_connect_params_(); } diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 4a038f602c..5f903e092a 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -45,6 +45,10 @@ extern "C" { #include #endif +#if defined(USE_ESP32) && defined(SOC_WIFI_SUPPORT_5G) +#include +#endif + #if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE) #include #include @@ -435,6 +439,9 @@ class WiFiComponent : public Component { void set_power_save_mode(WiFiPowerSaveMode power_save); void set_min_auth_mode(WifiMinAuthMode min_auth_mode) { min_auth_mode_ = min_auth_mode; } void set_output_power(float output_power) { output_power_ = output_power; } +#if defined(USE_ESP32) && defined(SOC_WIFI_SUPPORT_5G) + void set_band_mode(wifi_band_mode_t band_mode) { this->band_mode_ = band_mode; } +#endif void set_passive_scan(bool passive); @@ -652,6 +659,9 @@ class WiFiComponent : public Component { bool wifi_sta_pre_setup_(); bool wifi_apply_output_power_(float output_power); bool wifi_apply_power_save_(); +#if defined(USE_ESP32) && defined(SOC_WIFI_SUPPORT_5G) + bool wifi_apply_band_mode_(); +#endif bool wifi_sta_ip_config_(const optional &manual_ip); bool wifi_apply_hostname_(); bool wifi_sta_connect_(const WiFiAP &ap); @@ -774,6 +784,9 @@ class WiFiComponent : public Component { // 1-byte enums and integers WiFiComponentState state_{WIFI_COMPONENT_STATE_OFF}; WiFiPowerSaveMode power_save_{WIFI_POWER_SAVE_NONE}; +#if defined(USE_ESP32) && defined(SOC_WIFI_SUPPORT_5G) + wifi_band_mode_t band_mode_{WIFI_BAND_MODE_AUTO}; +#endif WifiMinAuthMode min_auth_mode_{WIFI_MIN_AUTH_MODE_WPA2}; WiFiRetryPhase retry_phase_{WiFiRetryPhase::INITIAL_CONNECT}; uint8_t num_retried_{0}; diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index 52ee482121..57bbceb1b8 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -292,6 +292,10 @@ bool WiFiComponent::wifi_apply_power_save_() { return success; } +#ifdef SOC_WIFI_SUPPORT_5G +bool WiFiComponent::wifi_apply_band_mode_() { return esp_wifi_set_band_mode(this->band_mode_) == ESP_OK; } +#endif + bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { // enable STA if (!this->wifi_mode_(true, {})) @@ -726,6 +730,9 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { s_sta_started = true; // re-apply power save mode wifi_apply_power_save_(); +#ifdef SOC_WIFI_SUPPORT_5G + wifi_apply_band_mode_(); +#endif } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_STA_STOP) { ESP_LOGV(TAG, "STA stop"); diff --git a/tests/components/wifi/test.esp32-c5-idf.yaml b/tests/components/wifi/test.esp32-c5-idf.yaml new file mode 100644 index 0000000000..92a52db09e --- /dev/null +++ b/tests/components/wifi/test.esp32-c5-idf.yaml @@ -0,0 +1,5 @@ +wifi: + band_mode: 5GHZ + +packages: + - !include common.yaml From 9c0eed8a67a392868d2a8e6c8a220314271c847e Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 20 Feb 2026 12:03:39 -0500 Subject: [PATCH 149/197] [e131] Remove dead LWIP TCP code path from loop() (#14155) Co-authored-by: Claude Opus 4.6 Co-authored-by: J. Nick Koston --- esphome/components/e131/e131.cpp | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/esphome/components/e131/e131.cpp b/esphome/components/e131/e131.cpp index d04ab8a58d..a7a695c167 100644 --- a/esphome/components/e131/e131.cpp +++ b/esphome/components/e131/e131.cpp @@ -85,22 +85,6 @@ void E131Component::loop() { ESP_LOGV(TAG, "Ignored packet for %d universe of size %d.", universe, packet.count); } } -#elif defined(USE_SOCKET_IMPL_LWIP_TCP) - while (auto packet_size = this->udp_.parsePacket()) { - auto len = this->udp_.read(buf, sizeof(buf)); - if (len <= 0) - continue; - - if (!this->packet_(buf, (size_t) len, universe, packet)) { - ESP_LOGV(TAG, "Invalid packet received of size %d.", (int) len); - continue; - } - - if (!this->process_(universe, packet)) { - ESP_LOGV(TAG, "Ignored packet for %d universe of size %d.", universe, packet.count); - } - } -#endif } void E131Component::add_effect(E131AddressableLightEffect *light_effect) { From b85a49cdb3c65c24ffdb725a56dc9f48c2fd7f3a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 20 Feb 2026 13:27:15 -0600 Subject: [PATCH 150/197] Bump github/codeql-action from 4.32.3 to 4.32.4 (#14161) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/codeql.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 376825bad6..5d7c32eaa9 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -58,7 +58,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@9e907b5e64f6b83e7804b09294d44122997950d6 # v4.32.3 + uses: github/codeql-action/init@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} @@ -86,6 +86,6 @@ jobs: exit 1 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@9e907b5e64f6b83e7804b09294d44122997950d6 # v4.32.3 + uses: github/codeql-action/analyze@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4 with: category: "/language:${{matrix.language}}" From 1a376328911b184cbc5b7251688607f7ff62384a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 20 Feb 2026 19:27:45 +0000 Subject: [PATCH 151/197] Bump pylint from 4.0.4 to 4.0.5 (#14160) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 9e99855f6f..611e552829 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,4 +1,4 @@ -pylint==4.0.4 +pylint==4.0.5 flake8==7.3.0 # also change in .pre-commit-config.yaml when updating ruff==0.15.1 # also change in .pre-commit-config.yaml when updating pyupgrade==3.21.2 # also change in .pre-commit-config.yaml when updating From edfc3e3501ec3b0652a7a341d5542f70c488b79c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 20 Feb 2026 19:32:41 +0000 Subject: [PATCH 152/197] Bump ruff from 0.15.1 to 0.15.2 (#14159) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston --- .pre-commit-config.yaml | 2 +- requirements_test.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6d89060b0d..07d02e0e3c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,7 +11,7 @@ ci: repos: - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.15.1 + rev: v0.15.2 hooks: # Run the linter. - id: ruff diff --git a/requirements_test.txt b/requirements_test.txt index 611e552829..3e5dc8a90c 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,6 +1,6 @@ pylint==4.0.5 flake8==7.3.0 # also change in .pre-commit-config.yaml when updating -ruff==0.15.1 # also change in .pre-commit-config.yaml when updating +ruff==0.15.2 # also change in .pre-commit-config.yaml when updating pyupgrade==3.21.2 # also change in .pre-commit-config.yaml when updating pre-commit From 48115eca18b8c9937595a0fe26ce50fde4ebb15f Mon Sep 17 00:00:00 2001 From: Pawelo <81100874+pgolawsk@users.noreply.github.com> Date: Fri, 20 Feb 2026 21:08:31 +0100 Subject: [PATCH 153/197] [safe_mode] Extract RTC_KEY constant for shared use (#14121) Co-authored-by: J. Nick Koston --- esphome/components/safe_mode/safe_mode.cpp | 2 +- esphome/components/safe_mode/safe_mode.h | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/esphome/components/safe_mode/safe_mode.cpp b/esphome/components/safe_mode/safe_mode.cpp index 6cae4bf9d5..bd80048c64 100644 --- a/esphome/components/safe_mode/safe_mode.cpp +++ b/esphome/components/safe_mode/safe_mode.cpp @@ -104,7 +104,7 @@ bool SafeModeComponent::should_enter_safe_mode(uint8_t num_attempts, uint32_t en this->safe_mode_enable_time_ = enable_time; this->safe_mode_boot_is_good_after_ = boot_is_good_after; this->safe_mode_num_attempts_ = num_attempts; - this->rtc_ = global_preferences->make_preference(233825507UL, false); + this->rtc_ = global_preferences->make_preference(RTC_KEY, false); #if defined(USE_ESP32) && defined(USE_OTA_ROLLBACK) // Check partition state to detect if bootloader supports rollback diff --git a/esphome/components/safe_mode/safe_mode.h b/esphome/components/safe_mode/safe_mode.h index d6f669f39f..a4d27c15da 100644 --- a/esphome/components/safe_mode/safe_mode.h +++ b/esphome/components/safe_mode/safe_mode.h @@ -11,6 +11,9 @@ namespace esphome::safe_mode { +/// RTC key for storing boot loop counter - used by safe_mode and preferences backends +constexpr uint32_t RTC_KEY = 233825507UL; + /// SafeModeComponent provides a safe way to recover from repeated boot failures class SafeModeComponent : public Component { public: From db6aa58f40b4c6be322f8c26312605bb2fca8dd6 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 20 Feb 2026 16:06:46 -0500 Subject: [PATCH 154/197] [max7219digit] Fix typo in action names (#14162) Co-authored-by: Claude Opus 4.6 --- esphome/components/max7219digit/display.py | 22 +++++++++++----------- tests/components/max7219digit/common.yaml | 14 +++++++------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/esphome/components/max7219digit/display.py b/esphome/components/max7219digit/display.py index e6d53efc5d..a251eaccea 100644 --- a/esphome/components/max7219digit/display.py +++ b/esphome/components/max7219digit/display.py @@ -133,12 +133,12 @@ MAX7219_ON_ACTION_SCHEMA = automation.maybe_simple_id( @automation.register_action( - "max7129digit.invert_off", DisplayInvertAction, MAX7219_OFF_ACTION_SCHEMA + "max7219digit.invert_off", DisplayInvertAction, MAX7219_OFF_ACTION_SCHEMA ) @automation.register_action( - "max7129digit.invert_on", DisplayInvertAction, MAX7219_ON_ACTION_SCHEMA + "max7219digit.invert_on", DisplayInvertAction, MAX7219_ON_ACTION_SCHEMA ) -async def max7129digit_invert_to_code(config, action_id, template_arg, args): +async def max7219digit_invert_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) cg.add(var.set_state(config[CONF_STATE])) @@ -146,12 +146,12 @@ async def max7129digit_invert_to_code(config, action_id, template_arg, args): @automation.register_action( - "max7129digit.turn_off", DisplayVisibilityAction, MAX7219_OFF_ACTION_SCHEMA + "max7219digit.turn_off", DisplayVisibilityAction, MAX7219_OFF_ACTION_SCHEMA ) @automation.register_action( - "max7129digit.turn_on", DisplayVisibilityAction, MAX7219_ON_ACTION_SCHEMA + "max7219digit.turn_on", DisplayVisibilityAction, MAX7219_ON_ACTION_SCHEMA ) -async def max7129digit_visible_to_code(config, action_id, template_arg, args): +async def max7219digit_visible_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) cg.add(var.set_state(config[CONF_STATE])) @@ -159,12 +159,12 @@ async def max7129digit_visible_to_code(config, action_id, template_arg, args): @automation.register_action( - "max7129digit.reverse_off", DisplayReverseAction, MAX7219_OFF_ACTION_SCHEMA + "max7219digit.reverse_off", DisplayReverseAction, MAX7219_OFF_ACTION_SCHEMA ) @automation.register_action( - "max7129digit.reverse_on", DisplayReverseAction, MAX7219_ON_ACTION_SCHEMA + "max7219digit.reverse_on", DisplayReverseAction, MAX7219_ON_ACTION_SCHEMA ) -async def max7129digit_reverse_to_code(config, action_id, template_arg, args): +async def max7219digit_reverse_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) cg.add(var.set_state(config[CONF_STATE])) @@ -183,9 +183,9 @@ MAX7219_INTENSITY_SCHEMA = cv.maybe_simple_value( @automation.register_action( - "max7129digit.intensity", DisplayIntensityAction, MAX7219_INTENSITY_SCHEMA + "max7219digit.intensity", DisplayIntensityAction, MAX7219_INTENSITY_SCHEMA ) -async def max7129digit_intensity_to_code(config, action_id, template_arg, args): +async def max7219digit_intensity_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) template_ = await cg.templatable(config[CONF_INTENSITY], args, cg.uint8) diff --git a/tests/components/max7219digit/common.yaml b/tests/components/max7219digit/common.yaml index 525b7b8d3e..4d2dbb781d 100644 --- a/tests/components/max7219digit/common.yaml +++ b/tests/components/max7219digit/common.yaml @@ -13,10 +13,10 @@ esphome: on_boot: - priority: 100 then: - - max7129digit.invert_off: - - max7129digit.invert_on: - - max7129digit.turn_on: - - max7129digit.turn_off: - - max7129digit.reverse_on: - - max7129digit.reverse_off: - - max7129digit.intensity: 10 + - max7219digit.invert_off: + - max7219digit.invert_on: + - max7219digit.turn_on: + - max7219digit.turn_off: + - max7219digit.reverse_on: + - max7219digit.reverse_off: + - max7219digit.intensity: 10 From 1d3054ef5e463f1b018e297ffe547c7e621ddfd4 Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Fri, 20 Feb 2026 23:12:50 +0100 Subject: [PATCH 155/197] [nrf52,logger] Early debug (#11685) --- esphome/components/debug/debug_zephyr.cpp | 45 +-------- esphome/components/logger/__init__.py | 24 +++++ esphome/components/logger/logger.cpp | 3 + esphome/components/logger/logger.h | 1 + esphome/components/logger/logger_zephyr.cpp | 97 +++++++++++++++++++ esphome/components/nrf52/__init__.py | 1 + esphome/components/zephyr/gpio.h | 6 +- esphome/components/zephyr/preferences.h | 6 +- esphome/components/zephyr/reset_reason.cpp | 63 ++++++++++++ esphome/components/zephyr/reset_reason.h | 18 ++++ esphome/config_validation.py | 2 + esphome/core/defines.h | 2 + .../logger/test.nrf52-adafruit.yaml | 2 + 13 files changed, 219 insertions(+), 51 deletions(-) create mode 100644 esphome/components/zephyr/reset_reason.cpp create mode 100644 esphome/components/zephyr/reset_reason.h diff --git a/esphome/components/debug/debug_zephyr.cpp b/esphome/components/debug/debug_zephyr.cpp index 0291cc3061..ecca7150bd 100644 --- a/esphome/components/debug/debug_zephyr.cpp +++ b/esphome/components/debug/debug_zephyr.cpp @@ -2,6 +2,7 @@ #ifdef USE_ZEPHYR #include #include "esphome/core/log.h" +#include #include #include #include @@ -15,16 +16,6 @@ static const char *const TAG = "debug"; constexpr std::uintptr_t MBR_PARAM_PAGE_ADDR = 0xFFC; constexpr std::uintptr_t MBR_BOOTLOADER_ADDR = 0xFF8; -static size_t append_reset_reason(char *buf, size_t size, size_t pos, bool set, const char *reason) { - if (!set) { - return pos; - } - if (pos > 0) { - pos = buf_append_printf(buf, size, pos, ", "); - } - return buf_append_printf(buf, size, pos, "%s", reason); -} - static inline uint32_t read_mem_u32(uintptr_t addr) { return *reinterpret_cast(addr); // NOLINT(performance-no-int-to-ptr) } @@ -57,39 +48,7 @@ static inline uint32_t sd_version_get() { } const char *DebugComponent::get_reset_reason_(std::span buffer) { - char *buf = buffer.data(); - const size_t size = RESET_REASON_BUFFER_SIZE; - - uint32_t cause; - auto ret = hwinfo_get_reset_cause(&cause); - if (ret) { - ESP_LOGE(TAG, "Unable to get reset cause: %d", ret); - buf[0] = '\0'; - return buf; - } - size_t pos = 0; - - pos = append_reset_reason(buf, size, pos, cause & RESET_PIN, "External pin"); - pos = append_reset_reason(buf, size, pos, cause & RESET_SOFTWARE, "Software reset"); - pos = append_reset_reason(buf, size, pos, cause & RESET_BROWNOUT, "Brownout (drop in voltage)"); - pos = append_reset_reason(buf, size, pos, cause & RESET_POR, "Power-on reset (POR)"); - pos = append_reset_reason(buf, size, pos, cause & RESET_WATCHDOG, "Watchdog timer expiration"); - pos = append_reset_reason(buf, size, pos, cause & RESET_DEBUG, "Debug event"); - pos = append_reset_reason(buf, size, pos, cause & RESET_SECURITY, "Security violation"); - pos = append_reset_reason(buf, size, pos, cause & RESET_LOW_POWER_WAKE, "Waking up from low power mode"); - pos = append_reset_reason(buf, size, pos, cause & RESET_CPU_LOCKUP, "CPU lock-up detected"); - pos = append_reset_reason(buf, size, pos, cause & RESET_PARITY, "Parity error"); - pos = append_reset_reason(buf, size, pos, cause & RESET_PLL, "PLL error"); - pos = append_reset_reason(buf, size, pos, cause & RESET_CLOCK, "Clock error"); - pos = append_reset_reason(buf, size, pos, cause & RESET_HARDWARE, "Hardware reset"); - pos = append_reset_reason(buf, size, pos, cause & RESET_USER, "User reset"); - pos = append_reset_reason(buf, size, pos, cause & RESET_TEMPERATURE, "Temperature reset"); - - // Ensure null termination if nothing was written - if (pos == 0) { - buf[0] = '\0'; - } - + const char *buf = zephyr::get_reset_reason(buffer); ESP_LOGD(TAG, "Reset Reason: %s", buf); return buf; } diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index b2952d7995..c8f3c52911 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -101,6 +101,8 @@ CONF_INITIAL_LEVEL = "initial_level" CONF_LOGGER_ID = "logger_id" CONF_RUNTIME_TAG_LEVELS = "runtime_tag_levels" CONF_TASK_LOG_BUFFER_SIZE = "task_log_buffer_size" +CONF_WAIT_FOR_CDC = "wait_for_cdc" +CONF_EARLY_MESSAGE = "early_message" UART_SELECTION_ESP32 = { VARIANT_ESP32: [UART0, UART1, UART2], @@ -208,6 +210,12 @@ def validate_initial_no_higher_than_global(config): return config +def validate_wait_for_cdc(config): + if config.get(CONF_WAIT_FOR_CDC) and config.get(CONF_HARDWARE_UART) != USB_CDC: + raise cv.Invalid("wait_for_cdc requires hardware_uart: USB_CDC") + return config + + Logger = logger_ns.class_("Logger", cg.Component) LoggerMessageTrigger = logger_ns.class_( "LoggerMessageTrigger", @@ -300,10 +308,18 @@ CONFIG_SCHEMA = cv.All( cv.SplitDefault( CONF_ESP8266_STORE_LOG_STRINGS_IN_FLASH, esp8266=True ): cv.All(cv.only_on_esp8266, cv.boolean), + cv.SplitDefault(CONF_WAIT_FOR_CDC, nrf52=False): cv.All( + cv.only_on(PLATFORM_NRF52), + cv.boolean, + ), + cv.SplitDefault(CONF_EARLY_MESSAGE, nrf52=False): cv.All( + cv.only_on(PLATFORM_NRF52), cv.boolean + ), } ).extend(cv.COMPONENT_SCHEMA), validate_local_no_higher_than_global, validate_initial_no_higher_than_global, + validate_wait_for_cdc, ) @@ -425,13 +441,21 @@ async def to_code(config): except cv.Invalid: pass + if config.get(CONF_WAIT_FOR_CDC): + cg.add_define("USE_LOGGER_WAIT_FOR_CDC") + if config.get(CONF_EARLY_MESSAGE): + cg.add_define("USE_LOGGER_EARLY_MESSAGE") + if CORE.is_nrf52: + # esphome implement own fatal error handler which save PC/LR before reset + zephyr_add_prj_conf("RESET_ON_FATAL_ERROR", False) zephyr_add_prj_conf("THREAD_LOCAL_STORAGE", True) if config[CONF_HARDWARE_UART] == UART0: zephyr_add_overlay("""&uart0 { status = "okay";};""") if config[CONF_HARDWARE_UART] == UART1: zephyr_add_overlay("""&uart1 { status = "okay";};""") if config[CONF_HARDWARE_UART] == USB_CDC: + cg.add_define("USE_LOGGER_UART_SELECTION_USB_CDC") zephyr_add_prj_conf("UART_LINE_CTRL", True) zephyr_add_cdc_acm(config, 0) diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index e1b49bcb61..87963c5fc5 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -261,6 +261,9 @@ void Logger::dump_config() { ESP_LOGCONFIG(TAG, " Level for '%s': %s", it.first, LOG_STR_ARG(get_log_level_str(it.second))); } #endif +#ifdef USE_ZEPHYR + dump_crash_(); +#endif } void Logger::set_log_level(uint8_t level) { diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index 2a7552af92..c6c379f6c6 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -317,6 +317,7 @@ class Logger : public Component { Stream *hw_serial_{nullptr}; #endif #if defined(USE_ZEPHYR) + void dump_crash_(); const device *uart_dev_{nullptr}; #endif #if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) diff --git a/esphome/components/logger/logger_zephyr.cpp b/esphome/components/logger/logger_zephyr.cpp index f565c5760c..d6193ff36b 100644 --- a/esphome/components/logger/logger_zephyr.cpp +++ b/esphome/components/logger/logger_zephyr.cpp @@ -8,9 +8,30 @@ #include #include #include +#ifdef USE_LOGGER_EARLY_MESSAGE +#include +#endif + +namespace esphome::zephyr_coredump { + +__attribute__((weak)) void print_coredump() {} + +} // namespace esphome::zephyr_coredump namespace esphome::logger { +static const uint32_t CRASH_MAGIC = 0xDEADBEEF; + +__attribute__((section(".noinit"))) struct { + uint32_t magic; + uint32_t reason; + uint32_t pc; + uint32_t lr; +#if defined(CONFIG_THREAD_NAME) + char thread[CONFIG_THREAD_MAX_NAME_LEN]; +#endif +} crash_buf; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + static const char *const TAG = "logger"; #ifdef USE_LOGGER_USB_CDC @@ -57,10 +78,26 @@ void Logger::pre_setup() { ESP_LOGE(TAG, "%s is not ready.", LOG_STR_ARG(get_uart_selection_())); } else { this->uart_dev_ = uart_dev; +#if defined(USE_LOGGER_WAIT_FOR_CDC) && defined(USE_LOGGER_UART_SELECTION_USB_CDC) + uint32_t dtr = 0; + uint32_t count = (10 * 100); // wait 10 sec for USB CDC to have early logs + while (dtr == 0 && count-- != 0) { + uart_line_ctrl_get(this->uart_dev_, UART_LINE_CTRL_DTR, &dtr); + delay(10); + arch_feed_wdt(); + } +#endif } } global_logger = this; ESP_LOGI(TAG, "Log initialized"); +#ifdef USE_LOGGER_EARLY_MESSAGE + char reason_buffer[zephyr::RESET_REASON_BUFFER_SIZE]; + const char *reset_reason = zephyr::get_reset_reason(std::span(reason_buffer)); + ESP_LOGI(TAG, "Reset reason: %s", reset_reason); + dump_crash_(); + zephyr_coredump::print_coredump(); +#endif } void HOT Logger::write_msg_(const char *msg, uint16_t len) { @@ -93,6 +130,66 @@ const LogString *Logger::get_uart_selection_() { } } +static const uint8_t REASON_BUF_SIZE = 32; + +static const char *reason_to_str(unsigned int reason, char *buf) { + switch (reason) { + case K_ERR_CPU_EXCEPTION: + return "CPU exception"; + case K_ERR_SPURIOUS_IRQ: + return "Unhandled interrupt"; + case K_ERR_STACK_CHK_FAIL: + return "Stack overflow"; + case K_ERR_KERNEL_OOPS: + return "Kernel oops"; + case K_ERR_KERNEL_PANIC: + return "Kernel panic"; + default: + snprintf(buf, REASON_BUF_SIZE, "Unknown error (%u)", reason); + return buf; + } +} + +void Logger::dump_crash_() { + ESP_LOGD(TAG, "Crash buffer address %p", &crash_buf); + if (crash_buf.magic == CRASH_MAGIC) { + char reason_buf[REASON_BUF_SIZE]; + ESP_LOGE(TAG, "Last crash:"); + ESP_LOGE(TAG, "Reason=%s PC=0x%08x LR=0x%08x", reason_to_str(crash_buf.reason, reason_buf), crash_buf.pc, + crash_buf.lr); +#if defined(CONFIG_THREAD_NAME) + ESP_LOGE(TAG, "Thread: %s", crash_buf.thread); +#endif + } +} + +void k_sys_fatal_error_handler(unsigned int reason, const z_arch_esf_t *esf) { + crash_buf.magic = CRASH_MAGIC; + crash_buf.reason = reason; + if (esf) { + crash_buf.pc = esf->basic.pc; + crash_buf.lr = esf->basic.lr; + } +#if defined(CONFIG_THREAD_NAME) + auto thread = k_current_get(); + const char *name = k_thread_name_get(thread); + if (name) { + strncpy(crash_buf.thread, name, sizeof(crash_buf.thread) - 1); + crash_buf.thread[sizeof(crash_buf.thread) - 1] = '\0'; + } else { + crash_buf.thread[0] = '\0'; + } +#endif + arch_restart(); +} + } // namespace esphome::logger +extern "C" { + +void k_sys_fatal_error_handler(unsigned int reason, const z_arch_esf_t *esf) { + esphome::logger::k_sys_fatal_error_handler(reason, esf); +} +} + #endif diff --git a/esphome/components/nrf52/__init__.py b/esphome/components/nrf52/__init__.py index 7d3d59f0ad..95e3670124 100644 --- a/esphome/components/nrf52/__init__.py +++ b/esphome/components/nrf52/__init__.py @@ -266,6 +266,7 @@ async def to_code(config: ConfigType) -> None: }; """ ) + zephyr_add_prj_conf("REBOOT", True) @coroutine_with_priority(CoroPriority.DIAGNOSTICS) diff --git a/esphome/components/zephyr/gpio.h b/esphome/components/zephyr/gpio.h index 94f25f02ac..907fbe9f9c 100644 --- a/esphome/components/zephyr/gpio.h +++ b/esphome/components/zephyr/gpio.h @@ -3,8 +3,7 @@ #ifdef USE_ZEPHYR #include "esphome/core/hal.h" #include -namespace esphome { -namespace zephyr { +namespace esphome::zephyr { class ZephyrGPIOPin : public InternalGPIOPin { public: @@ -39,7 +38,6 @@ class ZephyrGPIOPin : public InternalGPIOPin { bool value_{false}; }; -} // namespace zephyr -} // namespace esphome +} // namespace esphome::zephyr #endif // USE_ZEPHYR diff --git a/esphome/components/zephyr/preferences.h b/esphome/components/zephyr/preferences.h index 6a37e41b46..4bee96d79e 100644 --- a/esphome/components/zephyr/preferences.h +++ b/esphome/components/zephyr/preferences.h @@ -2,12 +2,10 @@ #ifdef USE_ZEPHYR -namespace esphome { -namespace zephyr { +namespace esphome::zephyr { void setup_preferences(); -} // namespace zephyr -} // namespace esphome +} #endif diff --git a/esphome/components/zephyr/reset_reason.cpp b/esphome/components/zephyr/reset_reason.cpp new file mode 100644 index 0000000000..24b32196db --- /dev/null +++ b/esphome/components/zephyr/reset_reason.cpp @@ -0,0 +1,63 @@ +#include "reset_reason.h" + +#if defined(USE_ZEPHYR) && (defined(USE_LOGGER_EARLY_MESSAGE) || defined(USE_DEBUG)) +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" +#include + +namespace esphome::zephyr { + +static const char *const TAG = "zephyr"; + +static size_t append_reset_reason(char *buf, size_t size, size_t pos, bool set, const char *reason) { + if (!set) { + return pos; + } + if (pos > 0) { + pos = buf_append_printf(buf, size, pos, ", "); + } + return buf_append_printf(buf, size, pos, "%s", reason); +} + +const char *get_reset_reason(std::span buffer) { + char *buf = buffer.data(); + const size_t size = RESET_REASON_BUFFER_SIZE; + + uint32_t cause; + auto ret = hwinfo_get_reset_cause(&cause); + if (ret) { + ESP_LOGE(TAG, "Unable to get reset cause: %d", ret); + buf[0] = '\0'; + return buf; + } + size_t pos = 0; + + if (cause == 0) { + pos = append_reset_reason(buf, size, pos, true, "None"); + } else { + pos = append_reset_reason(buf, size, pos, cause & RESET_PIN, "External pin"); + pos = append_reset_reason(buf, size, pos, cause & RESET_SOFTWARE, "Software reset"); + pos = append_reset_reason(buf, size, pos, cause & RESET_BROWNOUT, "Brownout (drop in voltage)"); + pos = append_reset_reason(buf, size, pos, cause & RESET_POR, "Power-on reset (POR)"); + pos = append_reset_reason(buf, size, pos, cause & RESET_WATCHDOG, "Watchdog timer expiration"); + pos = append_reset_reason(buf, size, pos, cause & RESET_DEBUG, "Debug event"); + pos = append_reset_reason(buf, size, pos, cause & RESET_SECURITY, "Security violation"); + pos = append_reset_reason(buf, size, pos, cause & RESET_LOW_POWER_WAKE, "Waking up from low power mode"); + pos = append_reset_reason(buf, size, pos, cause & RESET_CPU_LOCKUP, "CPU lock-up detected"); + pos = append_reset_reason(buf, size, pos, cause & RESET_PARITY, "Parity error"); + pos = append_reset_reason(buf, size, pos, cause & RESET_PLL, "PLL error"); + pos = append_reset_reason(buf, size, pos, cause & RESET_CLOCK, "Clock error"); + pos = append_reset_reason(buf, size, pos, cause & RESET_HARDWARE, "Hardware reset"); + pos = append_reset_reason(buf, size, pos, cause & RESET_USER, "User reset"); + pos = append_reset_reason(buf, size, pos, cause & RESET_TEMPERATURE, "Temperature reset"); + } + + // Ensure null termination if nothing was written + if (pos == 0) { + buf[0] = '\0'; + } + return buf; +} +} // namespace esphome::zephyr + +#endif diff --git a/esphome/components/zephyr/reset_reason.h b/esphome/components/zephyr/reset_reason.h new file mode 100644 index 0000000000..2c2e7b8470 --- /dev/null +++ b/esphome/components/zephyr/reset_reason.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/core/defines.h" + +#if defined(USE_ZEPHYR) && (defined(USE_LOGGER_EARLY_MESSAGE) || defined(USE_DEBUG)) + +#include +#include + +namespace esphome::zephyr { + +static constexpr size_t RESET_REASON_BUFFER_SIZE = 128; + +const char *get_reset_reason(std::span buffer); + +} // namespace esphome::zephyr + +#endif diff --git a/esphome/config_validation.py b/esphome/config_validation.py index a9d1a72e5a..ef1c66a20e 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -70,6 +70,7 @@ from esphome.const import ( KEY_TARGET_FRAMEWORK, PLATFORM_ESP32, PLATFORM_ESP8266, + PLATFORM_NRF52, PLATFORM_RP2040, SCHEDULER_DONT_RUN, TYPE_GIT, @@ -695,6 +696,7 @@ def only_with_framework( only_on_esp32 = only_on(PLATFORM_ESP32) only_on_esp8266 = only_on(PLATFORM_ESP8266) +only_on_nrf52 = only_on(PLATFORM_NRF52) only_on_rp2040 = only_on(PLATFORM_RP2040) only_with_arduino = only_with_framework(Framework.ARDUINO) diff --git a/esphome/core/defines.h b/esphome/core/defines.h index a7fb9f197c..c82b222a3d 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -334,6 +334,8 @@ #ifdef USE_NRF52 #define USE_ESPHOME_TASK_LOG_BUFFER +#define USE_LOGGER_EARLY_MESSAGE +#define USE_LOGGER_WAIT_FOR_CDC #define USE_NRF52_DFU #define USE_NRF52_REG0_VOUT 5 #define USE_NRF52_UICR_ERASE diff --git a/tests/components/logger/test.nrf52-adafruit.yaml b/tests/components/logger/test.nrf52-adafruit.yaml index 821a136250..062451188f 100644 --- a/tests/components/logger/test.nrf52-adafruit.yaml +++ b/tests/components/logger/test.nrf52-adafruit.yaml @@ -5,4 +5,6 @@ esphome: logger: level: DEBUG + wait_for_cdc: true + early_message: true task_log_buffer_size: 0 From d206c75b0b815c1cdd5d4f0741f0b04f22918138 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 20 Feb 2026 19:20:44 -0600 Subject: [PATCH 156/197] [logger] Fix loop disable optimization using wrong preprocessor guard (#14158) --- esphome/components/logger/logger.cpp | 13 ++++++------- esphome/components/logger/logger.h | 10 +++++----- esphome/components/logger/logger_zephyr.cpp | 2 +- esphome/core/defines.h | 4 ++++ 4 files changed, 16 insertions(+), 13 deletions(-) diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index 87963c5fc5..22a95e4835 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -170,19 +170,19 @@ void Logger::init_log_buffer(size_t total_buffer_size) { // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) - allocated once, never freed this->log_buffer_ = new logger::TaskLogBuffer(total_buffer_size); -// Zephyr needs loop working to check when CDC port is open -#if !(defined(USE_ZEPHYR) || defined(USE_LOGGER_USB_CDC)) - // Start with loop disabled when using task buffer (unless using USB CDC on ESP32) +#if !(defined(USE_ZEPHYR) && defined(USE_LOGGER_UART_SELECTION_USB_CDC)) + // Start with loop disabled when using task buffer // The loop will be enabled automatically when messages arrive + // Zephyr with USB CDC needs loop active to poll port readiness via cdc_loop_() this->disable_loop_when_buffer_empty_(); #endif } #endif -#if defined(USE_ESPHOME_TASK_LOG_BUFFER) || (defined(USE_ZEPHYR) && defined(USE_LOGGER_USB_CDC)) +#if defined(USE_ESPHOME_TASK_LOG_BUFFER) || (defined(USE_ZEPHYR) && defined(USE_LOGGER_UART_SELECTION_USB_CDC)) void Logger::loop() { this->process_messages_(); -#if defined(USE_ZEPHYR) && defined(USE_LOGGER_USB_CDC) +#if defined(USE_ZEPHYR) && defined(USE_LOGGER_UART_SELECTION_USB_CDC) this->cdc_loop_(); #endif } @@ -204,8 +204,7 @@ void Logger::process_messages_() { this->write_log_buffer_to_console_(buf); } } -// Zephyr needs loop working to check when CDC port is open -#if !(defined(USE_ZEPHYR) || defined(USE_LOGGER_USB_CDC)) +#if !(defined(USE_ZEPHYR) && defined(USE_LOGGER_UART_SELECTION_USB_CDC)) else { // No messages to process, disable loop if appropriate // This reduces overhead when there's no async logging activity diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index c6c379f6c6..8bf1edebb8 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -147,7 +147,7 @@ class Logger : public Component { #ifdef USE_ESPHOME_TASK_LOG_BUFFER void init_log_buffer(size_t total_buffer_size); #endif -#if defined(USE_ESPHOME_TASK_LOG_BUFFER) || (defined(USE_ZEPHYR) && defined(USE_LOGGER_USB_CDC)) +#if defined(USE_ESPHOME_TASK_LOG_BUFFER) || (defined(USE_ZEPHYR) && defined(USE_LOGGER_UART_SELECTION_USB_CDC)) void loop() override; #endif /// Manually set the baud rate for serial, set to 0 to disable. @@ -229,7 +229,7 @@ class Logger : public Component { void log_vprintf_non_main_thread_(uint8_t level, const char *tag, int line, const char *format, va_list args, const char *thread_name); #endif -#if defined(USE_ZEPHYR) && defined(USE_LOGGER_USB_CDC) +#if defined(USE_ZEPHYR) && defined(USE_LOGGER_UART_SELECTION_USB_CDC) void cdc_loop_(); #endif void process_messages_(); @@ -465,9 +465,9 @@ class Logger : public Component { inline RecursionGuard make_non_main_task_guard_() { return RecursionGuard(non_main_task_recursion_guard_); } #endif -// Zephyr needs loop working to check when CDC port is open -#if defined(USE_ESPHOME_TASK_LOG_BUFFER) && !(defined(USE_ZEPHYR) || defined(USE_LOGGER_USB_CDC)) - // Disable loop when task buffer is empty (with USB CDC check on ESP32) +#if defined(USE_ESPHOME_TASK_LOG_BUFFER) && !(defined(USE_ZEPHYR) && defined(USE_LOGGER_UART_SELECTION_USB_CDC)) + // Disable loop when task buffer is empty + // Zephyr with USB CDC needs loop active to poll port readiness via cdc_loop_() inline void disable_loop_when_buffer_empty_() { // Thread safety note: This is safe even if another task calls enable_loop_soon_any_context() // concurrently. If that happens between our check and disable_loop(), the enable request diff --git a/esphome/components/logger/logger_zephyr.cpp b/esphome/components/logger/logger_zephyr.cpp index d6193ff36b..c2d24d6efc 100644 --- a/esphome/components/logger/logger_zephyr.cpp +++ b/esphome/components/logger/logger_zephyr.cpp @@ -34,7 +34,7 @@ __attribute__((section(".noinit"))) struct { static const char *const TAG = "logger"; -#ifdef USE_LOGGER_USB_CDC +#ifdef USE_LOGGER_UART_SELECTION_USB_CDC void Logger::cdc_loop_() { if (this->uart_ != UART_SELECTION_USB_CDC || this->uart_dev_ == nullptr) { return; diff --git a/esphome/core/defines.h b/esphome/core/defines.h index c82b222a3d..5109dd36f4 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -270,10 +270,12 @@ #if defined(USE_ESP32_VARIANT_ESP32S2) #define USE_LOGGER_USB_CDC +#define USE_LOGGER_UART_SELECTION_USB_CDC #elif defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C5) || \ defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32C61) || defined(USE_ESP32_VARIANT_ESP32H2) || \ defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S3) #define USE_LOGGER_USB_CDC +#define USE_LOGGER_UART_SELECTION_USB_CDC #define USE_LOGGER_USB_SERIAL_JTAG #endif #endif @@ -335,6 +337,8 @@ #ifdef USE_NRF52 #define USE_ESPHOME_TASK_LOG_BUFFER #define USE_LOGGER_EARLY_MESSAGE +#define USE_LOGGER_UART_SELECTION_USB_CDC +#define USE_LOGGER_USB_CDC #define USE_LOGGER_WAIT_FOR_CDC #define USE_NRF52_DFU #define USE_NRF52_REG0_VOUT 5 From 35037d1a5b7c5ad7f4dbabaaa60dab2e7d76af01 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 20 Feb 2026 19:20:58 -0600 Subject: [PATCH 157/197] [core] Deduplicate base64 encode/decode logic (#14143) Co-authored-by: Claude Opus 4.6 --- esphome/core/helpers.cpp | 88 ++++++++++++++++++---------------------- 1 file changed, 40 insertions(+), 48 deletions(-) diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index 09e755ca71..9f850b5df8 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -545,38 +545,36 @@ static inline bool is_base64(char c) { return (isalnum(c) || (c == '+') || (c == std::string base64_encode(const std::vector &buf) { return base64_encode(buf.data(), buf.size()); } +// Encode 3 input bytes to 4 base64 characters, append 'count' to ret. +static inline void base64_encode_triple(const char *char_array_3, int count, std::string &ret) { + char char_array_4[4]; + char_array_4[0] = (char_array_3[0] & 0xfc) >> 2; + char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4); + char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6); + char_array_4[3] = char_array_3[2] & 0x3f; + + for (int j = 0; j < count; j++) + ret += BASE64_CHARS[static_cast(char_array_4[j])]; +} + std::string base64_encode(const uint8_t *buf, size_t buf_len) { std::string ret; int i = 0; - int j = 0; char char_array_3[3]; - char char_array_4[4]; while (buf_len--) { char_array_3[i++] = *(buf++); if (i == 3) { - char_array_4[0] = (char_array_3[0] & 0xfc) >> 2; - char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4); - char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6); - char_array_4[3] = char_array_3[2] & 0x3f; - - for (i = 0; (i < 4); i++) - ret += BASE64_CHARS[static_cast(char_array_4[i])]; + base64_encode_triple(char_array_3, 4, ret); i = 0; } } if (i) { - for (j = i; j < 3; j++) + for (int j = i; j < 3; j++) char_array_3[j] = '\0'; - char_array_4[0] = (char_array_3[0] & 0xfc) >> 2; - char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4); - char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6); - char_array_4[3] = char_array_3[2] & 0x3f; - - for (j = 0; (j < i + 1); j++) - ret += BASE64_CHARS[static_cast(char_array_4[j])]; + base64_encode_triple(char_array_3, i + 1, ret); while ((i++ < 3)) ret += '='; @@ -589,13 +587,33 @@ size_t base64_decode(const std::string &encoded_string, uint8_t *buf, size_t buf return base64_decode(reinterpret_cast(encoded_string.data()), encoded_string.size(), buf, buf_len); } +// Decode 4 base64 characters to up to 'count' output bytes, returns true if truncated. +static inline bool base64_decode_quad(uint8_t *char_array_4, int count, uint8_t *buf, size_t buf_len, size_t &out) { + for (int i = 0; i < 4; i++) + char_array_4[i] = base64_find_char(char_array_4[i]); + + uint8_t char_array_3[3]; + char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4); + char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2); + char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3]; + + bool truncated = false; + for (int j = 0; j < count; j++) { + if (out < buf_len) { + buf[out++] = char_array_3[j]; + } else { + truncated = true; + } + } + return truncated; +} + size_t base64_decode(const uint8_t *encoded_data, size_t encoded_len, uint8_t *buf, size_t buf_len) { size_t in_len = encoded_len; int i = 0; - int j = 0; size_t in = 0; size_t out = 0; - uint8_t char_array_4[4], char_array_3[3]; + uint8_t char_array_4[4]; bool truncated = false; // SAFETY: The loop condition checks is_base64() before processing each character. @@ -605,42 +623,16 @@ size_t base64_decode(const uint8_t *encoded_data, size_t encoded_len, uint8_t *b char_array_4[i++] = encoded_data[in]; in++; if (i == 4) { - for (i = 0; i < 4; i++) - char_array_4[i] = base64_find_char(char_array_4[i]); - - char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4); - char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2); - char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3]; - - for (i = 0; i < 3; i++) { - if (out < buf_len) { - buf[out++] = char_array_3[i]; - } else { - truncated = true; - } - } + truncated |= base64_decode_quad(char_array_4, 3, buf, buf_len, out); i = 0; } } if (i) { - for (j = i; j < 4; j++) + for (int j = i; j < 4; j++) char_array_4[j] = 0; - for (j = 0; j < 4; j++) - char_array_4[j] = base64_find_char(char_array_4[j]); - - char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4); - char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2); - char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3]; - - for (j = 0; j < i - 1; j++) { - if (out < buf_len) { - buf[out++] = char_array_3[j]; - } else { - truncated = true; - } - } + truncated |= base64_decode_quad(char_array_4, i - 1, buf, buf_len, out); } if (truncated) { From a3f279c1cf150f6908b84df9976e72821995e7b0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 20 Feb 2026 19:21:14 -0600 Subject: [PATCH 158/197] [usb_host] Implement disable_loop/enable_loop pattern for USB components (#14163) --- esphome/components/usb_host/usb_host.h | 21 +++++++++++-------- .../components/usb_host/usb_host_client.cpp | 17 ++++++++++++++- esphome/components/usb_uart/usb_uart.cpp | 11 +++++++++- 3 files changed, 38 insertions(+), 11 deletions(-) diff --git a/esphome/components/usb_host/usb_host.h b/esphome/components/usb_host/usb_host.h index d1ec356613..a6a97d0bd7 100644 --- a/esphome/components/usb_host/usb_host.h +++ b/esphome/components/usb_host/usb_host.h @@ -73,12 +73,12 @@ static constexpr UBaseType_t USB_TASK_PRIORITY = 5; // Higher priority than mai // used to report a transfer status struct TransferStatus { - bool success; - uint16_t error_code; uint8_t *data; size_t data_len; - uint8_t endpoint; void *user_data; + uint16_t error_code; + uint8_t endpoint; + bool success; }; using transfer_cb_t = std::function; @@ -127,7 +127,7 @@ class USBClient : public Component { friend class USBHost; public: - USBClient(uint16_t vid, uint16_t pid) : vid_(vid), pid_(pid), trq_in_use_(0) {} + USBClient(uint16_t vid, uint16_t pid) : trq_in_use_(0), vid_(vid), pid_(pid) {} void setup() override; void loop() override; // setup must happen after the host bus has been setup @@ -148,6 +148,10 @@ class USBClient : public Component { EventPool event_pool; protected: + // Process USB events from the queue. Returns true if any work was done. + // Subclasses should call this instead of USBClient::loop() to combine + // with their own work check for a single disable_loop() decision. + bool process_usb_events_(); void handle_open_state_(); TransferRequest *get_trq_(); // Lock-free allocation using atomic bitmask (multi-consumer safe) virtual void disconnect(); @@ -161,20 +165,19 @@ class USBClient : public Component { static void usb_task_fn(void *arg); [[noreturn]] void usb_task_loop() const; + // Members ordered to minimize struct padding on 32-bit platforms + TransferRequest requests_[MAX_REQUESTS]{}; TaskHandle_t usb_task_handle_{nullptr}; - usb_host_client_handle_t handle_{}; usb_device_handle_t device_handle_{}; int device_addr_{-1}; int state_{USB_CLIENT_INIT}; - uint16_t vid_{}; - uint16_t pid_{}; // Lock-free pool management using atomic bitmask (no dynamic allocation) // Bit i = 1: requests_[i] is in use, Bit i = 0: requests_[i] is available // Supports multiple concurrent consumers and producers (both threads can allocate/deallocate) - // Bitmask type automatically selected: uint16_t for <= 16 slots, uint32_t for 17-32 slots std::atomic trq_in_use_; - TransferRequest requests_[MAX_REQUESTS]{}; + uint16_t vid_{}; + uint16_t pid_{}; }; class USBHost : public Component { public: diff --git a/esphome/components/usb_host/usb_host_client.cpp b/esphome/components/usb_host/usb_host_client.cpp index 0612d7a841..a9be38fb03 100644 --- a/esphome/components/usb_host/usb_host_client.cpp +++ b/esphome/components/usb_host/usb_host_client.cpp @@ -197,6 +197,9 @@ static void client_event_cb(const usb_host_client_event_msg_t *event_msg, void * // Push to lock-free queue (always succeeds since pool size == queue size) client->event_queue.push(event); + // Re-enable component loop to process the queued event + client->enable_loop_soon_any_context(); + // Wake main loop immediately to process USB event instead of waiting for select() timeout #if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) App.wake_loop_threadsafe(); @@ -243,10 +246,13 @@ void USBClient::usb_task_loop() const { } } -void USBClient::loop() { +bool USBClient::process_usb_events_() { + bool had_work = false; + // Process any events from the USB task UsbEvent *event; while ((event = this->event_queue.pop()) != nullptr) { + had_work = true; switch (event->type) { case EVENT_DEVICE_NEW: this->on_opened(event->data.device_new.address); @@ -266,8 +272,17 @@ void USBClient::loop() { } if (this->state_ == USB_CLIENT_OPEN) { + had_work = true; this->handle_open_state_(); } + + return had_work; +} + +void USBClient::loop() { + if (!this->process_usb_events_()) { + this->disable_loop(); + } } void USBClient::handle_open_state_() { diff --git a/esphome/components/usb_uart/usb_uart.cpp b/esphome/components/usb_uart/usb_uart.cpp index edd01c26c6..5c2806c456 100644 --- a/esphome/components/usb_uart/usb_uart.cpp +++ b/esphome/components/usb_uart/usb_uart.cpp @@ -172,11 +172,12 @@ bool USBUartChannel::read_array(uint8_t *data, size_t len) { } void USBUartComponent::setup() { USBClient::setup(); } void USBUartComponent::loop() { - USBClient::loop(); + bool had_work = this->process_usb_events_(); // Process USB data from the lock-free queue UsbDataChunk *chunk; while ((chunk = this->usb_data_queue_.pop()) != nullptr) { + had_work = true; auto *channel = chunk->channel; #ifdef USE_UART_DEBUGGER @@ -198,6 +199,11 @@ void USBUartComponent::loop() { if (dropped > 0) { ESP_LOGW(TAG, "Dropped %u USB data chunks due to buffer overflow", dropped); } + + // Disable loop when idle. Callbacks re-enable via enable_loop_soon_any_context(). + if (!had_work) { + this->disable_loop(); + } } void USBUartComponent::dump_config() { USBClient::dump_config(); @@ -264,6 +270,9 @@ void USBUartComponent::start_input(USBUartChannel *channel) { // Push always succeeds because pool size == queue size this->usb_data_queue_.push(chunk); + // Re-enable component loop to process the queued data + this->enable_loop_soon_any_context(); + // Wake main loop immediately to process USB data instead of waiting for select() timeout #if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) App.wake_loop_threadsafe(); From 0e38acd67a178ca6490284093c7034e8bfde5532 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 20 Feb 2026 19:21:56 -0600 Subject: [PATCH 159/197] [api] Warn when clients connect with outdated API version (#14145) --- esphome/components/api/api_connection.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 5a7994a322..5b02bee537 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1534,6 +1534,12 @@ bool APIConnection::send_hello_response_(const HelloRequest &msg) { ESP_LOGV(TAG, "Hello from client: '%s' | %s | API Version %" PRIu16 ".%" PRIu16, this->helper_->get_client_name(), this->helper_->get_peername_to(peername), this->client_api_version_major_, this->client_api_version_minor_); + // TODO: Remove before 2026.8.0 (one version after get_object_id backward compat removal) + if (!this->client_supports_api_version(1, 14)) { + ESP_LOGW(TAG, "'%s' using outdated API %" PRIu16 ".%" PRIu16 ", update to 1.14+", this->helper_->get_client_name(), + this->client_api_version_major_, this->client_api_version_minor_); + } + HelloResponse resp; resp.api_version_major = 1; resp.api_version_minor = 14; From 8589f80d8fea4751aeac15134c91ca79fdf850a9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 20 Feb 2026 20:59:49 -0600 Subject: [PATCH 160/197] [api,ota,captive_portal] Fix fd leaks and clean up socket_ip_loop_monitored setup paths (#14167) --- esphome/components/api/api_server.cpp | 30 +++++++++---------- esphome/components/api/api_server.h | 9 +++++- .../captive_portal/dns_server_esp32_idf.cpp | 9 ++---- .../captive_portal/dns_server_esp32_idf.h | 9 ++++-- .../components/esphome/ota/ota_esphome.cpp | 26 +++++++++------- esphome/components/esphome/ota/ota_esphome.h | 3 +- 6 files changed, 49 insertions(+), 37 deletions(-) diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 1a1d0b229b..5b096788f5 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -30,6 +30,12 @@ APIServer *global_api_server = nullptr; // NOLINT(cppcoreguidelines-avoid-non-c APIServer::APIServer() { global_api_server = this; } +void APIServer::socket_failed_(const LogString *msg) { + ESP_LOGW(TAG, "Socket %s: errno %d", LOG_STR_ARG(msg), errno); + this->destroy_socket_(); + this->mark_failed(); +} + void APIServer::setup() { ControllerRegistry::register_controller(this); @@ -48,22 +54,20 @@ void APIServer::setup() { #endif #endif - this->socket_ = socket::socket_ip_loop_monitored(SOCK_STREAM, 0); // monitored for incoming connections + this->socket_ = socket::socket_ip_loop_monitored(SOCK_STREAM, 0).release(); // monitored for incoming connections if (this->socket_ == nullptr) { - ESP_LOGW(TAG, "Could not create socket"); - this->mark_failed(); + this->socket_failed_(LOG_STR("creation")); return; } int enable = 1; int err = this->socket_->setsockopt(SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int)); if (err != 0) { - ESP_LOGW(TAG, "Socket unable to set reuseaddr: errno %d", err); + ESP_LOGW(TAG, "Socket reuseaddr: errno %d", errno); // we can still continue } err = this->socket_->setblocking(false); if (err != 0) { - ESP_LOGW(TAG, "Socket unable to set nonblocking mode: errno %d", err); - this->mark_failed(); + this->socket_failed_(LOG_STR("nonblocking")); return; } @@ -71,22 +75,19 @@ void APIServer::setup() { socklen_t sl = socket::set_sockaddr_any((struct sockaddr *) &server, sizeof(server), this->port_); if (sl == 0) { - ESP_LOGW(TAG, "Socket unable to set sockaddr: errno %d", errno); - this->mark_failed(); + this->socket_failed_(LOG_STR("set sockaddr")); return; } err = this->socket_->bind((struct sockaddr *) &server, sl); if (err != 0) { - ESP_LOGW(TAG, "Socket unable to bind: errno %d", errno); - this->mark_failed(); + this->socket_failed_(LOG_STR("bind")); return; } err = this->socket_->listen(this->listen_backlog_); if (err != 0) { - ESP_LOGW(TAG, "Socket unable to listen: errno %d", errno); - this->mark_failed(); + this->socket_failed_(LOG_STR("listen")); return; } @@ -622,10 +623,7 @@ void APIServer::on_shutdown() { this->shutting_down_ = true; // Close the listening socket to prevent new connections - if (this->socket_) { - this->socket_->close(); - this->socket_ = nullptr; - } + this->destroy_socket_(); // Change batch delay to 5ms for quick flushing during shutdown this->batch_delay_ = 5; diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index 3b9ba0e23b..fed29016b3 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -249,8 +249,15 @@ class APIServer : public Component, void add_state_subscription_(std::string entity_id, optional attribute, std::function f, bool once); #endif // USE_API_HOMEASSISTANT_STATES + // No explicit close() needed — listen sockets have no active connections on + // failure/shutdown. Destructor handles fd cleanup (close or abort per platform). + inline void destroy_socket_() { + delete this->socket_; + this->socket_ = nullptr; + } + void socket_failed_(const LogString *msg); // Pointers and pointer-like types first (4 bytes each) - std::unique_ptr socket_ = nullptr; + socket::Socket *socket_{nullptr}; #ifdef USE_API_CLIENT_CONNECTED_TRIGGER Trigger client_connected_trigger_; #endif diff --git a/esphome/components/captive_portal/dns_server_esp32_idf.cpp b/esphome/components/captive_portal/dns_server_esp32_idf.cpp index 5743cbd671..bd9989a40c 100644 --- a/esphome/components/captive_portal/dns_server_esp32_idf.cpp +++ b/esphome/components/captive_portal/dns_server_esp32_idf.cpp @@ -53,7 +53,7 @@ void DNSServer::start(const network::IPAddress &ip) { #endif // Create loop-monitored UDP socket - this->socket_ = socket::socket_ip_loop_monitored(SOCK_DGRAM, IPPROTO_UDP); + this->socket_ = socket::socket_ip_loop_monitored(SOCK_DGRAM, IPPROTO_UDP).release(); if (this->socket_ == nullptr) { ESP_LOGE(TAG, "Socket create failed"); return; @@ -70,17 +70,14 @@ void DNSServer::start(const network::IPAddress &ip) { int err = this->socket_->bind((struct sockaddr *) &server_addr, addr_len); if (err != 0) { ESP_LOGE(TAG, "Bind failed: %d", errno); - this->socket_ = nullptr; + this->destroy_socket_(); return; } ESP_LOGV(TAG, "Bound to port %d", DNS_PORT); } void DNSServer::stop() { - if (this->socket_ != nullptr) { - this->socket_->close(); - this->socket_ = nullptr; - } + this->destroy_socket_(); ESP_LOGV(TAG, "Stopped"); } diff --git a/esphome/components/captive_portal/dns_server_esp32_idf.h b/esphome/components/captive_portal/dns_server_esp32_idf.h index 3e0ac07373..f8e4cfec84 100644 --- a/esphome/components/captive_portal/dns_server_esp32_idf.h +++ b/esphome/components/captive_portal/dns_server_esp32_idf.h @@ -1,7 +1,6 @@ #pragma once #ifdef USE_ESP32 -#include #include "esphome/core/helpers.h" #include "esphome/components/network/ip_address.h" #include "esphome/components/socket/socket.h" @@ -15,9 +14,15 @@ class DNSServer { void process_next_request(); protected: + // No explicit close() needed — listen sockets have no active connections on + // failure/shutdown. Destructor handles fd cleanup (close or abort per platform). + inline void destroy_socket_() { + delete this->socket_; + this->socket_ = nullptr; + } static constexpr size_t DNS_BUFFER_SIZE = 192; - std::unique_ptr socket_{nullptr}; + socket::Socket *socket_{nullptr}; network::IPAddress server_ip_; uint8_t buffer_[DNS_BUFFER_SIZE]; }; diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index df2ea98f2c..a1cdf59d2b 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -28,10 +28,9 @@ static constexpr uint32_t OTA_SOCKET_TIMEOUT_HANDSHAKE = 20000; // milliseconds static constexpr uint32_t OTA_SOCKET_TIMEOUT_DATA = 90000; // milliseconds for data transfer void ESPHomeOTAComponent::setup() { - this->server_ = socket::socket_ip_loop_monitored(SOCK_STREAM, 0); // monitored for incoming connections + this->server_ = socket::socket_ip_loop_monitored(SOCK_STREAM, 0).release(); // monitored for incoming connections if (this->server_ == nullptr) { - this->log_socket_error_(LOG_STR("creation")); - this->mark_failed(); + this->server_failed_(LOG_STR("creation")); return; } int enable = 1; @@ -42,8 +41,7 @@ void ESPHomeOTAComponent::setup() { } err = this->server_->setblocking(false); if (err != 0) { - this->log_socket_error_(LOG_STR("non-blocking")); - this->mark_failed(); + this->server_failed_(LOG_STR("nonblocking")); return; } @@ -51,22 +49,19 @@ void ESPHomeOTAComponent::setup() { socklen_t sl = socket::set_sockaddr_any((struct sockaddr *) &server, sizeof(server), this->port_); if (sl == 0) { - this->log_socket_error_(LOG_STR("set sockaddr")); - this->mark_failed(); + this->server_failed_(LOG_STR("set sockaddr")); return; } err = this->server_->bind((struct sockaddr *) &server, sizeof(server)); if (err != 0) { - this->log_socket_error_(LOG_STR("bind")); - this->mark_failed(); + this->server_failed_(LOG_STR("bind")); return; } err = this->server_->listen(1); // Only one client at a time if (err != 0) { - this->log_socket_error_(LOG_STR("listen")); - this->mark_failed(); + this->server_failed_(LOG_STR("listen")); return; } } @@ -455,6 +450,15 @@ void ESPHomeOTAComponent::log_remote_closed_(const LogString *during) { ESP_LOGW(TAG, "Remote closed at %s", LOG_STR_ARG(during)); } +void ESPHomeOTAComponent::server_failed_(const LogString *msg) { + this->log_socket_error_(msg); + // No explicit close() needed — listen sockets have no active connections on + // failure/shutdown. Destructor handles fd cleanup (close or abort per platform). + delete this->server_; + this->server_ = nullptr; + this->mark_failed(); +} + bool ESPHomeOTAComponent::handle_read_error_(ssize_t read, const LogString *desc) { if (read == -1 && this->would_block_(errno)) { return false; // No data yet, try again next loop diff --git a/esphome/components/esphome/ota/ota_esphome.h b/esphome/components/esphome/ota/ota_esphome.h index e199b7e406..c9e89c82ba 100644 --- a/esphome/components/esphome/ota/ota_esphome.h +++ b/esphome/components/esphome/ota/ota_esphome.h @@ -66,6 +66,7 @@ class ESPHomeOTAComponent : public ota::OTAComponent { this->handshake_buf_pos_ = 0; // Reset buffer position for next state } + void server_failed_(const LogString *msg); void log_socket_error_(const LogString *msg); void log_read_error_(const LogString *what); void log_start_(const LogString *phase); @@ -83,7 +84,7 @@ class ESPHomeOTAComponent : public ota::OTAComponent { std::unique_ptr auth_buf_; #endif // USE_OTA_PASSWORD - std::unique_ptr server_; + socket::Socket *server_{nullptr}; std::unique_ptr client_; std::unique_ptr backend_; From abe37c98413c6b78a7a01bec82f4b2f14e68cd77 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 20 Feb 2026 21:08:49 -0600 Subject: [PATCH 161/197] [uptime] Use scheduler millis_64() for rollover-safe uptime tracking (#14170) --- .../uptime/sensor/uptime_seconds_sensor.cpp | 27 +++++-------------- .../uptime/sensor/uptime_seconds_sensor.h | 9 ++----- .../uptime/sensor/uptime_timestamp_sensor.cpp | 6 ++--- .../uptime/sensor/uptime_timestamp_sensor.h | 6 ++--- .../uptime/text_sensor/uptime_text_sensor.cpp | 24 ++++------------- .../uptime/text_sensor/uptime_text_sensor.h | 8 ++---- esphome/core/scheduler.cpp | 2 ++ esphome/core/scheduler.h | 3 +++ 8 files changed, 24 insertions(+), 61 deletions(-) diff --git a/esphome/components/uptime/sensor/uptime_seconds_sensor.cpp b/esphome/components/uptime/sensor/uptime_seconds_sensor.cpp index 54260d7e80..20e8ed8fda 100644 --- a/esphome/components/uptime/sensor/uptime_seconds_sensor.cpp +++ b/esphome/components/uptime/sensor/uptime_seconds_sensor.cpp @@ -1,30 +1,16 @@ #include "uptime_seconds_sensor.h" -#include "esphome/core/hal.h" -#include "esphome/core/helpers.h" +#include "esphome/core/application.h" #include "esphome/core/log.h" -namespace esphome { -namespace uptime { +namespace esphome::uptime { static const char *const TAG = "uptime.sensor"; void UptimeSecondsSensor::update() { - const uint32_t ms = millis(); - const uint64_t ms_mask = (1ULL << 32) - 1ULL; - const uint32_t last_ms = this->uptime_ & ms_mask; - if (ms < last_ms) { - this->uptime_ += ms_mask + 1ULL; - ESP_LOGD(TAG, "Detected roll-over \xf0\x9f\xa6\x84"); - } - this->uptime_ &= ~ms_mask; - this->uptime_ |= ms; - - // Do separate second and milliseconds conversion to avoid floating point division errors - // Probably some IEEE standard already guarantees this division can be done without loss - // of precision in a single division, but let's do it like this to be sure. - const uint64_t seconds_int = this->uptime_ / 1000ULL; - const float seconds = float(seconds_int) + (this->uptime_ % 1000ULL) / 1000.0f; + const uint64_t uptime = App.scheduler.millis_64(); + const uint64_t seconds_int = uptime / 1000ULL; + const float seconds = float(seconds_int) + (uptime % 1000ULL) / 1000.0f; this->publish_state(seconds); } float UptimeSecondsSensor::get_setup_priority() const { return setup_priority::HARDWARE; } @@ -33,5 +19,4 @@ void UptimeSecondsSensor::dump_config() { ESP_LOGCONFIG(TAG, " Type: Seconds"); } -} // namespace uptime -} // namespace esphome +} // namespace esphome::uptime diff --git a/esphome/components/uptime/sensor/uptime_seconds_sensor.h b/esphome/components/uptime/sensor/uptime_seconds_sensor.h index 210195052f..1b80a4480a 100644 --- a/esphome/components/uptime/sensor/uptime_seconds_sensor.h +++ b/esphome/components/uptime/sensor/uptime_seconds_sensor.h @@ -3,8 +3,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/core/component.h" -namespace esphome { -namespace uptime { +namespace esphome::uptime { class UptimeSecondsSensor : public sensor::Sensor, public PollingComponent { public: @@ -12,10 +11,6 @@ class UptimeSecondsSensor : public sensor::Sensor, public PollingComponent { void dump_config() override; float get_setup_priority() const override; - - protected: - uint64_t uptime_{0}; }; -} // namespace uptime -} // namespace esphome +} // namespace esphome::uptime diff --git a/esphome/components/uptime/sensor/uptime_timestamp_sensor.cpp b/esphome/components/uptime/sensor/uptime_timestamp_sensor.cpp index 69033be11c..4e0f06be1c 100644 --- a/esphome/components/uptime/sensor/uptime_timestamp_sensor.cpp +++ b/esphome/components/uptime/sensor/uptime_timestamp_sensor.cpp @@ -6,8 +6,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace uptime { +namespace esphome::uptime { static const char *const TAG = "uptime.sensor"; @@ -33,7 +32,6 @@ void UptimeTimestampSensor::dump_config() { ESP_LOGCONFIG(TAG, " Type: Timestamp"); } -} // namespace uptime -} // namespace esphome +} // namespace esphome::uptime #endif // USE_TIME diff --git a/esphome/components/uptime/sensor/uptime_timestamp_sensor.h b/esphome/components/uptime/sensor/uptime_timestamp_sensor.h index f38b5d53b4..912c0b7655 100644 --- a/esphome/components/uptime/sensor/uptime_timestamp_sensor.h +++ b/esphome/components/uptime/sensor/uptime_timestamp_sensor.h @@ -8,8 +8,7 @@ #include "esphome/components/time/real_time_clock.h" #include "esphome/core/component.h" -namespace esphome { -namespace uptime { +namespace esphome::uptime { class UptimeTimestampSensor : public sensor::Sensor, public Component { public: @@ -24,7 +23,6 @@ class UptimeTimestampSensor : public sensor::Sensor, public Component { time::RealTimeClock *time_; }; -} // namespace uptime -} // namespace esphome +} // namespace esphome::uptime #endif // USE_TIME diff --git a/esphome/components/uptime/text_sensor/uptime_text_sensor.cpp b/esphome/components/uptime/text_sensor/uptime_text_sensor.cpp index acd3980a1a..88ae53fbfc 100644 --- a/esphome/components/uptime/text_sensor/uptime_text_sensor.cpp +++ b/esphome/components/uptime/text_sensor/uptime_text_sensor.cpp @@ -1,11 +1,10 @@ #include "uptime_text_sensor.h" -#include "esphome/core/hal.h" +#include "esphome/core/application.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace uptime { +namespace esphome::uptime { static const char *const TAG = "uptime.sensor"; @@ -17,22 +16,10 @@ static void append_unit(char *buf, size_t buf_size, size_t &pos, const char *sep pos = buf_append_printf(buf, buf_size, pos, "%u%s", value, label); } -void UptimeTextSensor::setup() { - this->last_ms_ = millis(); - if (this->last_ms_ < 60 * 1000) - this->last_ms_ = 0; - this->update(); -} +void UptimeTextSensor::setup() { this->update(); } void UptimeTextSensor::update() { - auto now = millis(); - // get whole seconds since last update. Note that even if the millis count has overflowed between updates, - // the difference will still be correct due to the way twos-complement arithmetic works. - uint32_t delta = now - this->last_ms_; - this->last_ms_ = now - delta % 1000; // save remainder for next update - delta /= 1000; - this->uptime_ += delta; - uint32_t uptime = this->uptime_; + uint32_t uptime = static_cast(App.scheduler.millis_64() / 1000); unsigned interval = this->get_update_interval() / 1000; // Calculate all time units @@ -89,5 +76,4 @@ void UptimeTextSensor::update() { float UptimeTextSensor::get_setup_priority() const { return setup_priority::HARDWARE; } void UptimeTextSensor::dump_config() { LOG_TEXT_SENSOR("", "Uptime Text Sensor", this); } -} // namespace uptime -} // namespace esphome +} // namespace esphome::uptime diff --git a/esphome/components/uptime/text_sensor/uptime_text_sensor.h b/esphome/components/uptime/text_sensor/uptime_text_sensor.h index 947d9c91e9..a97ba332bb 100644 --- a/esphome/components/uptime/text_sensor/uptime_text_sensor.h +++ b/esphome/components/uptime/text_sensor/uptime_text_sensor.h @@ -5,8 +5,7 @@ #include "esphome/components/text_sensor/text_sensor.h" #include "esphome/core/component.h" -namespace esphome { -namespace uptime { +namespace esphome::uptime { class UptimeTextSensor : public text_sensor::TextSensor, public PollingComponent { public: @@ -35,9 +34,6 @@ class UptimeTextSensor : public text_sensor::TextSensor, public PollingComponent const char *seconds_text_; const char *separator_; bool expand_{}; - uint32_t uptime_{0}; // uptime in seconds, will overflow after 136 years - uint32_t last_ms_{0}; }; -} // namespace uptime -} // namespace esphome +} // namespace esphome::uptime diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index 3294f689e8..e82efcc520 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -675,6 +675,8 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, NameType name_type return total_cancelled > 0; } +uint64_t Scheduler::millis_64() { return this->millis_64_(millis()); } + uint64_t Scheduler::millis_64_(uint32_t now) { // THREAD SAFETY NOTE: // This function has three implementations, based on the precompiler flags diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index 394178a831..afe11aaca6 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -116,6 +116,9 @@ class Scheduler { ESPDEPRECATED("cancel_retry is deprecated and will be removed in 2026.8.0.", "2026.2.0") bool cancel_retry(Component *component, uint32_t id); + /// Get 64-bit millisecond timestamp (handles 32-bit millis() rollover) + uint64_t millis_64(); + // Calculate when the next scheduled item should run // @param now Fresh timestamp from millis() - must not be stale/cached // Returns the time in milliseconds until the next scheduled item, or nullopt if no items From f8f98bf428e1db199f4b5bf79a4b3362b7d5672d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 20 Feb 2026 21:16:49 -0600 Subject: [PATCH 162/197] [logger] Reduce UART driver heap waste on ESP32 (#14168) --- esphome/components/logger/logger_esp32.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/esphome/components/logger/logger_esp32.cpp b/esphome/components/logger/logger_esp32.cpp index dfa643d5e9..9d64771aec 100644 --- a/esphome/components/logger/logger_esp32.cpp +++ b/esphome/components/logger/logger_esp32.cpp @@ -77,9 +77,10 @@ void init_uart(uart_port_t uart_num, uint32_t baud_rate, int tx_buffer_size) { uart_config.flow_ctrl = UART_HW_FLOWCTRL_DISABLE; uart_config.source_clk = UART_SCLK_DEFAULT; uart_param_config(uart_num, &uart_config); - const int uart_buffer_size = tx_buffer_size; - // Install UART driver using an event queue here - uart_driver_install(uart_num, uart_buffer_size, uart_buffer_size, 10, nullptr, 0); + // The logger only writes to UART, never reads, so use the minimum RX buffer. + // ESP-IDF requires rx_buffer_size > UART_HW_FIFO_LEN (128 bytes). + const int min_rx_buffer_size = UART_HW_FIFO_LEN(uart_num) + 1; + uart_driver_install(uart_num, min_rx_buffer_size, tx_buffer_size, 0, nullptr, 0); } void Logger::pre_setup() { From f77da803c9b316c78a51e602563dcd6d916682f7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 20 Feb 2026 21:39:18 -0600 Subject: [PATCH 163/197] [api] Write protobuf encode output to pre-sized buffer directly (#14018) --- esphome/components/api/api_connection.cpp | 35 ++--- esphome/components/api/api_pb2.cpp | 180 +++++++++++----------- esphome/components/api/api_pb2.h | 176 ++++++++++----------- esphome/components/api/proto.cpp | 15 ++ esphome/components/api/proto.h | 108 ++++++++----- script/api_protobuf/api_protobuf.py | 12 +- 6 files changed, 286 insertions(+), 240 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 5b02bee537..9fc263abbd 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -347,9 +347,7 @@ uint16_t APIConnection::encode_message_to_buffer(ProtoMessage &msg, uint8_t mess #endif // Calculate size - ProtoSize size_calc; - msg.calculate_size(size_calc); - uint32_t calculated_size = size_calc.get_size(); + uint32_t calculated_size = msg.calculated_size(); // Cache frame sizes to avoid repeated virtual calls const uint8_t header_padding = conn->helper_->frame_header_padding(); @@ -377,19 +375,14 @@ uint16_t APIConnection::encode_message_to_buffer(ProtoMessage &msg, uint8_t mess shared_buf.resize(current_size + footer_size + header_padding); } - // Encode directly into buffer - size_t size_before_encode = shared_buf.size(); - msg.encode({&shared_buf}); + // Pre-resize buffer to include payload, then encode through raw pointer + size_t write_start = shared_buf.size(); + shared_buf.resize(write_start + calculated_size); + ProtoWriteBuffer buffer{&shared_buf, write_start}; + msg.encode(buffer); - // Calculate actual encoded size (not including header that was already added) - size_t actual_payload_size = shared_buf.size() - size_before_encode; - - // Return actual total size (header + actual payload + footer) - size_t actual_total_size = header_padding + actual_payload_size + footer_size; - - // Verify that calculate_size() returned the correct value - assert(calculated_size == actual_payload_size); - return static_cast(actual_total_size); + // Return total size (header + payload + footer) + return static_cast(header_padding + calculated_size + footer_size); } #ifdef USE_BINARY_SENSOR @@ -1854,12 +1847,14 @@ bool APIConnection::try_to_clear_buffer(bool log_out_of_space) { return false; } bool APIConnection::send_message_impl(const ProtoMessage &msg, uint8_t message_type) { - ProtoSize size; - msg.calculate_size(size); + uint32_t payload_size = msg.calculated_size(); std::vector &shared_buf = this->parent_->get_shared_buffer_ref(); - this->prepare_first_message_buffer(shared_buf, size.get_size()); - msg.encode({&shared_buf}); - return this->send_buffer({&shared_buf}, message_type); + this->prepare_first_message_buffer(shared_buf, payload_size); + size_t write_start = shared_buf.size(); + shared_buf.resize(write_start + payload_size); + ProtoWriteBuffer buffer{&shared_buf, write_start}; + msg.encode(buffer); + return this->send_buffer(ProtoWriteBuffer{&shared_buf}, message_type); } bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) { const bool is_log_message = (message_type == SubscribeLogsResponse::MESSAGE_TYPE); diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 743f51dac7..5c50a8aa5b 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -31,7 +31,7 @@ bool HelloRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) } return true; } -void HelloResponse::encode(ProtoWriteBuffer buffer) const { +void HelloResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(1, this->api_version_major); buffer.encode_uint32(2, this->api_version_minor); buffer.encode_string(3, this->server_info); @@ -44,7 +44,7 @@ void HelloResponse::calculate_size(ProtoSize &size) const { size.add_length(1, this->name.size()); } #ifdef USE_AREAS -void AreaInfo::encode(ProtoWriteBuffer buffer) const { +void AreaInfo::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(1, this->area_id); buffer.encode_string(2, this->name); } @@ -54,7 +54,7 @@ void AreaInfo::calculate_size(ProtoSize &size) const { } #endif #ifdef USE_DEVICES -void DeviceInfo::encode(ProtoWriteBuffer buffer) const { +void DeviceInfo::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(1, this->device_id); buffer.encode_string(2, this->name); buffer.encode_uint32(3, this->area_id); @@ -65,7 +65,7 @@ void DeviceInfo::calculate_size(ProtoSize &size) const { size.add_uint32(1, this->area_id); } #endif -void DeviceInfoResponse::encode(ProtoWriteBuffer buffer) const { +void DeviceInfoResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(2, this->name); buffer.encode_string(3, this->mac_address); buffer.encode_string(4, this->esphome_version); @@ -111,7 +111,7 @@ void DeviceInfoResponse::encode(ProtoWriteBuffer buffer) const { } #endif #ifdef USE_AREAS - buffer.encode_message(22, this->area); + buffer.encode_message(22, this->area, false); #endif #ifdef USE_ZWAVE_PROXY buffer.encode_uint32(23, this->zwave_proxy_feature_flags); @@ -176,7 +176,7 @@ void DeviceInfoResponse::calculate_size(ProtoSize &size) const { #endif } #ifdef USE_BINARY_SENSOR -void ListEntitiesBinarySensorResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesBinarySensorResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); @@ -206,7 +206,7 @@ void ListEntitiesBinarySensorResponse::calculate_size(ProtoSize &size) const { size.add_uint32(1, this->device_id); #endif } -void BinarySensorStateResponse::encode(ProtoWriteBuffer buffer) const { +void BinarySensorStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_bool(2, this->state); buffer.encode_bool(3, this->missing_state); @@ -224,7 +224,7 @@ void BinarySensorStateResponse::calculate_size(ProtoSize &size) const { } #endif #ifdef USE_COVER -void ListEntitiesCoverResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesCoverResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); @@ -260,7 +260,7 @@ void ListEntitiesCoverResponse::calculate_size(ProtoSize &size) const { size.add_uint32(1, this->device_id); #endif } -void CoverStateResponse::encode(ProtoWriteBuffer buffer) const { +void CoverStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_float(3, this->position); buffer.encode_float(4, this->tilt); @@ -317,7 +317,7 @@ bool CoverCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } #endif #ifdef USE_FAN -void ListEntitiesFanResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesFanResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); @@ -359,7 +359,7 @@ void ListEntitiesFanResponse::calculate_size(ProtoSize &size) const { size.add_uint32(1, this->device_id); #endif } -void FanStateResponse::encode(ProtoWriteBuffer buffer) const { +void FanStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_bool(2, this->state); buffer.encode_bool(3, this->oscillating); @@ -443,7 +443,7 @@ bool FanCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } #endif #ifdef USE_LIGHT -void ListEntitiesLightResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesLightResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); @@ -489,7 +489,7 @@ void ListEntitiesLightResponse::calculate_size(ProtoSize &size) const { size.add_uint32(2, this->device_id); #endif } -void LightStateResponse::encode(ProtoWriteBuffer buffer) const { +void LightStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_bool(2, this->state); buffer.encode_float(3, this->brightness); @@ -635,7 +635,7 @@ bool LightCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } #endif #ifdef USE_SENSOR -void ListEntitiesSensorResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesSensorResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); @@ -671,7 +671,7 @@ void ListEntitiesSensorResponse::calculate_size(ProtoSize &size) const { size.add_uint32(1, this->device_id); #endif } -void SensorStateResponse::encode(ProtoWriteBuffer buffer) const { +void SensorStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_float(2, this->state); buffer.encode_bool(3, this->missing_state); @@ -689,7 +689,7 @@ void SensorStateResponse::calculate_size(ProtoSize &size) const { } #endif #ifdef USE_SWITCH -void ListEntitiesSwitchResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesSwitchResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); @@ -719,7 +719,7 @@ void ListEntitiesSwitchResponse::calculate_size(ProtoSize &size) const { size.add_uint32(1, this->device_id); #endif } -void SwitchStateResponse::encode(ProtoWriteBuffer buffer) const { +void SwitchStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_bool(2, this->state); #ifdef USE_DEVICES @@ -760,7 +760,7 @@ bool SwitchCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } #endif #ifdef USE_TEXT_SENSOR -void ListEntitiesTextSensorResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesTextSensorResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); @@ -788,7 +788,7 @@ void ListEntitiesTextSensorResponse::calculate_size(ProtoSize &size) const { size.add_uint32(1, this->device_id); #endif } -void TextSensorStateResponse::encode(ProtoWriteBuffer buffer) const { +void TextSensorStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_string(2, this->state); buffer.encode_bool(3, this->missing_state); @@ -818,7 +818,7 @@ bool SubscribeLogsRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { } return true; } -void SubscribeLogsResponse::encode(ProtoWriteBuffer buffer) const { +void SubscribeLogsResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(1, static_cast(this->level)); buffer.encode_bytes(3, this->message_ptr_, this->message_len_); } @@ -839,11 +839,11 @@ bool NoiseEncryptionSetKeyRequest::decode_length(uint32_t field_id, ProtoLengthD } return true; } -void NoiseEncryptionSetKeyResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(1, this->success); } +void NoiseEncryptionSetKeyResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_bool(1, this->success); } void NoiseEncryptionSetKeyResponse::calculate_size(ProtoSize &size) const { size.add_bool(1, this->success); } #endif #ifdef USE_API_HOMEASSISTANT_SERVICES -void HomeassistantServiceMap::encode(ProtoWriteBuffer buffer) const { +void HomeassistantServiceMap::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->key); buffer.encode_string(2, this->value); } @@ -851,7 +851,7 @@ void HomeassistantServiceMap::calculate_size(ProtoSize &size) const { size.add_length(1, this->key.size()); size.add_length(1, this->value.size()); } -void HomeassistantActionRequest::encode(ProtoWriteBuffer buffer) const { +void HomeassistantActionRequest::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->service); for (auto &it : this->data) { buffer.encode_message(2, it); @@ -924,7 +924,7 @@ bool HomeassistantActionResponse::decode_length(uint32_t field_id, ProtoLengthDe } #endif #ifdef USE_API_HOMEASSISTANT_STATES -void SubscribeHomeAssistantStateResponse::encode(ProtoWriteBuffer buffer) const { +void SubscribeHomeAssistantStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->entity_id); buffer.encode_string(2, this->attribute); buffer.encode_bool(3, this->once); @@ -976,7 +976,7 @@ bool GetTimeResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { return true; } #ifdef USE_API_USER_DEFINED_ACTIONS -void ListEntitiesServicesArgument::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesServicesArgument::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->name); buffer.encode_uint32(2, static_cast(this->type)); } @@ -984,7 +984,7 @@ void ListEntitiesServicesArgument::calculate_size(ProtoSize &size) const { size.add_length(1, this->name.size()); size.add_uint32(1, static_cast(this->type)); } -void ListEntitiesServicesResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesServicesResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->name); buffer.encode_fixed32(2, this->key); for (auto &it : this->args) { @@ -1103,7 +1103,7 @@ void ExecuteServiceRequest::decode(const uint8_t *buffer, size_t length) { } #endif #ifdef USE_API_USER_DEFINED_ACTION_RESPONSES -void ExecuteServiceResponse::encode(ProtoWriteBuffer buffer) const { +void ExecuteServiceResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(1, this->call_id); buffer.encode_bool(2, this->success); buffer.encode_string(3, this->error_message); @@ -1121,7 +1121,7 @@ void ExecuteServiceResponse::calculate_size(ProtoSize &size) const { } #endif #ifdef USE_CAMERA -void ListEntitiesCameraResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesCameraResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); @@ -1147,7 +1147,7 @@ void ListEntitiesCameraResponse::calculate_size(ProtoSize &size) const { size.add_uint32(1, this->device_id); #endif } -void CameraImageResponse::encode(ProtoWriteBuffer buffer) const { +void CameraImageResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_bytes(2, this->data_ptr_, this->data_len_); buffer.encode_bool(3, this->done); @@ -1178,7 +1178,7 @@ bool CameraImageRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { } #endif #ifdef USE_CLIMATE -void ListEntitiesClimateResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesClimateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); @@ -1276,7 +1276,7 @@ void ListEntitiesClimateResponse::calculate_size(ProtoSize &size) const { #endif size.add_uint32(2, this->feature_flags); } -void ClimateStateResponse::encode(ProtoWriteBuffer buffer) const { +void ClimateStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_uint32(2, static_cast(this->mode)); buffer.encode_float(3, this->current_temperature); @@ -1407,7 +1407,7 @@ bool ClimateCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } #endif #ifdef USE_WATER_HEATER -void ListEntitiesWaterHeaterResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesWaterHeaterResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); @@ -1449,7 +1449,7 @@ void ListEntitiesWaterHeaterResponse::calculate_size(ProtoSize &size) const { } size.add_uint32(1, this->supported_features); } -void WaterHeaterStateResponse::encode(ProtoWriteBuffer buffer) const { +void WaterHeaterStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_float(2, this->current_temperature); buffer.encode_float(3, this->target_temperature); @@ -1515,7 +1515,7 @@ bool WaterHeaterCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value } #endif #ifdef USE_NUMBER -void ListEntitiesNumberResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesNumberResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); @@ -1553,7 +1553,7 @@ void ListEntitiesNumberResponse::calculate_size(ProtoSize &size) const { size.add_uint32(1, this->device_id); #endif } -void NumberStateResponse::encode(ProtoWriteBuffer buffer) const { +void NumberStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_float(2, this->state); buffer.encode_bool(3, this->missing_state); @@ -1596,7 +1596,7 @@ bool NumberCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } #endif #ifdef USE_SELECT -void ListEntitiesSelectResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesSelectResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); @@ -1630,7 +1630,7 @@ void ListEntitiesSelectResponse::calculate_size(ProtoSize &size) const { size.add_uint32(1, this->device_id); #endif } -void SelectStateResponse::encode(ProtoWriteBuffer buffer) const { +void SelectStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_string(2, this->state); buffer.encode_bool(3, this->missing_state); @@ -1681,7 +1681,7 @@ bool SelectCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } #endif #ifdef USE_SIREN -void ListEntitiesSirenResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesSirenResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); @@ -1719,7 +1719,7 @@ void ListEntitiesSirenResponse::calculate_size(ProtoSize &size) const { size.add_uint32(1, this->device_id); #endif } -void SirenStateResponse::encode(ProtoWriteBuffer buffer) const { +void SirenStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_bool(2, this->state); #ifdef USE_DEVICES @@ -1789,7 +1789,7 @@ bool SirenCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } #endif #ifdef USE_LOCK -void ListEntitiesLockResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesLockResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); @@ -1823,7 +1823,7 @@ void ListEntitiesLockResponse::calculate_size(ProtoSize &size) const { size.add_uint32(1, this->device_id); #endif } -void LockStateResponse::encode(ProtoWriteBuffer buffer) const { +void LockStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_uint32(2, static_cast(this->state)); #ifdef USE_DEVICES @@ -1878,7 +1878,7 @@ bool LockCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } #endif #ifdef USE_BUTTON -void ListEntitiesButtonResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesButtonResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); @@ -1930,7 +1930,7 @@ bool ButtonCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } #endif #ifdef USE_MEDIA_PLAYER -void MediaPlayerSupportedFormat::encode(ProtoWriteBuffer buffer) const { +void MediaPlayerSupportedFormat::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->format); buffer.encode_uint32(2, this->sample_rate); buffer.encode_uint32(3, this->num_channels); @@ -1944,7 +1944,7 @@ void MediaPlayerSupportedFormat::calculate_size(ProtoSize &size) const { size.add_uint32(1, static_cast(this->purpose)); size.add_uint32(1, this->sample_bytes); } -void ListEntitiesMediaPlayerResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesMediaPlayerResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); @@ -1978,7 +1978,7 @@ void ListEntitiesMediaPlayerResponse::calculate_size(ProtoSize &size) const { #endif size.add_uint32(1, this->feature_flags); } -void MediaPlayerStateResponse::encode(ProtoWriteBuffer buffer) const { +void MediaPlayerStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_uint32(2, static_cast(this->state)); buffer.encode_float(3, this->volume); @@ -2062,7 +2062,7 @@ bool SubscribeBluetoothLEAdvertisementsRequest::decode_varint(uint32_t field_id, } return true; } -void BluetoothLERawAdvertisement::encode(ProtoWriteBuffer buffer) const { +void BluetoothLERawAdvertisement::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint64(1, this->address); buffer.encode_sint32(2, this->rssi); buffer.encode_uint32(3, this->address_type); @@ -2074,7 +2074,7 @@ void BluetoothLERawAdvertisement::calculate_size(ProtoSize &size) const { size.add_uint32(1, this->address_type); size.add_length(1, this->data_len); } -void BluetoothLERawAdvertisementsResponse::encode(ProtoWriteBuffer buffer) const { +void BluetoothLERawAdvertisementsResponse::encode(ProtoWriteBuffer &buffer) const { for (uint16_t i = 0; i < this->advertisements_len; i++) { buffer.encode_message(1, this->advertisements[i]); } @@ -2103,7 +2103,7 @@ bool BluetoothDeviceRequest::decode_varint(uint32_t field_id, ProtoVarInt value) } return true; } -void BluetoothDeviceConnectionResponse::encode(ProtoWriteBuffer buffer) const { +void BluetoothDeviceConnectionResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint64(1, this->address); buffer.encode_bool(2, this->connected); buffer.encode_uint32(3, this->mtu); @@ -2125,7 +2125,7 @@ bool BluetoothGATTGetServicesRequest::decode_varint(uint32_t field_id, ProtoVarI } return true; } -void BluetoothGATTDescriptor::encode(ProtoWriteBuffer buffer) const { +void BluetoothGATTDescriptor::encode(ProtoWriteBuffer &buffer) const { if (this->uuid[0] != 0 || this->uuid[1] != 0) { buffer.encode_uint64(1, this->uuid[0], true); buffer.encode_uint64(1, this->uuid[1], true); @@ -2141,7 +2141,7 @@ void BluetoothGATTDescriptor::calculate_size(ProtoSize &size) const { size.add_uint32(1, this->handle); size.add_uint32(1, this->short_uuid); } -void BluetoothGATTCharacteristic::encode(ProtoWriteBuffer buffer) const { +void BluetoothGATTCharacteristic::encode(ProtoWriteBuffer &buffer) const { if (this->uuid[0] != 0 || this->uuid[1] != 0) { buffer.encode_uint64(1, this->uuid[0], true); buffer.encode_uint64(1, this->uuid[1], true); @@ -2163,7 +2163,7 @@ void BluetoothGATTCharacteristic::calculate_size(ProtoSize &size) const { size.add_repeated_message(1, this->descriptors); size.add_uint32(1, this->short_uuid); } -void BluetoothGATTService::encode(ProtoWriteBuffer buffer) const { +void BluetoothGATTService::encode(ProtoWriteBuffer &buffer) const { if (this->uuid[0] != 0 || this->uuid[1] != 0) { buffer.encode_uint64(1, this->uuid[0], true); buffer.encode_uint64(1, this->uuid[1], true); @@ -2183,7 +2183,7 @@ void BluetoothGATTService::calculate_size(ProtoSize &size) const { size.add_repeated_message(1, this->characteristics); size.add_uint32(1, this->short_uuid); } -void BluetoothGATTGetServicesResponse::encode(ProtoWriteBuffer buffer) const { +void BluetoothGATTGetServicesResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint64(1, this->address); for (auto &it : this->services) { buffer.encode_message(2, it); @@ -2193,7 +2193,7 @@ void BluetoothGATTGetServicesResponse::calculate_size(ProtoSize &size) const { size.add_uint64(1, this->address); size.add_repeated_message(1, this->services); } -void BluetoothGATTGetServicesDoneResponse::encode(ProtoWriteBuffer buffer) const { +void BluetoothGATTGetServicesDoneResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint64(1, this->address); } void BluetoothGATTGetServicesDoneResponse::calculate_size(ProtoSize &size) const { size.add_uint64(1, this->address); } @@ -2210,7 +2210,7 @@ bool BluetoothGATTReadRequest::decode_varint(uint32_t field_id, ProtoVarInt valu } return true; } -void BluetoothGATTReadResponse::encode(ProtoWriteBuffer buffer) const { +void BluetoothGATTReadResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint64(1, this->address); buffer.encode_uint32(2, this->handle); buffer.encode_bytes(3, this->data_ptr_, this->data_len_); @@ -2302,7 +2302,7 @@ bool BluetoothGATTNotifyRequest::decode_varint(uint32_t field_id, ProtoVarInt va } return true; } -void BluetoothGATTNotifyDataResponse::encode(ProtoWriteBuffer buffer) const { +void BluetoothGATTNotifyDataResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint64(1, this->address); buffer.encode_uint32(2, this->handle); buffer.encode_bytes(3, this->data_ptr_, this->data_len_); @@ -2312,7 +2312,7 @@ void BluetoothGATTNotifyDataResponse::calculate_size(ProtoSize &size) const { size.add_uint32(1, this->handle); size.add_length(1, this->data_len_); } -void BluetoothConnectionsFreeResponse::encode(ProtoWriteBuffer buffer) const { +void BluetoothConnectionsFreeResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(1, this->free); buffer.encode_uint32(2, this->limit); for (const auto &it : this->allocated) { @@ -2330,7 +2330,7 @@ void BluetoothConnectionsFreeResponse::calculate_size(ProtoSize &size) const { } } } -void BluetoothGATTErrorResponse::encode(ProtoWriteBuffer buffer) const { +void BluetoothGATTErrorResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint64(1, this->address); buffer.encode_uint32(2, this->handle); buffer.encode_int32(3, this->error); @@ -2340,7 +2340,7 @@ void BluetoothGATTErrorResponse::calculate_size(ProtoSize &size) const { size.add_uint32(1, this->handle); size.add_int32(1, this->error); } -void BluetoothGATTWriteResponse::encode(ProtoWriteBuffer buffer) const { +void BluetoothGATTWriteResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint64(1, this->address); buffer.encode_uint32(2, this->handle); } @@ -2348,7 +2348,7 @@ void BluetoothGATTWriteResponse::calculate_size(ProtoSize &size) const { size.add_uint64(1, this->address); size.add_uint32(1, this->handle); } -void BluetoothGATTNotifyResponse::encode(ProtoWriteBuffer buffer) const { +void BluetoothGATTNotifyResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint64(1, this->address); buffer.encode_uint32(2, this->handle); } @@ -2356,7 +2356,7 @@ void BluetoothGATTNotifyResponse::calculate_size(ProtoSize &size) const { size.add_uint64(1, this->address); size.add_uint32(1, this->handle); } -void BluetoothDevicePairingResponse::encode(ProtoWriteBuffer buffer) const { +void BluetoothDevicePairingResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint64(1, this->address); buffer.encode_bool(2, this->paired); buffer.encode_int32(3, this->error); @@ -2366,7 +2366,7 @@ void BluetoothDevicePairingResponse::calculate_size(ProtoSize &size) const { size.add_bool(1, this->paired); size.add_int32(1, this->error); } -void BluetoothDeviceUnpairingResponse::encode(ProtoWriteBuffer buffer) const { +void BluetoothDeviceUnpairingResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint64(1, this->address); buffer.encode_bool(2, this->success); buffer.encode_int32(3, this->error); @@ -2376,7 +2376,7 @@ void BluetoothDeviceUnpairingResponse::calculate_size(ProtoSize &size) const { size.add_bool(1, this->success); size.add_int32(1, this->error); } -void BluetoothDeviceClearCacheResponse::encode(ProtoWriteBuffer buffer) const { +void BluetoothDeviceClearCacheResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint64(1, this->address); buffer.encode_bool(2, this->success); buffer.encode_int32(3, this->error); @@ -2386,7 +2386,7 @@ void BluetoothDeviceClearCacheResponse::calculate_size(ProtoSize &size) const { size.add_bool(1, this->success); size.add_int32(1, this->error); } -void BluetoothScannerStateResponse::encode(ProtoWriteBuffer buffer) const { +void BluetoothScannerStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(1, static_cast(this->state)); buffer.encode_uint32(2, static_cast(this->mode)); buffer.encode_uint32(3, static_cast(this->configured_mode)); @@ -2421,7 +2421,7 @@ bool SubscribeVoiceAssistantRequest::decode_varint(uint32_t field_id, ProtoVarIn } return true; } -void VoiceAssistantAudioSettings::encode(ProtoWriteBuffer buffer) const { +void VoiceAssistantAudioSettings::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(1, this->noise_suppression_level); buffer.encode_uint32(2, this->auto_gain); buffer.encode_float(3, this->volume_multiplier); @@ -2431,11 +2431,11 @@ void VoiceAssistantAudioSettings::calculate_size(ProtoSize &size) const { size.add_uint32(1, this->auto_gain); size.add_float(1, this->volume_multiplier); } -void VoiceAssistantRequest::encode(ProtoWriteBuffer buffer) const { +void VoiceAssistantRequest::encode(ProtoWriteBuffer &buffer) const { buffer.encode_bool(1, this->start); buffer.encode_string(2, this->conversation_id); buffer.encode_uint32(3, this->flags); - buffer.encode_message(4, this->audio_settings); + buffer.encode_message(4, this->audio_settings, false); buffer.encode_string(5, this->wake_word_phrase); } void VoiceAssistantRequest::calculate_size(ProtoSize &size) const { @@ -2516,7 +2516,7 @@ bool VoiceAssistantAudio::decode_length(uint32_t field_id, ProtoLengthDelimited } return true; } -void VoiceAssistantAudio::encode(ProtoWriteBuffer buffer) const { +void VoiceAssistantAudio::encode(ProtoWriteBuffer &buffer) const { buffer.encode_bytes(1, this->data, this->data_len); buffer.encode_bool(2, this->end); } @@ -2587,9 +2587,9 @@ bool VoiceAssistantAnnounceRequest::decode_length(uint32_t field_id, ProtoLength } return true; } -void VoiceAssistantAnnounceFinished::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(1, this->success); } +void VoiceAssistantAnnounceFinished::encode(ProtoWriteBuffer &buffer) const { buffer.encode_bool(1, this->success); } void VoiceAssistantAnnounceFinished::calculate_size(ProtoSize &size) const { size.add_bool(1, this->success); } -void VoiceAssistantWakeWord::encode(ProtoWriteBuffer buffer) const { +void VoiceAssistantWakeWord::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->id); buffer.encode_string(2, this->wake_word); for (auto &it : this->trained_languages) { @@ -2656,7 +2656,7 @@ bool VoiceAssistantConfigurationRequest::decode_length(uint32_t field_id, ProtoL } return true; } -void VoiceAssistantConfigurationResponse::encode(ProtoWriteBuffer buffer) const { +void VoiceAssistantConfigurationResponse::encode(ProtoWriteBuffer &buffer) const { for (auto &it : this->available_wake_words) { buffer.encode_message(1, it); } @@ -2686,7 +2686,7 @@ bool VoiceAssistantSetConfiguration::decode_length(uint32_t field_id, ProtoLengt } #endif #ifdef USE_ALARM_CONTROL_PANEL -void ListEntitiesAlarmControlPanelResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesAlarmControlPanelResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); @@ -2718,7 +2718,7 @@ void ListEntitiesAlarmControlPanelResponse::calculate_size(ProtoSize &size) cons size.add_uint32(1, this->device_id); #endif } -void AlarmControlPanelStateResponse::encode(ProtoWriteBuffer buffer) const { +void AlarmControlPanelStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_uint32(2, static_cast(this->state)); #ifdef USE_DEVICES @@ -2770,7 +2770,7 @@ bool AlarmControlPanelCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit } #endif #ifdef USE_TEXT -void ListEntitiesTextResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesTextResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); @@ -2804,7 +2804,7 @@ void ListEntitiesTextResponse::calculate_size(ProtoSize &size) const { size.add_uint32(1, this->device_id); #endif } -void TextStateResponse::encode(ProtoWriteBuffer buffer) const { +void TextStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_string(2, this->state); buffer.encode_bool(3, this->missing_state); @@ -2855,7 +2855,7 @@ bool TextCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } #endif #ifdef USE_DATETIME_DATE -void ListEntitiesDateResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesDateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); @@ -2881,7 +2881,7 @@ void ListEntitiesDateResponse::calculate_size(ProtoSize &size) const { size.add_uint32(1, this->device_id); #endif } -void DateStateResponse::encode(ProtoWriteBuffer buffer) const { +void DateStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_bool(2, this->missing_state); buffer.encode_uint32(3, this->year); @@ -2934,7 +2934,7 @@ bool DateCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } #endif #ifdef USE_DATETIME_TIME -void ListEntitiesTimeResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesTimeResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); @@ -2960,7 +2960,7 @@ void ListEntitiesTimeResponse::calculate_size(ProtoSize &size) const { size.add_uint32(1, this->device_id); #endif } -void TimeStateResponse::encode(ProtoWriteBuffer buffer) const { +void TimeStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_bool(2, this->missing_state); buffer.encode_uint32(3, this->hour); @@ -3013,7 +3013,7 @@ bool TimeCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } #endif #ifdef USE_EVENT -void ListEntitiesEventResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesEventResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); @@ -3049,7 +3049,7 @@ void ListEntitiesEventResponse::calculate_size(ProtoSize &size) const { size.add_uint32(1, this->device_id); #endif } -void EventResponse::encode(ProtoWriteBuffer buffer) const { +void EventResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_string(2, this->event_type); #ifdef USE_DEVICES @@ -3065,7 +3065,7 @@ void EventResponse::calculate_size(ProtoSize &size) const { } #endif #ifdef USE_VALVE -void ListEntitiesValveResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesValveResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); @@ -3099,7 +3099,7 @@ void ListEntitiesValveResponse::calculate_size(ProtoSize &size) const { size.add_uint32(1, this->device_id); #endif } -void ValveStateResponse::encode(ProtoWriteBuffer buffer) const { +void ValveStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_float(2, this->position); buffer.encode_uint32(3, static_cast(this->current_operation)); @@ -3148,7 +3148,7 @@ bool ValveCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } #endif #ifdef USE_DATETIME_DATETIME -void ListEntitiesDateTimeResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesDateTimeResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); @@ -3174,7 +3174,7 @@ void ListEntitiesDateTimeResponse::calculate_size(ProtoSize &size) const { size.add_uint32(1, this->device_id); #endif } -void DateTimeStateResponse::encode(ProtoWriteBuffer buffer) const { +void DateTimeStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_bool(2, this->missing_state); buffer.encode_fixed32(3, this->epoch_seconds); @@ -3217,7 +3217,7 @@ bool DateTimeCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } #endif #ifdef USE_UPDATE -void ListEntitiesUpdateResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesUpdateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); @@ -3245,7 +3245,7 @@ void ListEntitiesUpdateResponse::calculate_size(ProtoSize &size) const { size.add_uint32(1, this->device_id); #endif } -void UpdateStateResponse::encode(ProtoWriteBuffer buffer) const { +void UpdateStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_bool(2, this->missing_state); buffer.encode_bool(3, this->in_progress); @@ -3314,7 +3314,7 @@ bool ZWaveProxyFrame::decode_length(uint32_t field_id, ProtoLengthDelimited valu } return true; } -void ZWaveProxyFrame::encode(ProtoWriteBuffer buffer) const { buffer.encode_bytes(1, this->data, this->data_len); } +void ZWaveProxyFrame::encode(ProtoWriteBuffer &buffer) const { buffer.encode_bytes(1, this->data, this->data_len); } void ZWaveProxyFrame::calculate_size(ProtoSize &size) const { size.add_length(1, this->data_len); } bool ZWaveProxyRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { @@ -3338,7 +3338,7 @@ bool ZWaveProxyRequest::decode_length(uint32_t field_id, ProtoLengthDelimited va } return true; } -void ZWaveProxyRequest::encode(ProtoWriteBuffer buffer) const { +void ZWaveProxyRequest::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(1, static_cast(this->type)); buffer.encode_bytes(2, this->data, this->data_len); } @@ -3348,7 +3348,7 @@ void ZWaveProxyRequest::calculate_size(ProtoSize &size) const { } #endif #ifdef USE_INFRARED -void ListEntitiesInfraredResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesInfraredResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); @@ -3419,7 +3419,7 @@ bool InfraredRFTransmitRawTimingsRequest::decode_32bit(uint32_t field_id, Proto3 } return true; } -void InfraredRFReceiveEvent::encode(ProtoWriteBuffer buffer) const { +void InfraredRFReceiveEvent::encode(ProtoWriteBuffer &buffer) const { #ifdef USE_DEVICES buffer.encode_uint32(1, this->device_id); #endif diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index d001f869c5..c90873d993 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -382,7 +382,7 @@ class HelloResponse final : public ProtoMessage { uint32_t api_version_minor{0}; StringRef server_info{}; StringRef name{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -447,7 +447,7 @@ class AreaInfo final : public ProtoMessage { public: uint32_t area_id{0}; StringRef name{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -462,7 +462,7 @@ class DeviceInfo final : public ProtoMessage { uint32_t device_id{0}; StringRef name{}; uint32_t area_id{0}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -527,7 +527,7 @@ class DeviceInfoResponse final : public ProtoMessage { #ifdef USE_ZWAVE_PROXY uint32_t zwave_home_id{0}; #endif - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -558,7 +558,7 @@ class ListEntitiesBinarySensorResponse final : public InfoResponseProtoMessage { #endif StringRef device_class{}; bool is_status_binary_sensor{false}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -575,7 +575,7 @@ class BinarySensorStateResponse final : public StateResponseProtoMessage { #endif bool state{false}; bool missing_state{false}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -597,7 +597,7 @@ class ListEntitiesCoverResponse final : public InfoResponseProtoMessage { bool supports_tilt{false}; StringRef device_class{}; bool supports_stop{false}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -615,7 +615,7 @@ class CoverStateResponse final : public StateResponseProtoMessage { float position{0.0f}; float tilt{0.0f}; enums::CoverOperation current_operation{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -657,7 +657,7 @@ class ListEntitiesFanResponse final : public InfoResponseProtoMessage { bool supports_direction{false}; int32_t supported_speed_count{0}; const std::vector *supported_preset_modes{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -677,7 +677,7 @@ class FanStateResponse final : public StateResponseProtoMessage { enums::FanDirection direction{}; int32_t speed_level{0}; StringRef preset_mode{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -724,7 +724,7 @@ class ListEntitiesLightResponse final : public InfoResponseProtoMessage { float min_mireds{0.0f}; float max_mireds{0.0f}; const FixedVector *effects{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -751,7 +751,7 @@ class LightStateResponse final : public StateResponseProtoMessage { float cold_white{0.0f}; float warm_white{0.0f}; StringRef effect{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -815,7 +815,7 @@ class ListEntitiesSensorResponse final : public InfoResponseProtoMessage { bool force_update{false}; StringRef device_class{}; enums::SensorStateClass state_class{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -832,7 +832,7 @@ class SensorStateResponse final : public StateResponseProtoMessage { #endif float state{0.0f}; bool missing_state{false}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -851,7 +851,7 @@ class ListEntitiesSwitchResponse final : public InfoResponseProtoMessage { #endif bool assumed_state{false}; StringRef device_class{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -867,7 +867,7 @@ class SwitchStateResponse final : public StateResponseProtoMessage { const char *message_name() const override { return "switch_state_response"; } #endif bool state{false}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -901,7 +901,7 @@ class ListEntitiesTextSensorResponse final : public InfoResponseProtoMessage { const char *message_name() const override { return "list_entities_text_sensor_response"; } #endif StringRef device_class{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -918,7 +918,7 @@ class TextSensorStateResponse final : public StateResponseProtoMessage { #endif StringRef state{}; bool missing_state{false}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -957,7 +957,7 @@ class SubscribeLogsResponse final : public ProtoMessage { this->message_ptr_ = data; this->message_len_ = len; } - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -990,7 +990,7 @@ class NoiseEncryptionSetKeyResponse final : public ProtoMessage { const char *message_name() const override { return "noise_encryption_set_key_response"; } #endif bool success{false}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1004,7 +1004,7 @@ class HomeassistantServiceMap final : public ProtoMessage { public: StringRef key{}; StringRef value{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1033,7 +1033,7 @@ class HomeassistantActionRequest final : public ProtoMessage { #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON StringRef response_template{}; #endif - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1077,7 +1077,7 @@ class SubscribeHomeAssistantStateResponse final : public ProtoMessage { StringRef entity_id{}; StringRef attribute{}; bool once{false}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1138,7 +1138,7 @@ class ListEntitiesServicesArgument final : public ProtoMessage { public: StringRef name{}; enums::ServiceArgType type{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1157,7 +1157,7 @@ class ListEntitiesServicesResponse final : public ProtoMessage { uint32_t key{0}; FixedVector args{}; enums::SupportsResponseType supports_response{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1227,7 +1227,7 @@ class ExecuteServiceResponse final : public ProtoMessage { const uint8_t *response_data{nullptr}; uint16_t response_data_len{0}; #endif - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1244,7 +1244,7 @@ class ListEntitiesCameraResponse final : public InfoResponseProtoMessage { #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_camera_response"; } #endif - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1266,7 +1266,7 @@ class CameraImageResponse final : public StateResponseProtoMessage { this->data_len_ = len; } bool done{false}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1317,7 +1317,7 @@ class ListEntitiesClimateResponse final : public InfoResponseProtoMessage { float visual_min_humidity{0.0f}; float visual_max_humidity{0.0f}; uint32_t feature_flags{0}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1345,7 +1345,7 @@ class ClimateStateResponse final : public StateResponseProtoMessage { StringRef custom_preset{}; float current_humidity{0.0f}; float target_humidity{0.0f}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1403,7 +1403,7 @@ class ListEntitiesWaterHeaterResponse final : public InfoResponseProtoMessage { float target_temperature_step{0.0f}; const water_heater::WaterHeaterModeMask *supported_modes{}; uint32_t supported_features{0}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1424,7 +1424,7 @@ class WaterHeaterStateResponse final : public StateResponseProtoMessage { uint32_t state{0}; float target_temperature_low{0.0f}; float target_temperature_high{0.0f}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1468,7 +1468,7 @@ class ListEntitiesNumberResponse final : public InfoResponseProtoMessage { StringRef unit_of_measurement{}; enums::NumberMode mode{}; StringRef device_class{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1485,7 +1485,7 @@ class NumberStateResponse final : public StateResponseProtoMessage { #endif float state{0.0f}; bool missing_state{false}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1519,7 +1519,7 @@ class ListEntitiesSelectResponse final : public InfoResponseProtoMessage { const char *message_name() const override { return "list_entities_select_response"; } #endif const FixedVector *options{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1536,7 +1536,7 @@ class SelectStateResponse final : public StateResponseProtoMessage { #endif StringRef state{}; bool missing_state{false}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1573,7 +1573,7 @@ class ListEntitiesSirenResponse final : public InfoResponseProtoMessage { const FixedVector *tones{}; bool supports_duration{false}; bool supports_volume{false}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1589,7 +1589,7 @@ class SirenStateResponse final : public StateResponseProtoMessage { const char *message_name() const override { return "siren_state_response"; } #endif bool state{false}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1634,7 +1634,7 @@ class ListEntitiesLockResponse final : public InfoResponseProtoMessage { bool supports_open{false}; bool requires_code{false}; StringRef code_format{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1650,7 +1650,7 @@ class LockStateResponse final : public StateResponseProtoMessage { const char *message_name() const override { return "lock_state_response"; } #endif enums::LockState state{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1687,7 +1687,7 @@ class ListEntitiesButtonResponse final : public InfoResponseProtoMessage { const char *message_name() const override { return "list_entities_button_response"; } #endif StringRef device_class{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1719,7 +1719,7 @@ class MediaPlayerSupportedFormat final : public ProtoMessage { uint32_t num_channels{0}; enums::MediaPlayerFormatPurpose purpose{}; uint32_t sample_bytes{0}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1737,7 +1737,7 @@ class ListEntitiesMediaPlayerResponse final : public InfoResponseProtoMessage { bool supports_pause{false}; std::vector supported_formats{}; uint32_t feature_flags{0}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1755,7 +1755,7 @@ class MediaPlayerStateResponse final : public StateResponseProtoMessage { enums::MediaPlayerState state{}; float volume{0.0f}; bool muted{false}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1811,7 +1811,7 @@ class BluetoothLERawAdvertisement final : public ProtoMessage { uint32_t address_type{0}; uint8_t data[62]{}; uint8_t data_len{0}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1828,7 +1828,7 @@ class BluetoothLERawAdvertisementsResponse final : public ProtoMessage { #endif std::array advertisements{}; uint16_t advertisements_len{0}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1865,7 +1865,7 @@ class BluetoothDeviceConnectionResponse final : public ProtoMessage { bool connected{false}; uint32_t mtu{0}; int32_t error{0}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1893,7 +1893,7 @@ class BluetoothGATTDescriptor final : public ProtoMessage { std::array uuid{}; uint32_t handle{0}; uint32_t short_uuid{0}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1908,7 +1908,7 @@ class BluetoothGATTCharacteristic final : public ProtoMessage { uint32_t properties{0}; FixedVector descriptors{}; uint32_t short_uuid{0}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1922,7 +1922,7 @@ class BluetoothGATTService final : public ProtoMessage { uint32_t handle{0}; FixedVector characteristics{}; uint32_t short_uuid{0}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1939,7 +1939,7 @@ class BluetoothGATTGetServicesResponse final : public ProtoMessage { #endif uint64_t address{0}; std::vector services{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1955,7 +1955,7 @@ class BluetoothGATTGetServicesDoneResponse final : public ProtoMessage { const char *message_name() const override { return "bluetooth_gatt_get_services_done_response"; } #endif uint64_t address{0}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1994,7 +1994,7 @@ class BluetoothGATTReadResponse final : public ProtoMessage { this->data_ptr_ = data; this->data_len_ = len; } - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2089,7 +2089,7 @@ class BluetoothGATTNotifyDataResponse final : public ProtoMessage { this->data_ptr_ = data; this->data_len_ = len; } - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2107,7 +2107,7 @@ class BluetoothConnectionsFreeResponse final : public ProtoMessage { uint32_t free{0}; uint32_t limit{0}; std::array allocated{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2125,7 +2125,7 @@ class BluetoothGATTErrorResponse final : public ProtoMessage { uint64_t address{0}; uint32_t handle{0}; int32_t error{0}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2142,7 +2142,7 @@ class BluetoothGATTWriteResponse final : public ProtoMessage { #endif uint64_t address{0}; uint32_t handle{0}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2159,7 +2159,7 @@ class BluetoothGATTNotifyResponse final : public ProtoMessage { #endif uint64_t address{0}; uint32_t handle{0}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2177,7 +2177,7 @@ class BluetoothDevicePairingResponse final : public ProtoMessage { uint64_t address{0}; bool paired{false}; int32_t error{0}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2195,7 +2195,7 @@ class BluetoothDeviceUnpairingResponse final : public ProtoMessage { uint64_t address{0}; bool success{false}; int32_t error{0}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2213,7 +2213,7 @@ class BluetoothDeviceClearCacheResponse final : public ProtoMessage { uint64_t address{0}; bool success{false}; int32_t error{0}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2231,7 +2231,7 @@ class BluetoothScannerStateResponse final : public ProtoMessage { enums::BluetoothScannerState state{}; enums::BluetoothScannerMode mode{}; enums::BluetoothScannerMode configured_mode{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2277,7 +2277,7 @@ class VoiceAssistantAudioSettings final : public ProtoMessage { uint32_t noise_suppression_level{0}; uint32_t auto_gain{0}; float volume_multiplier{0.0f}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2297,7 +2297,7 @@ class VoiceAssistantRequest final : public ProtoMessage { uint32_t flags{0}; VoiceAssistantAudioSettings audio_settings{}; StringRef wake_word_phrase{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2359,7 +2359,7 @@ class VoiceAssistantAudio final : public ProtoDecodableMessage { const uint8_t *data{nullptr}; uint16_t data_len{0}; bool end{false}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2417,7 +2417,7 @@ class VoiceAssistantAnnounceFinished final : public ProtoMessage { const char *message_name() const override { return "voice_assistant_announce_finished"; } #endif bool success{false}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2430,7 +2430,7 @@ class VoiceAssistantWakeWord final : public ProtoMessage { StringRef id{}; StringRef wake_word{}; std::vector trained_languages{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2480,7 +2480,7 @@ class VoiceAssistantConfigurationResponse final : public ProtoMessage { std::vector available_wake_words{}; const std::vector *active_wake_words{}; uint32_t max_active_wake_words{0}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2515,7 +2515,7 @@ class ListEntitiesAlarmControlPanelResponse final : public InfoResponseProtoMess uint32_t supported_features{0}; bool requires_code{false}; bool requires_code_to_arm{false}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2531,7 +2531,7 @@ class AlarmControlPanelStateResponse final : public StateResponseProtoMessage { const char *message_name() const override { return "alarm_control_panel_state_response"; } #endif enums::AlarmControlPanelState state{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2570,7 +2570,7 @@ class ListEntitiesTextResponse final : public InfoResponseProtoMessage { uint32_t max_length{0}; StringRef pattern{}; enums::TextMode mode{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2587,7 +2587,7 @@ class TextStateResponse final : public StateResponseProtoMessage { #endif StringRef state{}; bool missing_state{false}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2621,7 +2621,7 @@ class ListEntitiesDateResponse final : public InfoResponseProtoMessage { #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_date_response"; } #endif - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2640,7 +2640,7 @@ class DateStateResponse final : public StateResponseProtoMessage { uint32_t year{0}; uint32_t month{0}; uint32_t day{0}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2675,7 +2675,7 @@ class ListEntitiesTimeResponse final : public InfoResponseProtoMessage { #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_time_response"; } #endif - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2694,7 +2694,7 @@ class TimeStateResponse final : public StateResponseProtoMessage { uint32_t hour{0}; uint32_t minute{0}; uint32_t second{0}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2731,7 +2731,7 @@ class ListEntitiesEventResponse final : public InfoResponseProtoMessage { #endif StringRef device_class{}; const FixedVector *event_types{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2747,7 +2747,7 @@ class EventResponse final : public StateResponseProtoMessage { const char *message_name() const override { return "event_response"; } #endif StringRef event_type{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2768,7 +2768,7 @@ class ListEntitiesValveResponse final : public InfoResponseProtoMessage { bool assumed_state{false}; bool supports_position{false}; bool supports_stop{false}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2785,7 +2785,7 @@ class ValveStateResponse final : public StateResponseProtoMessage { #endif float position{0.0f}; enums::ValveOperation current_operation{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2820,7 +2820,7 @@ class ListEntitiesDateTimeResponse final : public InfoResponseProtoMessage { #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_date_time_response"; } #endif - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2837,7 +2837,7 @@ class DateTimeStateResponse final : public StateResponseProtoMessage { #endif bool missing_state{false}; uint32_t epoch_seconds{0}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2871,7 +2871,7 @@ class ListEntitiesUpdateResponse final : public InfoResponseProtoMessage { const char *message_name() const override { return "list_entities_update_response"; } #endif StringRef device_class{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2895,7 +2895,7 @@ class UpdateStateResponse final : public StateResponseProtoMessage { StringRef title{}; StringRef release_summary{}; StringRef release_url{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2930,7 +2930,7 @@ class ZWaveProxyFrame final : public ProtoDecodableMessage { #endif const uint8_t *data{nullptr}; uint16_t data_len{0}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2949,7 +2949,7 @@ class ZWaveProxyRequest final : public ProtoDecodableMessage { enums::ZWaveProxyRequestType type{}; const uint8_t *data{nullptr}; uint16_t data_len{0}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2969,7 +2969,7 @@ class ListEntitiesInfraredResponse final : public InfoResponseProtoMessage { const char *message_name() const override { return "list_entities_infrared_response"; } #endif uint32_t capabilities{0}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -3016,7 +3016,7 @@ class InfraredRFReceiveEvent final : public ProtoMessage { #endif uint32_t key{0}; const std::vector *timings{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; diff --git a/esphome/components/api/proto.cpp b/esphome/components/api/proto.cpp index 764dd3f391..73a3bab12a 100644 --- a/esphome/components/api/proto.cpp +++ b/esphome/components/api/proto.cpp @@ -70,6 +70,21 @@ uint32_t ProtoDecodableMessage::count_repeated_field(const uint8_t *buffer, size return count; } +#ifdef ESPHOME_DEBUG_API +void ProtoWriteBuffer::debug_check_bounds_(size_t bytes, const char *caller) { + if (this->pos_ + bytes > this->buffer_->data() + this->buffer_->size()) { + ESP_LOGE(TAG, "ProtoWriteBuffer bounds check failed in %s: bytes=%zu offset=%td buf_size=%zu", caller, bytes, + this->pos_ - this->buffer_->data(), this->buffer_->size()); + abort(); + } +} +void ProtoWriteBuffer::debug_check_encode_size_(uint32_t field_id, uint32_t expected, ptrdiff_t actual) { + ESP_LOGE(TAG, "encode_message: size mismatch for field %" PRIu32 ": calculated=%" PRIu32 " actual=%td", field_id, + expected, actual); + abort(); +} +#endif + void ProtoDecodableMessage::decode(const uint8_t *buffer, size_t length) { const uint8_t *ptr = buffer; const uint8_t *end = buffer + length; diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index 8ac79633cf..4522fc9665 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -217,21 +217,26 @@ class Proto32Bit { class ProtoWriteBuffer { public: - ProtoWriteBuffer(std::vector *buffer) : buffer_(buffer) {} - void write(uint8_t value) { this->buffer_->push_back(value); } + ProtoWriteBuffer(std::vector *buffer) : buffer_(buffer), pos_(buffer->data() + buffer->size()) {} + ProtoWriteBuffer(std::vector *buffer, size_t write_pos) + : buffer_(buffer), pos_(buffer->data() + write_pos) {} void encode_varint_raw(uint32_t value) { while (value > 0x7F) { - this->buffer_->push_back(static_cast(value | 0x80)); + this->debug_check_bounds_(1); + *this->pos_++ = static_cast(value | 0x80); value >>= 7; } - this->buffer_->push_back(static_cast(value)); + this->debug_check_bounds_(1); + *this->pos_++ = static_cast(value); } void encode_varint_raw_64(uint64_t value) { while (value > 0x7F) { - this->buffer_->push_back(static_cast(value | 0x80)); + this->debug_check_bounds_(1); + *this->pos_++ = static_cast(value | 0x80); value >>= 7; } - this->buffer_->push_back(static_cast(value)); + this->debug_check_bounds_(1); + *this->pos_++ = static_cast(value); } /** * Encode a field key (tag/wire type combination). @@ -245,23 +250,18 @@ class ProtoWriteBuffer { * * Following https://protobuf.dev/programming-guides/encoding/#structure */ - void encode_field_raw(uint32_t field_id, uint32_t type) { - uint32_t val = (field_id << 3) | (type & WIRE_TYPE_MASK); - this->encode_varint_raw(val); - } + void encode_field_raw(uint32_t field_id, uint32_t type) { this->encode_varint_raw((field_id << 3) | type); } void encode_string(uint32_t field_id, const char *string, size_t len, bool force = false) { if (len == 0 && !force) return; this->encode_field_raw(field_id, 2); // type 2: Length-delimited string this->encode_varint_raw(len); - - // Using resize + memcpy instead of insert provides significant performance improvement: - // ~10-11x faster for 16-32 byte strings, ~3x faster for 64-byte strings - // as it avoids iterator checks and potential element moves that insert performs - size_t old_size = this->buffer_->size(); - this->buffer_->resize(old_size + len); - std::memcpy(this->buffer_->data() + old_size, string, len); + // Direct memcpy into pre-sized buffer — avoids push_back() per-byte capacity checks + // and vector::insert() iterator overhead. ~10-11x faster for 16-32 byte strings. + this->debug_check_bounds_(len); + std::memcpy(this->pos_, string, len); + this->pos_ += len; } void encode_string(uint32_t field_id, const std::string &value, bool force = false) { this->encode_string(field_id, value.data(), value.size(), force); @@ -288,17 +288,26 @@ class ProtoWriteBuffer { if (!value && !force) return; this->encode_field_raw(field_id, 0); // type 0: Varint - bool - this->buffer_->push_back(value ? 0x01 : 0x00); + this->debug_check_bounds_(1); + *this->pos_++ = value ? 0x01 : 0x00; } - void encode_fixed32(uint32_t field_id, uint32_t value, bool force = false) { + // noinline: 51 call sites; inlining causes net code growth vs a single out-of-line copy + __attribute__((noinline)) void encode_fixed32(uint32_t field_id, uint32_t value, bool force = false) { if (value == 0 && !force) return; this->encode_field_raw(field_id, 5); // type 5: 32-bit fixed32 - this->write((value >> 0) & 0xFF); - this->write((value >> 8) & 0xFF); - this->write((value >> 16) & 0xFF); - this->write((value >> 24) & 0xFF); + this->debug_check_bounds_(4); +#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ + // Protobuf fixed32 is little-endian, so direct copy works + std::memcpy(this->pos_, &value, 4); + this->pos_ += 4; +#else + *this->pos_++ = (value >> 0) & 0xFF; + *this->pos_++ = (value >> 8) & 0xFF; + *this->pos_++ = (value >> 16) & 0xFF; + *this->pos_++ = (value >> 24) & 0xFF; +#endif } // NOTE: Wire type 1 (64-bit fixed: double, fixed64, sfixed64) is intentionally // not supported to reduce overhead on embedded systems. All ESPHome devices are @@ -334,11 +343,20 @@ class ProtoWriteBuffer { } /// Encode a packed repeated sint32 field (zero-copy from vector) void encode_packed_sint32(uint32_t field_id, const std::vector &values); - void encode_message(uint32_t field_id, const ProtoMessage &value); + /// Encode a nested message field (force=true for repeated, false for singular) + void encode_message(uint32_t field_id, const ProtoMessage &value, bool force = true); std::vector *get_buffer() const { return buffer_; } protected: +#ifdef ESPHOME_DEBUG_API + void debug_check_bounds_(size_t bytes, const char *caller = __builtin_FUNCTION()); + void debug_check_encode_size_(uint32_t field_id, uint32_t expected, ptrdiff_t actual); +#else + void debug_check_bounds_([[maybe_unused]] size_t bytes) {} +#endif + std::vector *buffer_; + uint8_t *pos_; }; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -416,9 +434,11 @@ class ProtoMessage { public: virtual ~ProtoMessage() = default; // Default implementation for messages with no fields - virtual void encode(ProtoWriteBuffer buffer) const {} + virtual void encode(ProtoWriteBuffer &buffer) const {} // Default implementation for messages with no fields virtual void calculate_size(ProtoSize &size) const {} + // Convenience: calculate and return size directly (defined after ProtoSize) + uint32_t calculated_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP virtual const char *dump_to(DumpBuffer &out) const = 0; virtual const char *message_name() const { return "unknown"; } @@ -877,6 +897,14 @@ class ProtoSize { } }; +// Implementation of methods that depend on ProtoSize being fully defined + +inline uint32_t ProtoMessage::calculated_size() const { + ProtoSize size; + this->calculate_size(size); + return size.get_size(); +} + // Implementation of encode_packed_sint32 - must be after ProtoSize is defined inline void ProtoWriteBuffer::encode_packed_sint32(uint32_t field_id, const std::vector &values) { if (values.empty()) @@ -897,30 +925,30 @@ inline void ProtoWriteBuffer::encode_packed_sint32(uint32_t field_id, const std: } // Implementation of encode_message - must be after ProtoMessage is defined -inline void ProtoWriteBuffer::encode_message(uint32_t field_id, const ProtoMessage &value) { - this->encode_field_raw(field_id, 2); // type 2: Length-delimited message - +inline void ProtoWriteBuffer::encode_message(uint32_t field_id, const ProtoMessage &value, bool force) { // Calculate the message size first ProtoSize msg_size; value.calculate_size(msg_size); uint32_t msg_length_bytes = msg_size.get_size(); - // Calculate how many bytes the length varint needs - uint32_t varint_length_bytes = ProtoSize::varint(msg_length_bytes); + // Skip empty singular messages (matches add_message_field which skips when nested_size == 0) + // Repeated messages (force=true) are always encoded since an empty item is meaningful + if (msg_length_bytes == 0 && !force) + return; - // Reserve exact space for the length varint - size_t begin = this->buffer_->size(); - this->buffer_->resize(this->buffer_->size() + varint_length_bytes); + this->encode_field_raw(field_id, 2); // type 2: Length-delimited message - // Write the length varint directly - encode_varint_to_buffer(msg_length_bytes, this->buffer_->data() + begin); - - // Now encode the message content - it will append to the buffer - value.encode(*this); + // Write the length varint directly through pos_ + this->encode_varint_raw(msg_length_bytes); + // Encode nested message - pos_ advances directly through the reference #ifdef ESPHOME_DEBUG_API - // Verify that the encoded size matches what we calculated - assert(this->buffer_->size() == begin + varint_length_bytes + msg_length_bytes); + uint8_t *start = this->pos_; + value.encode(*this); + if (static_cast(this->pos_ - start) != msg_length_bytes) + this->debug_check_encode_size_(field_id, msg_length_bytes, this->pos_ - start); +#else + value.encode(*this); #endif } diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 4fbee49dae..cc881caa5c 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -689,6 +689,14 @@ class MessageType(TypeInfo): def encode_func(self) -> str: return "encode_message" + @property + def encode_content(self) -> str: + # Singular message fields pass force=false (skip empty messages) + # The default for encode_nested_message is force=true (for repeated fields) + return ( + f"buffer.{self.encode_func}({self.number}, this->{self.field_name}, false);" + ) + @property def decode_length(self) -> str: # Override to return None for message types because we can't use template-based @@ -2186,7 +2194,7 @@ def build_message_type( # Only generate encode method if this message needs encoding and has fields if needs_encode and encode: - o = f"void {desc.name}::encode(ProtoWriteBuffer buffer) const {{" + o = f"void {desc.name}::encode(ProtoWriteBuffer &buffer) const {{" if len(encode) == 1 and len(encode[0]) + len(o) + 3 < 120: o += f" {encode[0]} }}\n" else: @@ -2194,7 +2202,7 @@ def build_message_type( o += indent("\n".join(encode)) + "\n" o += "}\n" cpp += o - prot = "void encode(ProtoWriteBuffer buffer) const override;" + prot = "void encode(ProtoWriteBuffer &buffer) const override;" public_content.append(prot) # If no fields to encode or message doesn't need encoding, the default implementation in ProtoMessage will be used From 2eac106f119627e2ac58d39f4ebd92990895366a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Mart=C3=ADn?= Date: Sat, 21 Feb 2026 05:20:27 +0100 Subject: [PATCH 164/197] [mqtt] add missing precision in HA autodiscovery (#14010) --- esphome/components/mqtt/mqtt_const.h | 1 + esphome/components/mqtt/mqtt_sensor.cpp | 4 ++++ esphome/components/sensor/sensor.h | 2 ++ 3 files changed, 7 insertions(+) diff --git a/esphome/components/mqtt/mqtt_const.h b/esphome/components/mqtt/mqtt_const.h index 221af00371..36a7dfb41a 100644 --- a/esphome/components/mqtt/mqtt_const.h +++ b/esphome/components/mqtt/mqtt_const.h @@ -243,6 +243,7 @@ X(MQTT_STATE_VALUE_TEMPLATE, "stat_val_tpl", "state_value_template") \ X(MQTT_STEP, "step", "step") \ X(MQTT_SUBTYPE, "stype", "subtype") \ + X(MQTT_SUGGESTED_DISPLAY_PRECISION, "sug_dsp_prc", "suggested_display_precision") \ X(MQTT_SUPPORTED_COLOR_MODES, "sup_clrm", "supported_color_modes") \ X(MQTT_SUPPORTED_FEATURES, "sup_feat", "supported_features") \ X(MQTT_SWING_MODE_COMMAND_TEMPLATE, "swing_mode_cmd_tpl", "swing_mode_command_template") \ diff --git a/esphome/components/mqtt/mqtt_sensor.cpp b/esphome/components/mqtt/mqtt_sensor.cpp index e83eab6732..a7d311d194 100644 --- a/esphome/components/mqtt/mqtt_sensor.cpp +++ b/esphome/components/mqtt/mqtt_sensor.cpp @@ -49,6 +49,10 @@ void MQTTSensorComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCon root[MQTT_DEVICE_CLASS] = device_class; } + if (this->sensor_->has_accuracy_decimals()) { + root[MQTT_SUGGESTED_DISPLAY_PRECISION] = this->sensor_->get_accuracy_decimals(); + } + const auto unit_of_measurement = this->sensor_->get_unit_of_measurement_ref(); if (!unit_of_measurement.empty()) { root[MQTT_UNIT_OF_MEASUREMENT] = unit_of_measurement; diff --git a/esphome/components/sensor/sensor.h b/esphome/components/sensor/sensor.h index f9a45cb1d0..80981b8e28 100644 --- a/esphome/components/sensor/sensor.h +++ b/esphome/components/sensor/sensor.h @@ -48,6 +48,8 @@ class Sensor : public EntityBase, public EntityBase_DeviceClass, public EntityBa int8_t get_accuracy_decimals(); /// Manually set the accuracy in decimals. void set_accuracy_decimals(int8_t accuracy_decimals); + /// Check if the accuracy in decimals has been manually set. + bool has_accuracy_decimals() const { return this->sensor_flags_.has_accuracy_override; } /// Get the state class, using the manual override if set. StateClass get_state_class(); From 518f08b9097bdb3a980483f1358e309987de7402 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Fri, 20 Feb 2026 22:51:13 -1000 Subject: [PATCH 165/197] [mipi_dsi] Disallow swap_xy (#14124) --- esphome/components/mipi_dsi/display.py | 26 +++++-------------- .../mipi_dsi/fixtures/mipi_dsi.yaml | 7 ++++- .../mipi_dsi/test_mipi_dsi_config.py | 6 +++-- 3 files changed, 16 insertions(+), 23 deletions(-) diff --git a/esphome/components/mipi_dsi/display.py b/esphome/components/mipi_dsi/display.py index c288b33cd2..de3791b3a4 100644 --- a/esphome/components/mipi_dsi/display.py +++ b/esphome/components/mipi_dsi/display.py @@ -87,38 +87,24 @@ COLOR_DEPTHS = { def model_schema(config): model = MODELS[config[CONF_MODEL].upper()] + model.defaults[CONF_SWAP_XY] = cv.UNDEFINED transform = cv.Schema( { cv.Required(CONF_MIRROR_X): cv.boolean, cv.Required(CONF_MIRROR_Y): cv.boolean, + cv.Optional(CONF_SWAP_XY): cv.invalid( + "Axis swapping not supported by DSI displays" + ), } ) - if model.get_default(CONF_SWAP_XY) != cv.UNDEFINED: - transform = transform.extend( - { - cv.Optional(CONF_SWAP_XY): cv.invalid( - "Axis swapping not supported by this model" - ) - } - ) - else: - transform = transform.extend( - { - cv.Required(CONF_SWAP_XY): cv.boolean, - } - ) # CUSTOM model will need to provide a custom init sequence iseqconf = ( cv.Required(CONF_INIT_SEQUENCE) if model.initsequence is None else cv.Optional(CONF_INIT_SEQUENCE) ) - swap_xy = config.get(CONF_TRANSFORM, {}).get(CONF_SWAP_XY, False) - - # Dimensions are optional if the model has a default width and the swap_xy transform is not overridden - cv_dimensions = ( - cv.Optional if model.get_default(CONF_WIDTH) and not swap_xy else cv.Required - ) + # Dimensions are optional if the model has a default width + cv_dimensions = cv.Optional if model.get_default(CONF_WIDTH) else cv.Required pixel_modes = (PIXEL_MODE_16BIT, PIXEL_MODE_24BIT, "16", "24") schema = display.FULL_DISPLAY_SCHEMA.extend( { diff --git a/tests/component_tests/mipi_dsi/fixtures/mipi_dsi.yaml b/tests/component_tests/mipi_dsi/fixtures/mipi_dsi.yaml index 7d1fc84121..6de2bd5a77 100644 --- a/tests/component_tests/mipi_dsi/fixtures/mipi_dsi.yaml +++ b/tests/component_tests/mipi_dsi/fixtures/mipi_dsi.yaml @@ -15,8 +15,13 @@ esp_ldo: display: - platform: mipi_dsi + id: p4_nano model: WAVESHARE-P4-NANO-10.1 - + rotation: 90 + - platform: mipi_dsi + id: p4_86 + model: "WAVESHARE-P4-86-PANEL" + rotation: 180 i2c: sda: GPIO7 scl: GPIO8 diff --git a/tests/component_tests/mipi_dsi/test_mipi_dsi_config.py b/tests/component_tests/mipi_dsi/test_mipi_dsi_config.py index f8a9af0279..d465a8c81b 100644 --- a/tests/component_tests/mipi_dsi/test_mipi_dsi_config.py +++ b/tests/component_tests/mipi_dsi/test_mipi_dsi_config.py @@ -119,9 +119,11 @@ def test_code_generation( main_cpp = generate_main(component_fixture_path("mipi_dsi.yaml")) assert ( - "mipi_dsi_mipi_dsi_id = new mipi_dsi::MIPI_DSI(800, 1280, display::COLOR_BITNESS_565, 16);" + "p4_nano = new mipi_dsi::MIPI_DSI(800, 1280, display::COLOR_BITNESS_565, 16);" in main_cpp ) assert "set_init_sequence({224, 1, 0, 225, 1, 147, 226, 1," in main_cpp - assert "mipi_dsi_mipi_dsi_id->set_lane_bit_rate(1500);" in main_cpp + assert "p4_nano->set_lane_bit_rate(1500);" in main_cpp + assert "p4_nano->set_rotation(display::DISPLAY_ROTATION_90_DEGREES);" in main_cpp + assert "p4_86->set_rotation(display::DISPLAY_ROTATION_0_DEGREES);" in main_cpp # assert "backlight_id = new light::LightState(mipi_dsi_dsibacklight_id);" in main_cpp From 6ecb01dedc00595aa2565bcf302cde3d7ad1d4e9 Mon Sep 17 00:00:00 2001 From: Sxt Fov <140247217+sxtfov@users.noreply.github.com> Date: Sat, 21 Feb 2026 14:45:15 +0100 Subject: [PATCH 166/197] [cc1101] actions to change general and tuner settings (#14141) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- esphome/components/cc1101/__init__.py | 92 +++++++++++++++++++++++++ esphome/components/cc1101/cc1101.h | 78 ++++++++++++++++++++++ tests/components/cc1101/common.yaml | 96 +++++++++++++++++++++++++++ 3 files changed, 266 insertions(+) diff --git a/esphome/components/cc1101/__init__.py b/esphome/components/cc1101/__init__.py index fbdd7010b4..14b92a18a4 100644 --- a/esphome/components/cc1101/__init__.py +++ b/esphome/components/cc1101/__init__.py @@ -9,6 +9,7 @@ from esphome.const import ( CONF_DATA, CONF_FREQUENCY, CONF_ID, + CONF_VALUE, CONF_WAIT_TIME, ) from esphome.core import ID @@ -333,3 +334,94 @@ async def send_packet_action_to_code(config, action_id, template_arg, args): arr = cg.static_const_array(arr_id, cg.ArrayInitializer(*data)) cg.add(var.set_data_static(arr, len(data))) return var + + +# Setter action definitions: (setter_name, validator, template_type, enum_map) +_SETTER_ACTIONS = [ + ( + "set_frequency", + cv.All(cv.frequency, cv.float_range(min=300.0e6, max=928.0e6)), + float, + None, + ), + ("set_output_power", cv.float_range(min=-30.0, max=11.0), float, None), + ("set_modulation_type", cv.enum(MODULATION, upper=False), Modulation, MODULATION), + ("set_symbol_rate", cv.float_range(min=600, max=500000), float, None), + ( + "set_rx_attenuation", + cv.enum(RX_ATTENUATION, upper=False), + RxAttenuation, + RX_ATTENUATION, + ), + ("set_dc_blocking_filter", cv.boolean, bool, None), + ("set_manchester", cv.boolean, bool, None), + ( + "set_filter_bandwidth", + cv.All(cv.frequency, cv.float_range(min=58000, max=812000)), + float, + None, + ), + ( + "set_fsk_deviation", + cv.All(cv.frequency, cv.float_range(min=1500, max=381000)), + float, + None, + ), + ("set_msk_deviation", cv.int_range(min=1, max=8), cg.uint8, None), + ("set_channel", cv.uint8_t, cg.uint8, None), + ( + "set_channel_spacing", + cv.All(cv.frequency, cv.float_range(min=25000, max=405000)), + float, + None, + ), + ( + "set_if_frequency", + cv.All(cv.frequency, cv.float_range(min=25000, max=788000)), + float, + None, + ), +] + + +def _register_setter_actions(): + for setter_name, validator, templ_type, enum_map in _SETTER_ACTIONS: + class_name = ( + "".join(word.capitalize() for word in setter_name.split("_")) + "Action" + ) + action_cls = ns.class_( + class_name, automation.Action, cg.Parented.template(CC1101Component) + ) + schema = cv.maybe_simple_value( + { + cv.GenerateID(): cv.use_id(CC1101Component), + cv.Required(CONF_VALUE): cv.templatable(validator), + }, + key=CONF_VALUE, + ) + + async def _setter_action_to_code( + config, + action_id, + template_arg, + args, + _setter=setter_name, + _type=templ_type, + _map=enum_map, + ): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + data = config[CONF_VALUE] + if cg.is_template(data): + templ_ = await cg.templatable(data, args, _type) + cg.add(getattr(var, _setter)(templ_)) + else: + cg.add(getattr(var, _setter)(_map[data] if _map else data)) + return var + + automation.register_action(f"cc1101.{setter_name}", action_cls, schema)( + _setter_action_to_code + ) + + +_register_setter_actions() diff --git a/esphome/components/cc1101/cc1101.h b/esphome/components/cc1101/cc1101.h index e55071e7e3..2efd9e082d 100644 --- a/esphome/components/cc1101/cc1101.h +++ b/esphome/components/cc1101/cc1101.h @@ -161,4 +161,82 @@ template class SendPacketAction : public Action, public P size_t data_static_len_{0}; }; +template class SetSymbolRateAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(float, symbol_rate) + void play(const Ts &...x) override { this->parent_->set_symbol_rate(this->symbol_rate_.value(x...)); } +}; + +template class SetFrequencyAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(float, frequency) + void play(const Ts &...x) override { this->parent_->set_frequency(this->frequency_.value(x...)); } +}; + +template class SetOutputPowerAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(float, output_power) + void play(const Ts &...x) override { this->parent_->set_output_power(this->output_power_.value(x...)); } +}; + +template class SetModulationTypeAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(Modulation, modulation_type) + void play(const Ts &...x) override { this->parent_->set_modulation_type(this->modulation_type_.value(x...)); } +}; + +template class SetRxAttenuationAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(RxAttenuation, rx_attenuation) + void play(const Ts &...x) override { this->parent_->set_rx_attenuation(this->rx_attenuation_.value(x...)); } +}; + +template class SetDcBlockingFilterAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(bool, dc_blocking_filter) + void play(const Ts &...x) override { this->parent_->set_dc_blocking_filter(this->dc_blocking_filter_.value(x...)); } +}; + +template class SetManchesterAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(bool, manchester) + void play(const Ts &...x) override { this->parent_->set_manchester(this->manchester_.value(x...)); } +}; + +template class SetFilterBandwidthAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(float, filter_bandwidth) + void play(const Ts &...x) override { this->parent_->set_filter_bandwidth(this->filter_bandwidth_.value(x...)); } +}; + +template class SetFskDeviationAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(float, fsk_deviation) + void play(const Ts &...x) override { this->parent_->set_fsk_deviation(this->fsk_deviation_.value(x...)); } +}; + +template class SetMskDeviationAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(uint8_t, msk_deviation) + void play(const Ts &...x) override { this->parent_->set_msk_deviation(this->msk_deviation_.value(x...)); } +}; + +template class SetChannelAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(uint8_t, channel) + void play(const Ts &...x) override { this->parent_->set_channel(this->channel_.value(x...)); } +}; + +template class SetChannelSpacingAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(float, channel_spacing) + void play(const Ts &...x) override { this->parent_->set_channel_spacing(this->channel_spacing_.value(x...)); } +}; + +template class SetIfFrequencyAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(float, if_frequency) + void play(const Ts &...x) override { this->parent_->set_if_frequency(this->if_frequency_.value(x...)); } +}; + } // namespace esphome::cc1101 diff --git a/tests/components/cc1101/common.yaml b/tests/components/cc1101/common.yaml index 42ec50911f..9784bfce8b 100644 --- a/tests/components/cc1101/common.yaml +++ b/tests/components/cc1101/common.yaml @@ -35,3 +35,99 @@ button: data: [0x12, 0x34, 0x56, 0x78, 0x90, 0xab, 0xcd, 0xef] - cc1101.send_packet: !lambda |- return {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}; + + - cc1101.set_frequency: !lambda |- + return 433.91e6; + - cc1101.set_frequency: + value: "433.91MHz" + - cc1101.set_frequency: + value: 433911000 + - cc1101.set_frequency: 433912000 + + - cc1101.set_output_power: !lambda |- + return -29.9; + - cc1101.set_output_power: + value: "-28" + - cc1101.set_output_power: + value: 10 + - cc1101.set_output_power: 11 + + - cc1101.set_modulation_type: !lambda |- + return cc1101::Modulation::MODULATION_2_FSK; + - cc1101.set_modulation_type: + value: "4-FSK" + - cc1101.set_modulation_type: "GFSK" + + - cc1101.set_symbol_rate: !lambda |- + return 6000.0; + - cc1101.set_symbol_rate: + value: "7000.0" + - cc1101.set_symbol_rate: + value: 8000.0 + - cc1101.set_symbol_rate: 9000 + + - cc1101.set_rx_attenuation: !lambda |- + return cc1101::RxAttenuation::RX_ATTENUATION_0DB; + - cc1101.set_rx_attenuation: + value: "6dB" + - cc1101.set_rx_attenuation: "12dB" + + - cc1101.set_dc_blocking_filter: !lambda |- + return false; + - cc1101.set_dc_blocking_filter: + value: true + - cc1101.set_dc_blocking_filter: false + + - cc1101.set_manchester: !lambda |- + return false; + - cc1101.set_manchester: + value: true + - cc1101.set_manchester: false + + - cc1101.set_filter_bandwidth: !lambda |- + return 58e3; + - cc1101.set_filter_bandwidth: + value: "59kHz" + - cc1101.set_filter_bandwidth: + value: 60000 + - cc1101.set_filter_bandwidth: "61kHz" + + - cc1101.set_fsk_deviation: !lambda |- + return 1.5e3; + - cc1101.set_fsk_deviation: + value: "1.6kHz" + - cc1101.set_fsk_deviation: + value: 1700 + - cc1101.set_fsk_deviation: "1.8kHz" + + - cc1101.set_msk_deviation: !lambda |- + return 1; + - cc1101.set_msk_deviation: + value: "2" + - cc1101.set_msk_deviation: + value: 3 + - cc1101.set_msk_deviation: "4" + + - cc1101.set_channel: !lambda |- + return 0; + - cc1101.set_channel: + value: "1" + - cc1101.set_channel: + value: 3 + - cc1101.set_channel: 3 + + - cc1101.set_channel_spacing: !lambda |- + return 25e3; + - cc1101.set_channel_spacing: + value: "26kHz" + - cc1101.set_channel_spacing: + value: 27000 + - cc1101.set_channel_spacing: "28kHz" + + - cc1101.set_if_frequency: !lambda |- + return 25e3; + - cc1101.set_if_frequency: + value: "26kHz" + - cc1101.set_if_frequency: + value: 27000 + - cc1101.set_if_frequency: "28kHz" From 7fb09da7cf228b3a5d2bbdfd60b3e6fb571b0331 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 21 Feb 2026 11:08:13 -0600 Subject: [PATCH 167/197] [dsmr] Add deprecated std::string overload for set_decryption_key (#14180) --- esphome/components/dsmr/dsmr.h | 3 +++ tests/components/dsmr/common.yaml | 9 +++++++++ 2 files changed, 12 insertions(+) diff --git a/esphome/components/dsmr/dsmr.h b/esphome/components/dsmr/dsmr.h index fafcf62b87..dc81ba9b2a 100644 --- a/esphome/components/dsmr/dsmr.h +++ b/esphome/components/dsmr/dsmr.h @@ -64,6 +64,9 @@ class Dsmr : public Component, public uart::UARTDevice { void dump_config() override; void set_decryption_key(const char *decryption_key); + // Remove before 2026.8.0 + ESPDEPRECATED("Pass .c_str() - e.g. set_decryption_key(key.c_str()). Removed in 2026.8.0", "2026.2.0") + void set_decryption_key(const std::string &decryption_key) { this->set_decryption_key(decryption_key.c_str()); } void set_max_telegram_length(size_t length) { this->max_telegram_len_ = length; } void set_request_pin(GPIOPin *request_pin) { this->request_pin_ = request_pin; } void set_request_interval(uint32_t interval) { this->request_interval_ = interval; } diff --git a/tests/components/dsmr/common.yaml b/tests/components/dsmr/common.yaml index 038bf2806b..d11ce37d59 100644 --- a/tests/components/dsmr/common.yaml +++ b/tests/components/dsmr/common.yaml @@ -1,4 +1,13 @@ +esphome: + on_boot: + then: + - lambda: |- + // Test deprecated std::string overload still compiles + std::string key = "00112233445566778899aabbccddeeff"; + id(dsmr_instance).set_decryption_key(key); + dsmr: + id: dsmr_instance decryption_key: 00112233445566778899aabbccddeeff max_telegram_length: 1000 request_pin: ${request_pin} From 416b97311b800e9563e48da0e403ed22e717ff61 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 21 Feb 2026 11:12:35 -0600 Subject: [PATCH 168/197] [mqtt] Remove broken ESP8266 ssl_fingerprints option (#14182) --- esphome/__main__.py | 14 --------- esphome/components/mqtt/__init__.py | 21 -------------- .../components/mqtt/mqtt_backend_esp8266.h | 5 ---- .../components/mqtt/mqtt_backend_libretiny.h | 5 ---- esphome/components/mqtt/mqtt_client.cpp | 7 ----- esphome/components/mqtt/mqtt_client.h | 15 ---------- esphome/const.py | 1 - esphome/mqtt.py | 29 ++----------------- 8 files changed, 2 insertions(+), 95 deletions(-) diff --git a/esphome/__main__.py b/esphome/__main__.py index c86b5604e1..488955f503 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -944,12 +944,6 @@ def command_clean_all(args: ArgsProtocol) -> int | None: return 0 -def command_mqtt_fingerprint(args: ArgsProtocol, config: ConfigType) -> int | None: - from esphome import mqtt - - return mqtt.get_fingerprint(config) - - def command_version(args: ArgsProtocol) -> int | None: safe_print(f"Version: {const.__version__}") return 0 @@ -1237,7 +1231,6 @@ POST_CONFIG_ACTIONS = { "run": command_run, "clean": command_clean, "clean-mqtt": command_clean_mqtt, - "mqtt-fingerprint": command_mqtt_fingerprint, "idedata": command_idedata, "rename": command_rename, "discover": command_discover, @@ -1451,13 +1444,6 @@ def parse_args(argv): ) parser_wizard.add_argument("configuration", help="Your YAML configuration file.") - parser_fingerprint = subparsers.add_parser( - "mqtt-fingerprint", help="Get the SSL fingerprint from a MQTT broker." - ) - parser_fingerprint.add_argument( - "configuration", help="Your YAML configuration file(s).", nargs="+" - ) - subparsers.add_parser("version", help="Print the ESPHome version and exit.") parser_clean = subparsers.add_parser( diff --git a/esphome/components/mqtt/__init__.py b/esphome/components/mqtt/__init__.py index fe153fedfa..44e8836487 100644 --- a/esphome/components/mqtt/__init__.py +++ b/esphome/components/mqtt/__init__.py @@ -1,5 +1,3 @@ -import re - from esphome import automation from esphome.automation import Condition import esphome.codegen as cg @@ -46,7 +44,6 @@ from esphome.const import ( CONF_RETAIN, CONF_SHUTDOWN_MESSAGE, CONF_SKIP_CERT_CN_CHECK, - CONF_SSL_FINGERPRINTS, CONF_STATE_TOPIC, CONF_SUBSCRIBE_QOS, CONF_TOPIC, @@ -221,13 +218,6 @@ def validate_config(value): return out -def validate_fingerprint(value): - value = cv.string(value) - if re.match(r"^[0-9a-f]{40}$", value) is None: - raise cv.Invalid("fingerprint must be valid SHA1 hash") - return value - - def _consume_mqtt_sockets(config: ConfigType) -> ConfigType: """Register socket needs for MQTT component.""" # MQTT needs 1 socket for the broker connection @@ -291,9 +281,6 @@ CONFIG_SCHEMA = cv.All( ), validate_message_just_topic, ), - cv.Optional(CONF_SSL_FINGERPRINTS): cv.All( - cv.only_on_esp8266, cv.ensure_list(validate_fingerprint) - ), cv.Optional(CONF_KEEPALIVE, default="15s"): cv.positive_time_period_seconds, cv.Optional( CONF_REBOOT_TIMEOUT, default="15min" @@ -444,14 +431,6 @@ async def to_code(config): if CONF_LEVEL in log_topic: cg.add(var.set_log_level(logger.LOG_LEVELS[log_topic[CONF_LEVEL]])) - if CONF_SSL_FINGERPRINTS in config: - for fingerprint in config[CONF_SSL_FINGERPRINTS]: - arr = [ - cg.RawExpression(f"0x{fingerprint[i : i + 2]}") for i in range(0, 40, 2) - ] - cg.add(var.add_ssl_fingerprint(arr)) - cg.add_build_flag("-DASYNC_TCP_SSL_ENABLED=1") - cg.add(var.set_keep_alive(config[CONF_KEEPALIVE])) cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT])) diff --git a/esphome/components/mqtt/mqtt_backend_esp8266.h b/esphome/components/mqtt/mqtt_backend_esp8266.h index 470d1e6a8b..0bf5b510a4 100644 --- a/esphome/components/mqtt/mqtt_backend_esp8266.h +++ b/esphome/components/mqtt/mqtt_backend_esp8266.h @@ -21,11 +21,6 @@ class MQTTBackendESP8266 final : public MQTTBackend { } void set_server(network::IPAddress ip, uint16_t port) final { mqtt_client_.setServer(ip, port); } void set_server(const char *host, uint16_t port) final { mqtt_client_.setServer(host, port); } -#if ASYNC_TCP_SSL_ENABLED - void set_secure(bool secure) { mqtt_client.setSecure(secure); } - void add_server_fingerprint(const uint8_t *fingerprint) { mqtt_client.addServerFingerprint(fingerprint); } -#endif - void set_on_connect(std::function &&callback) final { this->mqtt_client_.onConnect(std::move(callback)); } diff --git a/esphome/components/mqtt/mqtt_backend_libretiny.h b/esphome/components/mqtt/mqtt_backend_libretiny.h index 24bf018a90..5fa3406193 100644 --- a/esphome/components/mqtt/mqtt_backend_libretiny.h +++ b/esphome/components/mqtt/mqtt_backend_libretiny.h @@ -21,11 +21,6 @@ class MQTTBackendLibreTiny final : public MQTTBackend { } void set_server(network::IPAddress ip, uint16_t port) final { mqtt_client_.setServer(IPAddress(ip), port); } void set_server(const char *host, uint16_t port) final { mqtt_client_.setServer(host, port); } -#if ASYNC_TCP_SSL_ENABLED - void set_secure(bool secure) { mqtt_client.setSecure(secure); } - void add_server_fingerprint(const uint8_t *fingerprint) { mqtt_client.addServerFingerprint(fingerprint); } -#endif - void set_on_connect(std::function &&callback) final { this->mqtt_client_.onConnect(std::move(callback)); } diff --git a/esphome/components/mqtt/mqtt_client.cpp b/esphome/components/mqtt/mqtt_client.cpp index c433804dd9..1a03c5329e 100644 --- a/esphome/components/mqtt/mqtt_client.cpp +++ b/esphome/components/mqtt/mqtt_client.cpp @@ -749,13 +749,6 @@ void MQTTClientComponent::set_on_disconnect(mqtt_on_disconnect_callback_t &&call this->on_disconnect_.add(std::move(callback_copy)); } -#if ASYNC_TCP_SSL_ENABLED -void MQTTClientComponent::add_ssl_fingerprint(const std::array &fingerprint) { - this->mqtt_backend_.setSecure(true); - this->mqtt_backend_.addServerFingerprint(fingerprint.data()); -} -#endif - MQTTClientComponent *global_mqtt_client = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) // MQTTMessageTrigger diff --git a/esphome/components/mqtt/mqtt_client.h b/esphome/components/mqtt/mqtt_client.h index 21edd53eda..127e4073b0 100644 --- a/esphome/components/mqtt/mqtt_client.h +++ b/esphome/components/mqtt/mqtt_client.h @@ -137,21 +137,6 @@ class MQTTClientComponent : public Component { bool is_discovery_enabled() const; bool is_discovery_ip_enabled() const; -#if ASYNC_TCP_SSL_ENABLED - /** Add a SSL fingerprint to use for TCP SSL connections to the MQTT broker. - * - * To use this feature you first have to globally enable the `ASYNC_TCP_SSL_ENABLED` define flag. - * This function can be called multiple times and any certificate that matches any of the provided fingerprints - * will match. Calling this method will also automatically disable all non-ssl connections. - * - * @warning This is *not* secure and *not* how SSL is usually done. You'll have to add - * a separate fingerprint for every certificate you use. Additionally, the hashing - * algorithm used here due to the constraints of the MCU, SHA1, is known to be insecure. - * - * @param fingerprint The SSL fingerprint as a 20 value long std::array. - */ - void add_ssl_fingerprint(const std::array &fingerprint); -#endif #ifdef USE_ESP32 void set_ca_certificate(const char *cert) { this->mqtt_backend_.set_ca_certificate(cert); } void set_cl_certificate(const char *cert) { this->mqtt_backend_.set_cl_certificate(cert); } diff --git a/esphome/const.py b/esphome/const.py index f72cbc8893..0179aa129e 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -943,7 +943,6 @@ CONF_SPI = "spi" CONF_SPI_ID = "spi_id" CONF_SPIKE_REJECTION = "spike_rejection" CONF_SSID = "ssid" -CONF_SSL_FINGERPRINTS = "ssl_fingerprints" CONF_STARTUP_DELAY = "startup_delay" CONF_STATE = "state" CONF_STATE_CLASS = "state_class" diff --git a/esphome/mqtt.py b/esphome/mqtt.py index 042df12d67..cbf78bd3f6 100644 --- a/esphome/mqtt.py +++ b/esphome/mqtt.py @@ -1,6 +1,5 @@ import contextlib from datetime import datetime -import hashlib import json import logging import ssl @@ -22,14 +21,12 @@ from esphome.const import ( CONF_PASSWORD, CONF_PORT, CONF_SKIP_CERT_CN_CHECK, - CONF_SSL_FINGERPRINTS, CONF_TOPIC, CONF_TOPIC_PREFIX, CONF_USERNAME, ) -from esphome.core import CORE, EsphomeError +from esphome.core import EsphomeError from esphome.helpers import get_int_env, get_str_env -from esphome.log import AnsiFore, color from esphome.types import ConfigType from esphome.util import safe_print @@ -102,9 +99,7 @@ def prepare( elif username: client.username_pw_set(username, password) - if config[CONF_MQTT].get(CONF_SSL_FINGERPRINTS) or config[CONF_MQTT].get( - CONF_CERTIFICATE_AUTHORITY - ): + if config[CONF_MQTT].get(CONF_CERTIFICATE_AUTHORITY): context = ssl.create_default_context( cadata=config[CONF_MQTT].get(CONF_CERTIFICATE_AUTHORITY) ) @@ -283,23 +278,3 @@ def clear_topic(config, topic, username=None, password=None, client_id=None): client.publish(msg.topic, None, retain=True) return initialize(config, [topic], on_message, None, username, password, client_id) - - -# From marvinroger/async-mqtt-client -> scripts/get-fingerprint/get-fingerprint.py -def get_fingerprint(config): - addr = str(config[CONF_MQTT][CONF_BROKER]), int(config[CONF_MQTT][CONF_PORT]) - _LOGGER.info("Getting fingerprint from %s:%s", addr[0], addr[1]) - try: - cert_pem = ssl.get_server_certificate(addr) - except OSError as err: - _LOGGER.error("Unable to connect to server: %s", err) - return 1 - cert_der = ssl.PEM_cert_to_DER_cert(cert_pem) - - sha1 = hashlib.sha1(cert_der).hexdigest() - - safe_print(f"SHA1 Fingerprint: {color(AnsiFore.CYAN, sha1)}") - safe_print( - f"Copy the string above into mqtt.ssl_fingerprints section of {CORE.config_path}" - ) - return 0 From 6ff17fbf7c3d4b24ae0e20103c0828823d49b1dc Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Sat, 21 Feb 2026 07:17:54 -1000 Subject: [PATCH 169/197] [epaper_spi] Fix color mapping for weact (#14134) --- esphome/components/epaper_spi/epaper_spi.h | 2 +- .../components/epaper_spi/epaper_weact_3c.cpp | 43 ++++++++++++------- 2 files changed, 29 insertions(+), 16 deletions(-) diff --git a/esphome/components/epaper_spi/epaper_spi.h b/esphome/components/epaper_spi/epaper_spi.h index a8c2fe9b56..a743985518 100644 --- a/esphome/components/epaper_spi/epaper_spi.h +++ b/esphome/components/epaper_spi/epaper_spi.h @@ -76,7 +76,7 @@ class EPaperBase : public Display, static uint8_t color_to_bit(Color color) { // It's always a shade of gray. Map to BLACK or WHITE. // We split the luminance at a suitable point - if ((static_cast(color.r) + color.g + color.b) > 512) { + if ((color.r + color.g + color.b) >= 382) { return 1; } return 0; diff --git a/esphome/components/epaper_spi/epaper_weact_3c.cpp b/esphome/components/epaper_spi/epaper_weact_3c.cpp index bd83105dd7..d4dac7076c 100644 --- a/esphome/components/epaper_spi/epaper_weact_3c.cpp +++ b/esphome/components/epaper_spi/epaper_weact_3c.cpp @@ -5,9 +5,24 @@ namespace esphome::epaper_spi { static constexpr const char *const TAG = "epaper_weact_3c"; +enum class BwrState : uint8_t { + BWR_BLACK, + BWR_WHITE, + BWR_RED, +}; + +static BwrState color_to_bwr(Color color) { + if (color.r > color.g + color.b && color.r > 127) { + return BwrState::BWR_RED; + } + if (color.r + color.g + color.b >= 382) { + return BwrState::BWR_WHITE; + } + return BwrState::BWR_BLACK; +} // SSD1680 3-color display notes: // - Buffer uses 1 bit per pixel, 8 pixels per byte -// - Buffer first half (black_offset): Black/White plane (1=black, 0=white) +// - Buffer first half (black_offset): Black/White plane (0=black, 1=white) // - Buffer second half (red_offset): Red plane (1=red, 0=no red) // - Total buffer: width * height / 4 bytes = 2 * (width * height / 8) // - For 128x296: 128*296/4 = 9472 bytes total (4736 per color) @@ -23,20 +38,20 @@ void EPaperWeAct3C::draw_pixel_at(int x, int y, Color color) { // Use luminance threshold for B/W mapping // Split at halfway point (382 = (255*3)/2) - bool is_white = (static_cast(color.r) + color.g + color.b) > 382; + auto bwr = color_to_bwr(color); // Update black/white plane (first half of buffer) - if (is_white) { - // White pixel - clear bit in black plane - this->buffer_[pos] &= ~bit; - } else { - // Black pixel - set bit in black plane + if (bwr == BwrState::BWR_WHITE) { + // White pixel - set bit in black plane this->buffer_[pos] |= bit; + } else { + // Black pixel - clear bit in black plane + this->buffer_[pos] &= ~bit; } // Update red plane (second half of buffer) // Red if red component is dominant (r > g+b) - if (color.r > color.g + color.b) { + if (bwr == BwrState::BWR_RED) { // Red pixel - set bit in red plane this->buffer_[red_offset + pos] |= bit; } else { @@ -53,21 +68,20 @@ void EPaperWeAct3C::fill(Color color) { const size_t half_buffer = this->buffer_length_ / 2u; // Use luminance threshold for B/W mapping - bool is_white = (static_cast(color.r) + color.g + color.b) > 382; - bool is_red = color.r > color.g + color.b; + auto bits = color_to_bwr(color); // Fill both planes - if (is_white) { - // White - both planes = 0x00 + if (bits == BwrState::BWR_BLACK) { + // Black - both planes = 0x00 this->buffer_.fill(0x00); - } else if (is_red) { + } else if (bits == BwrState::BWR_RED) { // Red - black plane = 0x00, red plane = 0xFF for (size_t i = 0; i < half_buffer; i++) this->buffer_[i] = 0x00; for (size_t i = 0; i < half_buffer; i++) this->buffer_[half_buffer + i] = 0xFF; } else { - // Black - black plane = 0xFF, red plane = 0x00 + // White - black plane = 0xFF, red plane = 0x00 for (size_t i = 0; i < half_buffer; i++) this->buffer_[i] = 0xFF; for (size_t i = 0; i < half_buffer; i++) @@ -112,7 +126,6 @@ bool HOT EPaperWeAct3C::transfer_data() { ESP_LOGV(TAG, "transfer_data: buffer_length=%u, half_buffer=%u", buffer_length, half_buffer); // Use a local buffer for SPI transfers - static constexpr size_t MAX_TRANSFER_SIZE = 128; uint8_t bytes_to_send[MAX_TRANSFER_SIZE]; // First, send the RED buffer (0x26 = WRITE_COLOR) From 48ba007c22d7c542e9de8740aacb8c94ac438a10 Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Sat, 21 Feb 2026 20:16:20 +0100 Subject: [PATCH 170/197] [nrf52] print line number after crash in logs (#14165) --- esphome/__main__.py | 19 ++++++++++--- esphome/components/nrf52/__init__.py | 40 ++++++++++++++++++++++++++++ tests/unit_tests/test_main.py | 6 +++++ 3 files changed, 62 insertions(+), 3 deletions(-) diff --git a/esphome/__main__.py b/esphome/__main__.py index 488955f503..ffedb90bde 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -431,6 +431,14 @@ def run_miniterm(config: ConfigType, port: str, args) -> int: return 1 _LOGGER.info("Starting log output from %s with baud rate %s", port, baud_rate) + process_stacktrace = None + + try: + module = importlib.import_module("esphome.components." + CORE.target_platform) + process_stacktrace = getattr(module, "process_stacktrace") + except AttributeError: + pass + backtrace_state = False ser = serial.Serial() ser.baudrate = baud_rate @@ -472,9 +480,14 @@ def run_miniterm(config: ConfigType, port: str, args) -> int: ) safe_print(parser.parse_line(line, time_str)) - backtrace_state = platformio_api.process_stacktrace( - config, line, backtrace_state=backtrace_state - ) + if process_stacktrace: + backtrace_state = process_stacktrace( + config, line, backtrace_state + ) + else: + backtrace_state = platformio_api.process_stacktrace( + config, line, backtrace_state=backtrace_state + ) except serial.SerialException: _LOGGER.error("Serial port closed!") return 0 diff --git a/esphome/components/nrf52/__init__.py b/esphome/components/nrf52/__init__.py index 95e3670124..a12d1db1ab 100644 --- a/esphome/components/nrf52/__init__.py +++ b/esphome/components/nrf52/__init__.py @@ -3,6 +3,8 @@ from __future__ import annotations import asyncio import logging from pathlib import Path +import re +import subprocess from esphome import pins import esphome.codegen as cg @@ -380,3 +382,41 @@ def show_logs(config: ConfigType, args, devices: list[str]) -> bool: asyncio.run(logger_connect(address)) return True return False + + +def _addr2line(addr2line: str, elf: Path, addr: str) -> str: + try: + result = subprocess.run( + [addr2line, "-e", elf, addr], + capture_output=True, + text=True, + check=True, + ) + return result.stdout.strip().splitlines()[0] + except Exception as err: # pylint: disable=broad-except + _LOGGER.error("Running command failed: %s", err) + return "" + + +def process_stacktrace(config: ConfigType, line: str, backtrace_state: bool) -> bool: + if "Last crash:" in line: + return True + if backtrace_state: + match = re.search(r"PC=(0x[0-9a-fA-F]+)\s+LR=(0x[0-9a-fA-F]+)", line) + if match: + pc = match.group(1) + lr = match.group(2) + from esphome.analyze_memory.toolchain import find_tool + + addr2line = find_tool("addr2line") + if addr2line is None: + return False + elf = CORE.relative_pioenvs_path(CORE.name, "firmware.elf") + if not elf.exists(): + _LOGGER.warning("%s does not exists", elf) + return False + _LOGGER.error("=== CRASH ===") + _LOGGER.error("PC: %s", _addr2line(addr2line, elf, pc)) + _LOGGER.error("LR: %s", _addr2line(addr2line, elf, lr)) + + return False diff --git a/tests/unit_tests/test_main.py b/tests/unit_tests/test_main.py index c9aa446323..cef561c54b 100644 --- a/tests/unit_tests/test_main.py +++ b/tests/unit_tests/test_main.py @@ -2951,6 +2951,7 @@ def test_run_miniterm_batches_lines_with_same_timestamp( mock_serial = MockSerial([chunk, MOCK_SERIAL_END]) + CORE.data[KEY_CORE] = {KEY_TARGET_PLATFORM: PLATFORM_ESP32} config = { CONF_LOGGER: { CONF_BAUD_RATE: 115200, @@ -2989,6 +2990,7 @@ def test_run_miniterm_different_chunks_different_timestamps( mock_serial = MockSerial([chunk1, chunk2, MOCK_SERIAL_END]) + CORE.data[KEY_CORE] = {KEY_TARGET_PLATFORM: PLATFORM_ESP32} config = { CONF_LOGGER: { CONF_BAUD_RATE: 115200, @@ -3019,6 +3021,7 @@ def test_run_miniterm_handles_split_lines() -> None: mock_serial = MockSerial([chunk1, chunk2, MOCK_SERIAL_END]) + CORE.data[KEY_CORE] = {KEY_TARGET_PLATFORM: PLATFORM_ESP32} config = { CONF_LOGGER: { CONF_BAUD_RATE: 115200, @@ -3057,6 +3060,7 @@ def test_run_miniterm_backtrace_state_maintained() -> None: mock_serial = MockSerial([backtrace_chunk, MOCK_SERIAL_END]) + CORE.data[KEY_CORE] = {KEY_TARGET_PLATFORM: PLATFORM_ESP32} config = { CONF_LOGGER: { CONF_BAUD_RATE: 115200, @@ -3122,6 +3126,7 @@ def test_run_miniterm_handles_empty_reads( mock_serial = MockSerial([b"", chunk, b"", MOCK_SERIAL_END]) + CORE.data[KEY_CORE] = {KEY_TARGET_PLATFORM: PLATFORM_ESP32} config = { CONF_LOGGER: { CONF_BAUD_RATE: 115200, @@ -3194,6 +3199,7 @@ def test_run_miniterm_buffer_limit_prevents_unbounded_growth() -> None: mock_serial = MockSerial([large_data_no_newline, final_line, MOCK_SERIAL_END]) + CORE.data[KEY_CORE] = {KEY_TARGET_PLATFORM: PLATFORM_ESP32} config = { CONF_LOGGER: { CONF_BAUD_RATE: 115200, From 9571a979eb7ff1d881a4a833a639ff54c861174e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 21 Feb 2026 13:53:45 -0600 Subject: [PATCH 171/197] [ci] Suggest StringRef instead of std::string_view (#14183) --- script/ci-custom.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/script/ci-custom.py b/script/ci-custom.py index 231f587068..df819e0f04 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -494,6 +494,22 @@ def lint_no_byte_datatype(fname, match): ) +@lint_re_check( + r"(?:std\s*::\s*string_view|#include\s*)" + CPP_RE_EOL, + include=cpp_include, +) +def lint_no_std_string_view(fname, match): + return ( + f"{highlight('std::string_view')} is not allowed in ESPHome. " + f"It pulls in significant STL template machinery that bloats flash on " + f"resource-constrained embedded targets, does not work well with ArduinoJson, " + f"and duplicates functionality already provided by {highlight('StringRef')}.\n" + f"Please use {highlight('StringRef')} from {highlight('esphome/core/string_ref.h')} " + f"for non-owning string references, or {highlight('const char *')} for simple cases.\n" + f"(If strictly necessary, add `{highlight('// NOLINT')}` to the end of the line)" + ) + + @lint_post_check def lint_constants_usage(): errs = [] From 5a07908dfa9ef499b20e8a53f23e5ea94b9eb682 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 21 Feb 2026 13:54:20 -0600 Subject: [PATCH 172/197] [api] Fix build error when lambda returns StringRef in homeassistant.event data (#14187) --- esphome/components/api/homeassistant_service.h | 2 ++ tests/components/homeassistant/common.yaml | 13 +++++++++++++ 2 files changed, 15 insertions(+) diff --git a/esphome/components/api/homeassistant_service.h b/esphome/components/api/homeassistant_service.h index 2322d96eef..340699e1a6 100644 --- a/esphome/components/api/homeassistant_service.h +++ b/esphome/components/api/homeassistant_service.h @@ -36,6 +36,8 @@ template class TemplatableStringValue : public TemplatableValue() {} diff --git a/tests/components/homeassistant/common.yaml b/tests/components/homeassistant/common.yaml index 9c6cb71b8b..60e3defd49 100644 --- a/tests/components/homeassistant/common.yaml +++ b/tests/components/homeassistant/common.yaml @@ -90,6 +90,19 @@ text_sensor: id: ha_hello_world_text2 attribute: some_attribute +event: + - platform: template + name: Test Event + id: test_event + event_types: + - test_event_type + on_event: + - homeassistant.event: + event: esphome.test_event + data: + event_name: !lambda |- + return event_type; + time: - platform: homeassistant on_time: From e521522b388dce7066ade34d96869187e7c47ca6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 21 Feb 2026 13:54:43 -0600 Subject: [PATCH 173/197] [haier] Fix uninitialized HonSettings causing API connection failures (#14188) --- esphome/components/haier/hon_climate.h | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/esphome/components/haier/hon_climate.h b/esphome/components/haier/hon_climate.h index a567ab1d89..608d5e7f21 100644 --- a/esphome/components/haier/hon_climate.h +++ b/esphome/components/haier/hon_climate.h @@ -29,10 +29,10 @@ enum class CleaningState : uint8_t { enum class HonControlMethod { MONITOR_ONLY = 0, SET_GROUP_PARAMETERS, SET_SINGLE_PARAMETER }; struct HonSettings { - hon_protocol::VerticalSwingMode last_vertiacal_swing; - hon_protocol::HorizontalSwingMode last_horizontal_swing; - bool beeper_state; - bool quiet_mode_state; + hon_protocol::VerticalSwingMode last_vertiacal_swing{hon_protocol::VerticalSwingMode::CENTER}; + hon_protocol::HorizontalSwingMode last_horizontal_swing{hon_protocol::HorizontalSwingMode::CENTER}; + bool beeper_state{true}; + bool quiet_mode_state{false}; }; class HonClimate : public HaierClimateBase { @@ -189,7 +189,7 @@ class HonClimate : public HaierClimateBase { int big_data_sensors_{0}; esphome::optional current_vertical_swing_{}; esphome::optional current_horizontal_swing_{}; - HonSettings settings_; + HonSettings settings_{}; ESPPreferenceObject hon_rtc_; SwitchState quiet_mode_state_{SwitchState::OFF}; }; From 6f198adb0c79bcc8e14cdb07cbf484bb23aa1e52 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 21 Feb 2026 14:29:50 -0600 Subject: [PATCH 174/197] [scheduler] Reduce lock acquisitions in process_defer_queue_ (#14107) --- esphome/core/scheduler.cpp | 23 +++++++++++ esphome/core/scheduler.h | 79 ++++++++++++++++++-------------------- 2 files changed, 60 insertions(+), 42 deletions(-) diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index e82efcc520..b810df7e1c 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -421,6 +421,29 @@ void Scheduler::full_cleanup_removed_items_() { this->to_remove_ = 0; } +#ifndef ESPHOME_THREAD_SINGLE +void Scheduler::compact_defer_queue_locked_() { + // Rare case: new items were added during processing - compact the vector + // This only happens when: + // 1. A deferred callback calls defer() again, or + // 2. Another thread calls defer() while we're processing + // + // Move unprocessed items (added during this loop) to the front for next iteration + // + // SAFETY: Compacted items may include cancelled items (marked for removal via + // cancel_item_locked_() during execution). This is safe because should_skip_item_() + // checks is_item_removed_() before executing, so cancelled items will be skipped + // and recycled on the next loop iteration. + size_t remaining = this->defer_queue_.size() - this->defer_queue_front_; + for (size_t i = 0; i < remaining; i++) { + this->defer_queue_[i] = std::move(this->defer_queue_[this->defer_queue_front_ + i]); + } + // Use erase() instead of resize() to avoid instantiating _M_default_append + // (saves ~156 bytes flash). Erasing from the end is O(1) - no shifting needed. + this->defer_queue_.erase(this->defer_queue_.begin() + remaining, this->defer_queue_.end()); +} +#endif /* not ESPHOME_THREAD_SINGLE */ + void HOT Scheduler::call(uint32_t now) { #ifndef ESPHOME_THREAD_SINGLE this->process_defer_queue_(now); diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index afe11aaca6..041c6f4687 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -387,41 +387,46 @@ class Scheduler { // No lock needed: single consumer (main loop), stale read just means we process less this iteration size_t defer_queue_end = this->defer_queue_.size(); + // Fast path: nothing to process, avoid lock entirely. + // Safe without lock: single consumer (main loop) reads front_, and a stale size() read + // from a concurrent push can only make us see fewer items — they'll be processed next loop. + if (this->defer_queue_front_ >= defer_queue_end) + return; + + // Merge lock acquisitions: instead of separate locks for move-out and recycle (2N+1 total), + // recycle each item after re-acquiring the lock for the next iteration (N+1 total). + // The lock is held across: recycle → loop condition → move-out, then released for execution. + std::unique_ptr item; + + this->lock_.lock(); while (this->defer_queue_front_ < defer_queue_end) { - std::unique_ptr item; - { - LockGuard lock(this->lock_); - // SAFETY: Moving out the unique_ptr leaves a nullptr in the vector at defer_queue_front_. - // This is intentional and safe because: - // 1. The vector is only cleaned up by cleanup_defer_queue_locked_() at the end of this function - // 2. Any code iterating defer_queue_ MUST check for nullptr items (see mark_matching_items_removed_locked_ - // and has_cancelled_timeout_in_container_locked_ in scheduler.h) - // 3. The lock protects concurrent access, but the nullptr remains until cleanup - item = std::move(this->defer_queue_[this->defer_queue_front_]); - this->defer_queue_front_++; - } + // SAFETY: Moving out the unique_ptr leaves a nullptr in the vector at defer_queue_front_. + // This is intentional and safe because: + // 1. The vector is only cleaned up by cleanup_defer_queue_locked_() at the end of this function + // 2. Any code iterating defer_queue_ MUST check for nullptr items (see mark_matching_items_removed_locked_ + // and has_cancelled_timeout_in_container_locked_ in scheduler.h) + // 3. The lock protects concurrent access, but the nullptr remains until cleanup + item = std::move(this->defer_queue_[this->defer_queue_front_]); + this->defer_queue_front_++; + this->lock_.unlock(); // Execute callback without holding lock to prevent deadlocks // if the callback tries to call defer() again if (!this->should_skip_item_(item.get())) { now = this->execute_item_(item.get(), now); } - // Recycle the defer item after execution - { - LockGuard lock(this->lock_); - this->recycle_item_main_loop_(std::move(item)); - } - } - // If we've consumed all items up to the snapshot point, clean up the dead space - // Single consumer (main loop), so no lock needed for this check - if (this->defer_queue_front_ >= defer_queue_end) { - LockGuard lock(this->lock_); - this->cleanup_defer_queue_locked_(); + this->lock_.lock(); + this->recycle_item_main_loop_(std::move(item)); } + // Clean up the queue (lock already held from last recycle or initial acquisition) + this->cleanup_defer_queue_locked_(); + this->lock_.unlock(); } - // Helper to cleanup defer_queue_ after processing + // Helper to cleanup defer_queue_ after processing. + // Keeps the common clear() path inline, outlines the rare compaction to keep + // cold code out of the hot instruction cache lines. // IMPORTANT: Caller must hold the scheduler lock before calling this function. inline void cleanup_defer_queue_locked_() { // Check if new items were added by producers during processing @@ -429,27 +434,17 @@ class Scheduler { // Common case: no new items - clear everything this->defer_queue_.clear(); } else { - // Rare case: new items were added during processing - compact the vector - // This only happens when: - // 1. A deferred callback calls defer() again, or - // 2. Another thread calls defer() while we're processing - // - // Move unprocessed items (added during this loop) to the front for next iteration - // - // SAFETY: Compacted items may include cancelled items (marked for removal via - // cancel_item_locked_() during execution). This is safe because should_skip_item_() - // checks is_item_removed_() before executing, so cancelled items will be skipped - // and recycled on the next loop iteration. - size_t remaining = this->defer_queue_.size() - this->defer_queue_front_; - for (size_t i = 0; i < remaining; i++) { - this->defer_queue_[i] = std::move(this->defer_queue_[this->defer_queue_front_ + i]); - } - // Use erase() instead of resize() to avoid instantiating _M_default_append - // (saves ~156 bytes flash). Erasing from the end is O(1) - no shifting needed. - this->defer_queue_.erase(this->defer_queue_.begin() + remaining, this->defer_queue_.end()); + // Rare case: new items were added during processing - outlined to keep cold code + // out of the hot instruction cache lines + this->compact_defer_queue_locked_(); } this->defer_queue_front_ = 0; } + + // Cold path for compacting defer_queue_ when new items were added during processing. + // IMPORTANT: Caller must hold the scheduler lock before calling this function. + // IMPORTANT: Must not be inlined - rare path, outlined to keep it out of the hot instruction cache lines. + void __attribute__((noinline)) compact_defer_queue_locked_(); #endif /* not ESPHOME_THREAD_SINGLE */ // Helper to check if item is marked for removal (platform-specific) From 462ac2956312f21e506b1e2e13a6960e18d0d813 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 21 Feb 2026 17:29:41 -0600 Subject: [PATCH 175/197] [scheduler] Use relaxed memory ordering for atomic reads under lock (#14140) --- esphome/core/scheduler.cpp | 10 +++++----- esphome/core/scheduler.h | 18 +++++++++++++++--- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index b810df7e1c..36b65f6ff7 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -406,7 +406,7 @@ void Scheduler::full_cleanup_removed_items_() { // Compact in-place: move valid items forward, recycle removed ones size_t write = 0; for (size_t read = 0; read < this->items_.size(); ++read) { - if (!is_item_removed_(this->items_[read].get())) { + if (!is_item_removed_locked_(this->items_[read].get())) { if (write != read) { this->items_[write] = std::move(this->items_[read]); } @@ -531,7 +531,7 @@ void HOT Scheduler::call(uint32_t now) { // Multi-threaded platforms without atomics: must take lock to safely read remove flag { LockGuard guard{this->lock_}; - if (is_item_removed_(item.get())) { + if (is_item_removed_locked_(item.get())) { this->recycle_item_main_loop_(this->pop_raw_locked_()); this->to_remove_--; continue; @@ -568,7 +568,7 @@ void HOT Scheduler::call(uint32_t now) { // during the function call and know if we were cancelled. auto executed_item = this->pop_raw_locked_(); - if (executed_item->remove) { + if (this->is_item_removed_locked_(executed_item.get())) { // We were removed/cancelled in the function call, recycle and continue this->to_remove_--; this->recycle_item_main_loop_(std::move(executed_item)); @@ -595,7 +595,7 @@ void HOT Scheduler::call(uint32_t now) { void HOT Scheduler::process_to_add() { LockGuard guard{this->lock_}; for (auto &it : this->to_add_) { - if (is_item_removed_(it.get())) { + if (is_item_removed_locked_(it.get())) { // Recycle cancelled items this->recycle_item_main_loop_(std::move(it)); continue; @@ -628,7 +628,7 @@ size_t HOT Scheduler::cleanup_() { LockGuard guard{this->lock_}; while (!this->items_.empty()) { auto &item = this->items_[0]; - if (!item->remove) + if (!this->is_item_removed_locked_(item.get())) break; this->to_remove_--; this->recycle_item_main_loop_(this->pop_raw_locked_()); diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index 041c6f4687..384d76b6b0 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -314,8 +314,8 @@ class Scheduler { // Fixes: https://github.com/esphome/esphome/issues/11940 if (!item) return false; - if (item->component != component || item->type != type || (skip_removed && item->remove) || - (match_retry && !item->is_retry)) { + if (item->component != component || item->type != type || + (skip_removed && this->is_item_removed_locked_(item.get())) || (match_retry && !item->is_retry)) { return false; } // Name type must match @@ -463,6 +463,18 @@ class Scheduler { #endif } + // Helper to check if item is marked for removal when lock is already held. + // Uses relaxed ordering since the mutex provides all necessary synchronization. + // IMPORTANT: Caller must hold the scheduler lock before calling this function. + bool is_item_removed_locked_(SchedulerItem *item) const { +#ifdef ESPHOME_THREAD_MULTI_ATOMICS + // Lock already held - relaxed is sufficient, mutex provides ordering + return item->remove.load(std::memory_order_relaxed); +#else + return item->remove; +#endif + } + // Helper to set item removal flag (platform-specific) // For ESPHOME_THREAD_MULTI_NO_ATOMICS platforms, the caller must hold the scheduler lock before calling this // function. Uses memory_order_release when setting to true (for cancellation synchronization), @@ -519,7 +531,7 @@ class Scheduler { // it will iterate over these nullptr items. This check prevents crashes. if (!item) continue; - if (is_item_removed_(item.get()) && + if (this->is_item_removed_locked_(item.get()) && this->matches_item_locked_(item, component, name_type, static_name, hash_or_id, SchedulerItem::TIMEOUT, match_retry, /* skip_removed= */ false)) { return true; From a4682615234ac61ff7825e2a922807b11e6772fa Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 21 Feb 2026 19:41:26 -0600 Subject: [PATCH 176/197] [scheduler] De-template and consolidate scheduler helper functions (#14164) --- esphome/core/scheduler.cpp | 14 +++++++++---- esphome/core/scheduler.h | 41 +++++++------------------------------- 2 files changed, 17 insertions(+), 38 deletions(-) diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index 36b65f6ff7..e4e0751e10 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -119,10 +119,16 @@ uint32_t Scheduler::calculate_interval_offset_(uint32_t delay) { // Remove before 2026.8.0 along with all retry code bool Scheduler::is_retry_cancelled_locked_(Component *component, NameType name_type, const char *static_name, uint32_t hash_or_id) { - return has_cancelled_timeout_in_container_locked_(this->items_, component, name_type, static_name, hash_or_id, - /* match_retry= */ true) || - has_cancelled_timeout_in_container_locked_(this->to_add_, component, name_type, static_name, hash_or_id, - /* match_retry= */ true); + for (auto *container : {&this->items_, &this->to_add_}) { + for (auto &item : *container) { + if (item && this->is_item_removed_locked_(item.get()) && + this->matches_item_locked_(item, component, name_type, static_name, hash_or_id, SchedulerItem::TIMEOUT, + /* match_retry= */ true, /* skip_removed= */ false)) { + return true; + } + } + } + return false; } // Common implementation for both timeout and interval diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index 384d76b6b0..16b0ded312 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -308,8 +308,8 @@ class Scheduler { SchedulerItem::Type type, bool match_retry, bool skip_removed = true) const { // THREAD SAFETY: Check for nullptr first to prevent LoadProhibited crashes. On multi-threaded // platforms, items can be moved out of defer_queue_ during processing, leaving nullptr entries. - // PR #11305 added nullptr checks in callers (mark_matching_items_removed_locked_() and - // has_cancelled_timeout_in_container_locked_()), but this check provides defense-in-depth: helper + // PR #11305 added nullptr checks in callers (mark_matching_items_removed_locked_()), but this check + // provides defense-in-depth: helper // functions should be safe regardless of caller behavior. // Fixes: https://github.com/esphome/esphome/issues/11940 if (!item) @@ -403,8 +403,7 @@ class Scheduler { // SAFETY: Moving out the unique_ptr leaves a nullptr in the vector at defer_queue_front_. // This is intentional and safe because: // 1. The vector is only cleaned up by cleanup_defer_queue_locked_() at the end of this function - // 2. Any code iterating defer_queue_ MUST check for nullptr items (see mark_matching_items_removed_locked_ - // and has_cancelled_timeout_in_container_locked_ in scheduler.h) + // 2. Any code iterating defer_queue_ MUST check for nullptr items (see mark_matching_items_removed_locked_) // 3. The lock protects concurrent access, but the nullptr remains until cleanup item = std::move(this->defer_queue_[this->defer_queue_front_]); this->defer_queue_front_++; @@ -497,19 +496,16 @@ class Scheduler { // name_type determines matching: STATIC_STRING uses static_name, others use hash_or_id // Returns the number of items marked for removal // IMPORTANT: Must be called with scheduler lock held - template - size_t mark_matching_items_removed_locked_(Container &container, Component *component, NameType name_type, - const char *static_name, uint32_t hash_or_id, SchedulerItem::Type type, - bool match_retry) { + size_t mark_matching_items_removed_locked_(std::vector> &container, + Component *component, NameType name_type, const char *static_name, + uint32_t hash_or_id, SchedulerItem::Type type, bool match_retry) { size_t count = 0; for (auto &item : container) { // Skip nullptr items (can happen in defer_queue_ when items are being processed) // The defer_queue_ uses index-based processing: items are std::moved out but left in the // vector as nullptr until cleanup. Even though this function is called with lock held, // the vector can still contain nullptr items from the processing loop. This check prevents crashes. - if (!item) - continue; - if (this->matches_item_locked_(item, component, name_type, static_name, hash_or_id, type, match_retry)) { + if (item && this->matches_item_locked_(item, component, name_type, static_name, hash_or_id, type, match_retry)) { this->set_item_removed_(item.get(), true); count++; } @@ -517,29 +513,6 @@ class Scheduler { return count; } - // Template helper to check if any item in a container matches our criteria - // name_type determines matching: STATIC_STRING uses static_name, others use hash_or_id - // IMPORTANT: Must be called with scheduler lock held - template - bool has_cancelled_timeout_in_container_locked_(const Container &container, Component *component, NameType name_type, - const char *static_name, uint32_t hash_or_id, - bool match_retry) const { - for (const auto &item : container) { - // Skip nullptr items (can happen in defer_queue_ when items are being processed) - // The defer_queue_ uses index-based processing: items are std::moved out but left in the - // vector as nullptr until cleanup. If this function is called during defer queue processing, - // it will iterate over these nullptr items. This check prevents crashes. - if (!item) - continue; - if (this->is_item_removed_locked_(item.get()) && - this->matches_item_locked_(item, component, name_type, static_name, hash_or_id, SchedulerItem::TIMEOUT, - match_retry, /* skip_removed= */ false)) { - return true; - } - } - return false; - } - Mutex lock_; std::vector> items_; std::vector> to_add_; From d5c9c56fdfcdd0112e1913f05f84e297908460a7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 21 Feb 2026 19:41:43 -0600 Subject: [PATCH 177/197] [platformio] Add exponential backoff and session reset to download retries (#14191) --- esphome/platformio_api.py | 41 ++++- tests/unit_tests/test_platformio_api.py | 196 +++++++++++++++++++++++- 2 files changed, 231 insertions(+), 6 deletions(-) diff --git a/esphome/platformio_api.py b/esphome/platformio_api.py index d42f89d029..5d4065207f 100644 --- a/esphome/platformio_api.py +++ b/esphome/platformio_api.py @@ -5,6 +5,7 @@ import os from pathlib import Path import re import subprocess +import time from typing import Any from esphome.const import CONF_COMPILE_PROCESS_LIMIT, CONF_ESPHOME, KEY_CORE @@ -44,31 +45,61 @@ def patch_structhash(): def patch_file_downloader(): - """Patch PlatformIO's FileDownloader to retry on PackageException errors.""" + """Patch PlatformIO's FileDownloader to retry on PackageException errors. + + PlatformIO's FileDownloader uses HTTPSession which lacks built-in retry + for 502/503 errors. We add retries with exponential backoff and close the + session between attempts to force a fresh TCP connection, which may route + to a different CDN edge node. + """ from platformio.package.download import FileDownloader from platformio.package.exception import PackageException + if getattr(FileDownloader.__init__, "_esphome_patched", False): + return + original_init = FileDownloader.__init__ def patched_init(self, *args: Any, **kwargs: Any) -> None: - max_retries = 3 + max_retries = 5 for attempt in range(max_retries): try: - return original_init(self, *args, **kwargs) + original_init(self, *args, **kwargs) + return except PackageException as e: if attempt < max_retries - 1: + # Exponential backoff: 2, 4, 8, 16 seconds + delay = 2 ** (attempt + 1) _LOGGER.warning( - "Package download failed: %s. Retrying... (attempt %d/%d)", + "Package download failed: %s. " + "Retrying in %d seconds... (attempt %d/%d)", str(e), + delay, attempt + 1, max_retries, ) + # Close the response and session to free resources + # and force a new TCP connection on retry, which may + # route to a different CDN edge node + # pylint: disable=protected-access,broad-except + try: + if ( + hasattr(self, "_http_response") + and self._http_response is not None + ): + self._http_response.close() + if hasattr(self, "_http_session"): + self._http_session.close() + except Exception: + pass + # pylint: enable=protected-access,broad-except + time.sleep(delay) else: # Final attempt - re-raise raise - return None + patched_init._esphome_patched = True # type: ignore[attr-defined] # pylint: disable=protected-access FileDownloader.__init__ = patched_init diff --git a/tests/unit_tests/test_platformio_api.py b/tests/unit_tests/test_platformio_api.py index 4d7b635e59..1686144277 100644 --- a/tests/unit_tests/test_platformio_api.py +++ b/tests/unit_tests/test_platformio_api.py @@ -6,7 +6,7 @@ import os from pathlib import Path import shutil from types import SimpleNamespace -from unittest.mock import MagicMock, Mock, patch +from unittest.mock import MagicMock, Mock, call, patch import pytest @@ -673,6 +673,200 @@ def test_process_stacktrace_bad_alloc( assert state is False +def test_patch_file_downloader_succeeds_first_try() -> None: + """Test patch_file_downloader succeeds on first attempt.""" + mock_exception_cls = type("PackageException", (Exception,), {}) + original_init = MagicMock() + + with patch.dict( + "sys.modules", + { + "platformio": MagicMock(), + "platformio.package": MagicMock(), + "platformio.package.download": SimpleNamespace( + FileDownloader=type("FileDownloader", (), {"__init__": original_init}) + ), + "platformio.package.exception": SimpleNamespace( + PackageException=mock_exception_cls + ), + }, + ): + platformio_api.patch_file_downloader() + + from platformio.package.download import FileDownloader + + instance = object.__new__(FileDownloader) + FileDownloader.__init__(instance, "http://example.com/file.zip") + + original_init.assert_called_once() + + +def test_patch_file_downloader_retries_on_failure() -> None: + """Test patch_file_downloader retries with backoff on PackageException.""" + mock_exception_cls = type("PackageException", (Exception,), {}) + call_count = 0 + + def failing_init(self, *args, **kwargs): + nonlocal call_count + call_count += 1 + if call_count < 3: + raise mock_exception_cls(f"502 error attempt {call_count}") + + with ( + patch.dict( + "sys.modules", + { + "platformio": MagicMock(), + "platformio.package": MagicMock(), + "platformio.package.download": SimpleNamespace( + FileDownloader=type( + "FileDownloader", (), {"__init__": failing_init} + ) + ), + "platformio.package.exception": SimpleNamespace( + PackageException=mock_exception_cls + ), + }, + ), + patch("time.sleep") as mock_sleep, + ): + platformio_api.patch_file_downloader() + + from platformio.package.download import FileDownloader + + instance = object.__new__(FileDownloader) + FileDownloader.__init__(instance, "http://example.com/file.zip") + + # Should have been called 3 times (2 failures + 1 success) + assert call_count == 3 + + # Should have slept with exponential backoff: 2s, 4s + assert mock_sleep.call_count == 2 + mock_sleep.assert_any_call(2) + mock_sleep.assert_any_call(4) + + +def test_patch_file_downloader_raises_after_max_retries() -> None: + """Test patch_file_downloader raises after exhausting all retries.""" + mock_exception_cls = type("PackageException", (Exception,), {}) + + def always_failing_init(self, *args, **kwargs): + raise mock_exception_cls("502 error") + + with ( + patch.dict( + "sys.modules", + { + "platformio": MagicMock(), + "platformio.package": MagicMock(), + "platformio.package.download": SimpleNamespace( + FileDownloader=type( + "FileDownloader", (), {"__init__": always_failing_init} + ) + ), + "platformio.package.exception": SimpleNamespace( + PackageException=mock_exception_cls + ), + }, + ), + patch("time.sleep") as mock_sleep, + ): + platformio_api.patch_file_downloader() + + from platformio.package.download import FileDownloader + + instance = object.__new__(FileDownloader) + with pytest.raises(mock_exception_cls, match="502 error"): + FileDownloader.__init__(instance, "http://example.com/file.zip") + + # Should have slept 4 times (before attempts 2-5), not on final attempt + assert mock_sleep.call_count == 4 + mock_sleep.assert_has_calls([call(2), call(4), call(8), call(16)]) + + +def test_patch_file_downloader_closes_session_and_response_between_retries() -> None: + """Test patch_file_downloader closes HTTP session and response between retries.""" + mock_exception_cls = type("PackageException", (Exception,), {}) + mock_session = MagicMock() + mock_response = MagicMock() + call_count = 0 + + def failing_init_with_session(self, *args, **kwargs): + nonlocal call_count + call_count += 1 + self._http_session = mock_session + self._http_response = mock_response + if call_count < 2: + raise mock_exception_cls("502 error") + + with ( + patch.dict( + "sys.modules", + { + "platformio": MagicMock(), + "platformio.package": MagicMock(), + "platformio.package.download": SimpleNamespace( + FileDownloader=type( + "FileDownloader", + (), + {"__init__": failing_init_with_session}, + ) + ), + "platformio.package.exception": SimpleNamespace( + PackageException=mock_exception_cls + ), + }, + ), + patch("time.sleep"), + ): + platformio_api.patch_file_downloader() + + from platformio.package.download import FileDownloader + + instance = object.__new__(FileDownloader) + FileDownloader.__init__(instance, "http://example.com/file.zip") + + # Both response and session should have been closed between retries + mock_response.close.assert_called_once() + mock_session.close.assert_called_once() + + +def test_patch_file_downloader_idempotent() -> None: + """Test patch_file_downloader does not stack wrappers when called multiple times.""" + mock_exception_cls = type("PackageException", (Exception,), {}) + call_count = 0 + + def counting_init(self, *args, **kwargs): + nonlocal call_count + call_count += 1 + + with patch.dict( + "sys.modules", + { + "platformio": MagicMock(), + "platformio.package": MagicMock(), + "platformio.package.download": SimpleNamespace( + FileDownloader=type("FileDownloader", (), {"__init__": counting_init}) + ), + "platformio.package.exception": SimpleNamespace( + PackageException=mock_exception_cls + ), + }, + ): + # Patch multiple times + platformio_api.patch_file_downloader() + platformio_api.patch_file_downloader() + platformio_api.patch_file_downloader() + + from platformio.package.download import FileDownloader + + instance = object.__new__(FileDownloader) + FileDownloader.__init__(instance, "http://example.com/file.zip") + + # Should only be called once, not 3 times from stacked wrappers + assert call_count == 1 + + def test_platformio_log_filter_allows_non_platformio_messages() -> None: """Test that non-platformio logger messages are allowed through.""" log_filter = platformio_api.PlatformioLogFilter() From 49e4ae54beb20f7de57a24274c851c13a382d0e4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 21 Feb 2026 23:22:59 -0600 Subject: [PATCH 178/197] [bme68x_bsec2] Fix compilation on ESP32 Arduino (#14194) --- esphome/components/bme68x_bsec2/__init__.py | 5 ++++- tests/components/bme68x_bsec2_i2c/test.esp32-ard.yaml | 4 ++++ 2 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 tests/components/bme68x_bsec2_i2c/test.esp32-ard.yaml diff --git a/esphome/components/bme68x_bsec2/__init__.py b/esphome/components/bme68x_bsec2/__init__.py index e421efb2d6..4200b2f0b8 100644 --- a/esphome/components/bme68x_bsec2/__init__.py +++ b/esphome/components/bme68x_bsec2/__init__.py @@ -178,8 +178,11 @@ async def to_code_base(config): bsec2_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs) cg.add(var.set_bsec2_configuration(bsec2_arr, len(rhs))) - # Although this component does not use SPI, the BSEC2 Arduino library requires the SPI library + # The BSEC2 and BME68x Arduino libraries unconditionally include Wire.h and + # SPI.h in their source files, so these libraries must be available even though + # ESPHome uses its own I2C/SPI abstractions instead of the Arduino ones. if core.CORE.using_arduino: + cg.add_library("Wire", None) cg.add_library("SPI", None) cg.add_library( "BME68x Sensor library", diff --git a/tests/components/bme68x_bsec2_i2c/test.esp32-ard.yaml b/tests/components/bme68x_bsec2_i2c/test.esp32-ard.yaml new file mode 100644 index 0000000000..7c503b0ccb --- /dev/null +++ b/tests/components/bme68x_bsec2_i2c/test.esp32-ard.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-ard.yaml + +<<: !include common.yaml From e013b4867519c44b570fd6cf05872a7324845009 Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Sun, 22 Feb 2026 06:44:06 +0100 Subject: [PATCH 179/197] [nextion] Add error log for failed HTTP status during TFT upload (#14190) --- esphome/components/nextion/nextion_upload_arduino.cpp | 1 + esphome/components/nextion/nextion_upload_esp32.cpp | 1 + 2 files changed, 2 insertions(+) diff --git a/esphome/components/nextion/nextion_upload_arduino.cpp b/esphome/components/nextion/nextion_upload_arduino.cpp index 220c75f9d3..a433eff883 100644 --- a/esphome/components/nextion/nextion_upload_arduino.cpp +++ b/esphome/components/nextion/nextion_upload_arduino.cpp @@ -220,6 +220,7 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { } if (code != 200 and code != 206) { + ESP_LOGE(TAG, "HTTP request failed with status %d", code); return this->upload_end_(false); } diff --git a/esphome/components/nextion/nextion_upload_esp32.cpp b/esphome/components/nextion/nextion_upload_esp32.cpp index c4e6ff7182..46352afd75 100644 --- a/esphome/components/nextion/nextion_upload_esp32.cpp +++ b/esphome/components/nextion/nextion_upload_esp32.cpp @@ -238,6 +238,7 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { esp_get_free_heap_size()); int status_code = esp_http_client_get_status_code(http_client); if (status_code != 200 && status_code != 206) { + ESP_LOGE(TAG, "HTTP request failed with status %d", status_code); return this->upload_end_(false); } From 1753074eef3ea117dff2cb02995330227b242379 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Feb 2026 15:01:07 -0600 Subject: [PATCH 180/197] [web_server_base] Remove unnecessary Component inheritance and modernize (#14204) --- esphome/components/web_server_base/__init__.py | 3 +-- .../web_server_base/web_server_base.cpp | 18 ++---------------- .../web_server_base/web_server_base.h | 18 +++++++----------- 3 files changed, 10 insertions(+), 29 deletions(-) diff --git a/esphome/components/web_server_base/__init__.py b/esphome/components/web_server_base/__init__.py index 7986ac964d..183b907ae6 100644 --- a/esphome/components/web_server_base/__init__.py +++ b/esphome/components/web_server_base/__init__.py @@ -20,7 +20,7 @@ def AUTO_LOAD(): web_server_base_ns = cg.esphome_ns.namespace("web_server_base") -WebServerBase = web_server_base_ns.class_("WebServerBase", cg.Component) +WebServerBase = web_server_base_ns.class_("WebServerBase") CONF_WEB_SERVER_BASE_ID = "web_server_base_id" CONFIG_SCHEMA = cv.Schema( @@ -33,7 +33,6 @@ CONFIG_SCHEMA = cv.Schema( @coroutine_with_priority(CoroPriority.WEB_SERVER_BASE) async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - await cg.register_component(var, config) cg.add(cg.RawExpression(f"{web_server_base_ns}::global_web_server_base = {var}")) if CORE.is_esp32: diff --git a/esphome/components/web_server_base/web_server_base.cpp b/esphome/components/web_server_base/web_server_base.cpp index 6e7097338c..dbbcd10d8d 100644 --- a/esphome/components/web_server_base/web_server_base.cpp +++ b/esphome/components/web_server_base/web_server_base.cpp @@ -1,19 +1,11 @@ #include "web_server_base.h" #ifdef USE_NETWORK -#include "esphome/core/application.h" -#include "esphome/core/helpers.h" -#include "esphome/core/log.h" -namespace esphome { -namespace web_server_base { - -static const char *const TAG = "web_server_base"; +namespace esphome::web_server_base { WebServerBase *global_web_server_base = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) void WebServerBase::add_handler(AsyncWebHandler *handler) { - // remove all handlers - #ifdef USE_WEBSERVER_AUTH if (!credentials_.username.empty()) { handler = new internal::AuthMiddlewareHandler(handler, &credentials_); @@ -25,11 +17,5 @@ void WebServerBase::add_handler(AsyncWebHandler *handler) { } } -float WebServerBase::get_setup_priority() const { - // Before WiFi (captive portal) - return setup_priority::WIFI + 2.0f; -} - -} // namespace web_server_base -} // namespace esphome +} // namespace esphome::web_server_base #endif diff --git a/esphome/components/web_server_base/web_server_base.h b/esphome/components/web_server_base/web_server_base.h index 0c25467f1b..54421c851e 100644 --- a/esphome/components/web_server_base/web_server_base.h +++ b/esphome/components/web_server_base/web_server_base.h @@ -1,11 +1,9 @@ #pragma once #include "esphome/core/defines.h" #ifdef USE_NETWORK -#include #include #include -#include "esphome/core/component.h" #include "esphome/core/progmem.h" #if USE_ESP32 @@ -21,8 +19,7 @@ using PlatformString = std::string; using PlatformString = String; #endif -namespace esphome { -namespace web_server_base { +namespace esphome::web_server_base { class WebServerBase; extern WebServerBase *global_web_server_base; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) @@ -91,14 +88,14 @@ class AuthMiddlewareHandler : public MiddlewareHandler { } // namespace internal -class WebServerBase : public Component { +class WebServerBase { public: void init() { if (this->initialized_) { this->initialized_++; return; } - this->server_ = std::make_unique(this->port_); + this->server_ = new AsyncWebServer(this->port_); // All content is controlled and created by user - so allowing all origins is fine here. // NOTE: Currently 1 header. If more are added, update in __init__.py: // cg.add_define("WEB_SERVER_DEFAULT_HEADERS_COUNT", 1) @@ -113,11 +110,11 @@ class WebServerBase : public Component { void deinit() { this->initialized_--; if (this->initialized_ == 0) { + delete this->server_; this->server_ = nullptr; } } - AsyncWebServer *get_server() const { return this->server_.get(); } - float get_setup_priority() const override; + AsyncWebServer *get_server() const { return this->server_; } #ifdef USE_WEBSERVER_AUTH void set_auth_username(std::string auth_username) { credentials_.username = std::move(auth_username); } @@ -132,13 +129,12 @@ class WebServerBase : public Component { protected: int initialized_{0}; uint16_t port_{80}; - std::unique_ptr server_{nullptr}; + AsyncWebServer *server_{nullptr}; std::vector handlers_; #ifdef USE_WEBSERVER_AUTH internal::Credentials credentials_; #endif }; -} // namespace web_server_base -} // namespace esphome +} // namespace esphome::web_server_base #endif From 509f06afac19fc54b3fae2a67802771312b91133 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Feb 2026 15:56:09 -0600 Subject: [PATCH 181/197] [network] Improve IPAddress::str() deprecation warning with usage example (#14195) --- esphome/components/network/ip_address.h | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/esphome/components/network/ip_address.h b/esphome/components/network/ip_address.h index d0ac8164af..b2a2c563e2 100644 --- a/esphome/components/network/ip_address.h +++ b/esphome/components/network/ip_address.h @@ -61,7 +61,9 @@ 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; } // Remove before 2026.8.0 - ESPDEPRECATED("Use str_to() instead. Removed in 2026.8.0", "2026.2.0") + ESPDEPRECATED( + "str() is deprecated: use 'char buf[IP_ADDRESS_BUFFER_SIZE]; ip.str_to(buf);' instead. Removed in 2026.8.0", + "2026.2.0") std::string str() const { char buf[IP_ADDRESS_BUFFER_SIZE]; this->str_to(buf); @@ -150,7 +152,9 @@ struct IPAddress { bool is_ip6() const { return IP_IS_V6(&ip_addr_); } bool is_multicast() const { return ip_addr_ismulticast(&ip_addr_); } // Remove before 2026.8.0 - ESPDEPRECATED("Use str_to() instead. Removed in 2026.8.0", "2026.2.0") + ESPDEPRECATED( + "str() is deprecated: use 'char buf[IP_ADDRESS_BUFFER_SIZE]; ip.str_to(buf);' instead. Removed in 2026.8.0", + "2026.2.0") std::string str() const { char buf[IP_ADDRESS_BUFFER_SIZE]; this->str_to(buf); From ede2da2fbc167814c5598adb6d866c239a0772c8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Feb 2026 15:57:23 -0600 Subject: [PATCH 182/197] [core] Conditionally compile get_loop_priority with USE_LOOP_PRIORITY (#14210) --- esphome/components/ch422g/ch422g.cpp | 2 ++ esphome/components/ch422g/ch422g.h | 2 ++ esphome/components/ch423/ch423.cpp | 2 ++ esphome/components/ch423/ch423.h | 2 ++ esphome/components/deep_sleep/deep_sleep_component.cpp | 2 ++ esphome/components/deep_sleep/deep_sleep_component.h | 2 ++ esphome/components/pca9554/pca9554.cpp | 2 ++ esphome/components/pca9554/pca9554.h | 2 ++ esphome/components/pcf8574/pcf8574.cpp | 2 ++ esphome/components/pcf8574/pcf8574.h | 2 ++ esphome/components/status_led/light/status_led_light.h | 2 ++ esphome/components/status_led/status_led.cpp | 2 ++ esphome/components/status_led/status_led.h | 2 ++ esphome/components/wifi/__init__.py | 7 +++++++ esphome/components/wifi/wifi_component.cpp | 2 ++ esphome/components/wifi/wifi_component.h | 2 ++ esphome/core/application.cpp | 2 ++ esphome/core/component.cpp | 2 ++ esphome/core/component.h | 3 +++ esphome/core/defines.h | 1 + 20 files changed, 45 insertions(+) diff --git a/esphome/components/ch422g/ch422g.cpp b/esphome/components/ch422g/ch422g.cpp index eef95b9ba2..5f5e848c76 100644 --- a/esphome/components/ch422g/ch422g.cpp +++ b/esphome/components/ch422g/ch422g.cpp @@ -124,9 +124,11 @@ bool CH422GComponent::write_outputs_() { float CH422GComponent::get_setup_priority() const { return setup_priority::IO; } +#ifdef USE_LOOP_PRIORITY // Run our loop() method very early in the loop, so that we cache read values // before other components call our digital_read() method. float CH422GComponent::get_loop_priority() const { return 9.0f; } // Just after WIFI +#endif void CH422GGPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); } bool CH422GGPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) ^ this->inverted_; } diff --git a/esphome/components/ch422g/ch422g.h b/esphome/components/ch422g/ch422g.h index 8ed63db90a..1b96568209 100644 --- a/esphome/components/ch422g/ch422g.h +++ b/esphome/components/ch422g/ch422g.h @@ -23,7 +23,9 @@ class CH422GComponent : public Component, public i2c::I2CDevice { void pin_mode(uint8_t pin, gpio::Flags flags); float get_setup_priority() const override; +#ifdef USE_LOOP_PRIORITY float get_loop_priority() const override; +#endif void dump_config() override; protected: diff --git a/esphome/components/ch423/ch423.cpp b/esphome/components/ch423/ch423.cpp index 4abbbe7adf..805d8df877 100644 --- a/esphome/components/ch423/ch423.cpp +++ b/esphome/components/ch423/ch423.cpp @@ -129,9 +129,11 @@ bool CH423Component::write_outputs_() { float CH423Component::get_setup_priority() const { return setup_priority::IO; } +#ifdef USE_LOOP_PRIORITY // Run our loop() method very early in the loop, so that we cache read values // before other components call our digital_read() method. float CH423Component::get_loop_priority() const { return 9.0f; } // Just after WIFI +#endif void CH423GPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); } bool CH423GPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) ^ this->inverted_; } diff --git a/esphome/components/ch423/ch423.h b/esphome/components/ch423/ch423.h index 7adc7de6a1..d85648a8f9 100644 --- a/esphome/components/ch423/ch423.h +++ b/esphome/components/ch423/ch423.h @@ -22,7 +22,9 @@ class CH423Component : public Component, public i2c::I2CDevice { void pin_mode(uint8_t pin, gpio::Flags flags); float get_setup_priority() const override; +#ifdef USE_LOOP_PRIORITY float get_loop_priority() const override; +#endif void dump_config() override; protected: diff --git a/esphome/components/deep_sleep/deep_sleep_component.cpp b/esphome/components/deep_sleep/deep_sleep_component.cpp index 8066b411ff..0511518419 100644 --- a/esphome/components/deep_sleep/deep_sleep_component.cpp +++ b/esphome/components/deep_sleep/deep_sleep_component.cpp @@ -40,9 +40,11 @@ void DeepSleepComponent::loop() { this->begin_sleep(); } +#ifdef USE_LOOP_PRIORITY float DeepSleepComponent::get_loop_priority() const { return -100.0f; // run after everything else is ready } +#endif void DeepSleepComponent::set_sleep_duration(uint32_t time_ms) { this->sleep_duration_ = uint64_t(time_ms) * 1000; } diff --git a/esphome/components/deep_sleep/deep_sleep_component.h b/esphome/components/deep_sleep/deep_sleep_component.h index 3e6eda2257..1998b815f3 100644 --- a/esphome/components/deep_sleep/deep_sleep_component.h +++ b/esphome/components/deep_sleep/deep_sleep_component.h @@ -113,7 +113,9 @@ class DeepSleepComponent : public Component { void setup() override; void dump_config() override; void loop() override; +#ifdef USE_LOOP_PRIORITY float get_loop_priority() const override; +#endif float get_setup_priority() const override; /// Helper to enter deep sleep mode diff --git a/esphome/components/pca9554/pca9554.cpp b/esphome/components/pca9554/pca9554.cpp index c574ce6593..d94767ef07 100644 --- a/esphome/components/pca9554/pca9554.cpp +++ b/esphome/components/pca9554/pca9554.cpp @@ -122,8 +122,10 @@ bool PCA9554Component::write_register_(uint8_t reg, uint16_t value) { float PCA9554Component::get_setup_priority() const { return setup_priority::IO; } +#ifdef USE_LOOP_PRIORITY // Run our loop() method early to invalidate cache before any other components access the pins float PCA9554Component::get_loop_priority() const { return 9.0f; } // Just after WIFI +#endif void PCA9554GPIOPin::setup() { pin_mode(flags_); } void PCA9554GPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); } diff --git a/esphome/components/pca9554/pca9554.h b/esphome/components/pca9554/pca9554.h index bf752e50c9..6dd15ccb4b 100644 --- a/esphome/components/pca9554/pca9554.h +++ b/esphome/components/pca9554/pca9554.h @@ -23,7 +23,9 @@ class PCA9554Component : public Component, float get_setup_priority() const override; +#ifdef USE_LOOP_PRIORITY float get_loop_priority() const override; +#endif void dump_config() override; diff --git a/esphome/components/pcf8574/pcf8574.cpp b/esphome/components/pcf8574/pcf8574.cpp index b7d3848f0e..fa9496e7e4 100644 --- a/esphome/components/pcf8574/pcf8574.cpp +++ b/esphome/components/pcf8574/pcf8574.cpp @@ -99,8 +99,10 @@ bool PCF8574Component::write_gpio_() { } float PCF8574Component::get_setup_priority() const { return setup_priority::IO; } +#ifdef USE_LOOP_PRIORITY // Run our loop() method early to invalidate cache before any other components access the pins float PCF8574Component::get_loop_priority() const { return 9.0f; } // Just after WIFI +#endif void PCF8574GPIOPin::setup() { pin_mode(flags_); } void PCF8574GPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); } diff --git a/esphome/components/pcf8574/pcf8574.h b/esphome/components/pcf8574/pcf8574.h index 5203030142..23bccc26c9 100644 --- a/esphome/components/pcf8574/pcf8574.h +++ b/esphome/components/pcf8574/pcf8574.h @@ -26,7 +26,9 @@ class PCF8574Component : public Component, void pin_mode(uint8_t pin, gpio::Flags flags); float get_setup_priority() const override; +#ifdef USE_LOOP_PRIORITY float get_loop_priority() const override; +#endif void dump_config() override; diff --git a/esphome/components/status_led/light/status_led_light.h b/esphome/components/status_led/light/status_led_light.h index bfa144526a..a5c98d90d4 100644 --- a/esphome/components/status_led/light/status_led_light.h +++ b/esphome/components/status_led/light/status_led_light.h @@ -30,7 +30,9 @@ class StatusLEDLightOutput : public light::LightOutput, public Component { void dump_config() override; float get_setup_priority() const override { return setup_priority::HARDWARE; } +#ifdef USE_LOOP_PRIORITY float get_loop_priority() const override { return 50.0f; } +#endif protected: GPIOPin *pin_{nullptr}; diff --git a/esphome/components/status_led/status_led.cpp b/esphome/components/status_led/status_led.cpp index 344c1e3070..93a8d4b38e 100644 --- a/esphome/components/status_led/status_led.cpp +++ b/esphome/components/status_led/status_led.cpp @@ -28,7 +28,9 @@ void StatusLED::loop() { } } float StatusLED::get_setup_priority() const { return setup_priority::HARDWARE; } +#ifdef USE_LOOP_PRIORITY float StatusLED::get_loop_priority() const { return 50.0f; } +#endif } // namespace status_led } // namespace esphome diff --git a/esphome/components/status_led/status_led.h b/esphome/components/status_led/status_led.h index 490557f3e7..f262eb260c 100644 --- a/esphome/components/status_led/status_led.h +++ b/esphome/components/status_led/status_led.h @@ -14,7 +14,9 @@ class StatusLED : public Component { void dump_config() override; void loop() override; float get_setup_priority() const override; +#ifdef USE_LOOP_PRIORITY float get_loop_priority() const override; +#endif protected: GPIOPin *pin_; diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index 540d0a0ab1..8c1deb6217 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -536,6 +536,13 @@ async def to_code(config): cg.add_library("ESP8266WiFi", None) elif CORE.is_rp2040: cg.add_library("WiFi", None) + # RP2040's mDNS library (LEAmDNS) relies on LwipIntf::stateUpCB() to restart + # mDNS when the network interface reconnects. However, this callback is disabled + # in the arduino-pico framework. As a workaround, we block component setup until + # WiFi is connected via can_proceed(), ensuring mDNS.begin() is called with an + # active connection. This define enables the loop priority sorting infrastructure + # used during the setup blocking phase. + cg.add_define("USE_LOOP_PRIORITY") if CORE.is_esp32: if config[CONF_ENABLE_BTM] or config[CONF_ENABLE_RRM]: diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 8b3060c7c3..d5d0419395 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -968,9 +968,11 @@ void WiFiComponent::set_ap(const WiFiAP &ap) { } #endif // USE_WIFI_AP +#ifdef USE_LOOP_PRIORITY float WiFiComponent::get_loop_priority() const { return 10.0f; // before other loop components } +#endif void WiFiComponent::init_sta(size_t count) { this->sta_.init(count); } void WiFiComponent::add_sta(const WiFiAP &ap) { this->sta_.push_back(ap); } diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 5f903e092a..984930c80c 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -458,7 +458,9 @@ class WiFiComponent : public Component { void restart_adapter(); /// WIFI setup_priority. float get_setup_priority() const override; +#ifdef USE_LOOP_PRIORITY float get_loop_priority() const override; +#endif /// Reconnect WiFi if required. void loop() override; diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index b216233f9b..c6597897dc 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -109,9 +109,11 @@ void Application::setup() { if (component->can_proceed()) continue; +#ifdef USE_LOOP_PRIORITY // Sort components 0 through i by loop priority insertion_sort_by_prioritycomponents_.begin()), &Component::get_loop_priority>( this->components_.begin(), this->components_.begin() + i + 1); +#endif do { uint8_t new_app_state = STATUS_LED_WARNING; diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index b458ea2a84..b4a19a0776 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -85,7 +85,9 @@ void store_component_error_message(const Component *component, const char *messa static constexpr uint16_t WARN_IF_BLOCKING_INCREMENT_MS = 10U; ///< How long the blocking time must be larger to warn again +#ifdef USE_LOOP_PRIORITY float Component::get_loop_priority() const { return 0.0f; } +#endif float Component::get_setup_priority() const { return setup_priority::DATA; } diff --git a/esphome/core/component.h b/esphome/core/component.h index b99641a275..7ea9fdf3b3 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -5,6 +5,7 @@ #include #include +#include "esphome/core/defines.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" #include "esphome/core/optional.h" @@ -117,7 +118,9 @@ class Component { * * @return The loop priority of this component */ +#ifdef USE_LOOP_PRIORITY virtual float get_loop_priority() const; +#endif void call(); diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 5109dd36f4..02335e2745 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -312,6 +312,7 @@ #ifdef USE_RP2040 #define USE_ARDUINO_VERSION_CODE VERSION_CODE(3, 3, 0) +#define USE_LOOP_PRIORITY #define USE_HTTP_REQUEST_RESPONSE #define USE_I2C #define USE_LOGGER_USB_CDC From ee1f52132573ab5498eaa566d3289d1103a4a186 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Feb 2026 16:59:54 -0600 Subject: [PATCH 183/197] [http_request] Replace std::list
with std::vector in perform() chain (#14027) --- .../components/http_request/http_request.h | 89 +++++++++++++++---- .../http_request/http_request_arduino.cpp | 2 +- .../http_request/http_request_arduino.h | 2 +- .../http_request/http_request_host.cpp | 2 +- .../http_request/http_request_host.h | 2 +- .../http_request/http_request_idf.cpp | 2 +- .../http_request/http_request_idf.h | 2 +- .../components/online_image/online_image.cpp | 2 +- 8 files changed, 79 insertions(+), 24 deletions(-) diff --git a/esphome/components/http_request/http_request.h b/esphome/components/http_request/http_request.h index 2b2d05c63f..8bdea470b5 100644 --- a/esphome/components/http_request/http_request.h +++ b/esphome/components/http_request/http_request.h @@ -330,38 +330,72 @@ class HttpRequestComponent : public Component { void set_follow_redirects(bool follow_redirects) { this->follow_redirects_ = follow_redirects; } void set_redirect_limit(uint16_t limit) { this->redirect_limit_ = limit; } - std::shared_ptr get(const std::string &url) { return this->start(url, "GET", "", {}); } - std::shared_ptr get(const std::string &url, const std::list
&request_headers) { + std::shared_ptr get(const std::string &url) { + return this->start(url, "GET", "", std::vector
{}); + } + std::shared_ptr get(const std::string &url, const std::vector
&request_headers) { return this->start(url, "GET", "", request_headers); } - std::shared_ptr get(const std::string &url, const std::list
&request_headers, + std::shared_ptr get(const std::string &url, const std::vector
&request_headers, const std::vector &lower_case_collect_headers) { return this->start(url, "GET", "", request_headers, lower_case_collect_headers); } std::shared_ptr post(const std::string &url, const std::string &body) { - return this->start(url, "POST", body, {}); + return this->start(url, "POST", body, std::vector
{}); } std::shared_ptr post(const std::string &url, const std::string &body, - const std::list
&request_headers) { + const std::vector
&request_headers) { return this->start(url, "POST", body, request_headers); } std::shared_ptr post(const std::string &url, const std::string &body, - const std::list
&request_headers, + const std::vector
&request_headers, const std::vector &lower_case_collect_headers) { return this->start(url, "POST", body, request_headers, lower_case_collect_headers); } + // Remove before 2027.1.0 + ESPDEPRECATED("Pass request_headers as std::vector
instead of std::list. Removed in 2027.1.0.", "2026.7.0") + std::shared_ptr get(const std::string &url, const std::list
&request_headers) { + return this->get(url, std::vector
(request_headers.begin(), request_headers.end())); + } + // Remove before 2027.1.0 + ESPDEPRECATED("Pass request_headers as std::vector
instead of std::list. Removed in 2027.1.0.", "2026.7.0") + std::shared_ptr get(const std::string &url, const std::list
&request_headers, + const std::vector &collect_headers) { + return this->get(url, std::vector
(request_headers.begin(), request_headers.end()), collect_headers); + } + // Remove before 2027.1.0 + ESPDEPRECATED("Pass request_headers as std::vector
instead of std::list. Removed in 2027.1.0.", "2026.7.0") + std::shared_ptr post(const std::string &url, const std::string &body, + const std::list
&request_headers) { + return this->post(url, body, std::vector
(request_headers.begin(), request_headers.end())); + } + // Remove before 2027.1.0 + ESPDEPRECATED("Pass request_headers as std::vector
instead of std::list. Removed in 2027.1.0.", "2026.7.0") + std::shared_ptr post(const std::string &url, const std::string &body, + const std::list
&request_headers, + const std::vector &collect_headers) { + return this->post(url, body, std::vector
(request_headers.begin(), request_headers.end()), collect_headers); + } + + std::shared_ptr start(const std::string &url, const std::string &method, const std::string &body, + const std::vector
&request_headers) { + // Call perform() directly to avoid ambiguity with the deprecated overloads + return this->perform(url, method, body, request_headers, {}); + } + + // Remove before 2027.1.0 + ESPDEPRECATED("Pass request_headers as std::vector
instead of std::list. Removed in 2027.1.0.", "2026.7.0") std::shared_ptr start(const std::string &url, const std::string &method, const std::string &body, const std::list
&request_headers) { - // Call perform() directly to avoid ambiguity with the std::set overload - return this->perform(url, method, body, request_headers, {}); + return this->start(url, method, body, std::vector
(request_headers.begin(), request_headers.end())); } // Remove before 2027.1.0 ESPDEPRECATED("Pass collect_headers as std::vector instead of std::set. Removed in 2027.1.0.", "2026.7.0") std::shared_ptr start(const std::string &url, const std::string &method, const std::string &body, - const std::list
&request_headers, + const std::vector
&request_headers, const std::set &collect_headers) { std::vector lower; lower.reserve(collect_headers.size()); @@ -371,15 +405,39 @@ class HttpRequestComponent : public Component { return this->perform(url, method, body, request_headers, lower); } + // Remove before 2027.1.0 + ESPDEPRECATED("Pass request_headers as std::vector
instead of std::list, and collect_headers as " + "std::vector instead of std::set. Removed in 2027.1.0.", + "2026.7.0") std::shared_ptr start(const std::string &url, const std::string &method, const std::string &body, const std::list
&request_headers, + const std::set &collect_headers) { + std::vector lower; + lower.reserve(collect_headers.size()); + for (const auto &h : collect_headers) { + lower.push_back(str_lower_case(h)); + } + return this->perform(url, method, body, std::vector
(request_headers.begin(), request_headers.end()), lower); + } + + // Remove before 2027.1.0 + ESPDEPRECATED("Pass request_headers as std::vector
instead of std::list. Removed in 2027.1.0.", "2026.7.0") + std::shared_ptr start(const std::string &url, const std::string &method, const std::string &body, + const std::list
&request_headers, + const std::vector &lower_case_collect_headers) { + return this->perform(url, method, body, std::vector
(request_headers.begin(), request_headers.end()), + lower_case_collect_headers); + } + + std::shared_ptr start(const std::string &url, const std::string &method, const std::string &body, + const std::vector
&request_headers, const std::vector &lower_case_collect_headers) { return this->perform(url, method, body, request_headers, lower_case_collect_headers); } protected: virtual std::shared_ptr perform(const std::string &url, const std::string &method, - const std::string &body, const std::list
&request_headers, + const std::string &body, const std::vector
&request_headers, const std::vector &lower_case_collect_headers) = 0; const char *useragent_{nullptr}; bool follow_redirects_{}; @@ -436,13 +494,10 @@ template class HttpRequestSendAction : public Action { auto f = std::bind(&HttpRequestSendAction::encode_json_func_, this, x..., std::placeholders::_1); body = json::build_json(f); } - std::list
request_headers; - for (const auto &item : this->request_headers_) { - auto val = item.second; - Header header; - header.name = item.first; - header.value = val.value(x...); - request_headers.push_back(header); + std::vector
request_headers; + request_headers.reserve(this->request_headers_.size()); + for (const auto &[key, val] : this->request_headers_) { + request_headers.push_back({key, val.value(x...)}); } auto container = this->parent_->start(this->url_.value(x...), this->method_.value(x...), body, request_headers, diff --git a/esphome/components/http_request/http_request_arduino.cpp b/esphome/components/http_request/http_request_arduino.cpp index 3f60b76b58..56b51e8b5a 100644 --- a/esphome/components/http_request/http_request_arduino.cpp +++ b/esphome/components/http_request/http_request_arduino.cpp @@ -26,7 +26,7 @@ static constexpr int ESP8266_SSL_ERR_OOM = -1000; std::shared_ptr HttpRequestArduino::perform(const std::string &url, const std::string &method, const std::string &body, - const std::list
&request_headers, + const std::vector
&request_headers, const std::vector &lower_case_collect_headers) { if (!network::is_connected()) { this->status_momentary_error("failed", 1000); diff --git a/esphome/components/http_request/http_request_arduino.h b/esphome/components/http_request/http_request_arduino.h index dbd61de364..d5ce5c0ff3 100644 --- a/esphome/components/http_request/http_request_arduino.h +++ b/esphome/components/http_request/http_request_arduino.h @@ -49,7 +49,7 @@ class HttpContainerArduino : public HttpContainer { class HttpRequestArduino : public HttpRequestComponent { protected: std::shared_ptr perform(const std::string &url, const std::string &method, const std::string &body, - const std::list
&request_headers, + const std::vector
&request_headers, const std::vector &lower_case_collect_headers) override; }; diff --git a/esphome/components/http_request/http_request_host.cpp b/esphome/components/http_request/http_request_host.cpp index 714a73fc31..60ab4d68a0 100644 --- a/esphome/components/http_request/http_request_host.cpp +++ b/esphome/components/http_request/http_request_host.cpp @@ -18,7 +18,7 @@ static const char *const TAG = "http_request.host"; std::shared_ptr HttpRequestHost::perform(const std::string &url, const std::string &method, const std::string &body, - const std::list
&request_headers, + const std::vector
&request_headers, const std::vector &lower_case_collect_headers) { if (!network::is_connected()) { this->status_momentary_error("failed", 1000); diff --git a/esphome/components/http_request/http_request_host.h b/esphome/components/http_request/http_request_host.h index 79f5b7e817..52be0e8a16 100644 --- a/esphome/components/http_request/http_request_host.h +++ b/esphome/components/http_request/http_request_host.h @@ -19,7 +19,7 @@ class HttpContainerHost : public HttpContainer { class HttpRequestHost : public HttpRequestComponent { public: std::shared_ptr perform(const std::string &url, const std::string &method, const std::string &body, - const std::list
&request_headers, + const std::vector
&request_headers, const std::vector &lower_case_collect_headers) override; void set_ca_path(const char *ca_path) { this->ca_path_ = ca_path; } diff --git a/esphome/components/http_request/http_request_idf.cpp b/esphome/components/http_request/http_request_idf.cpp index 0921c50b9f..dda61e2400 100644 --- a/esphome/components/http_request/http_request_idf.cpp +++ b/esphome/components/http_request/http_request_idf.cpp @@ -54,7 +54,7 @@ esp_err_t HttpRequestIDF::http_event_handler(esp_http_client_event_t *evt) { std::shared_ptr HttpRequestIDF::perform(const std::string &url, const std::string &method, const std::string &body, - const std::list
&request_headers, + const std::vector
&request_headers, const std::vector &lower_case_collect_headers) { if (!network::is_connected()) { this->status_momentary_error("failed", 1000); diff --git a/esphome/components/http_request/http_request_idf.h b/esphome/components/http_request/http_request_idf.h index 9206ba6f5d..9ed1a97b1a 100644 --- a/esphome/components/http_request/http_request_idf.h +++ b/esphome/components/http_request/http_request_idf.h @@ -37,7 +37,7 @@ class HttpRequestIDF : public HttpRequestComponent { protected: std::shared_ptr perform(const std::string &url, const std::string &method, const std::string &body, - const std::list
&request_headers, + const std::vector
&request_headers, const std::vector &lower_case_collect_headers) override; // if zero ESP-IDF will use DEFAULT_HTTP_BUF_SIZE uint16_t buffer_size_rx_{}; diff --git a/esphome/components/online_image/online_image.cpp b/esphome/components/online_image/online_image.cpp index 6f5b82116d..da866599c9 100644 --- a/esphome/components/online_image/online_image.cpp +++ b/esphome/components/online_image/online_image.cpp @@ -49,7 +49,7 @@ void OnlineImage::update() { ESP_LOGD(TAG, "Updating image from %s", this->url_.c_str()); - std::list headers; + std::vector headers; // Add caching headers if we have them if (!this->etag_.empty()) { From 5fddce6638f47c296e1267bbeeadaf677277d635 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Feb 2026 17:02:05 -0600 Subject: [PATCH 184/197] [logger] Make tx_buffer_ compile-time sized (#14205) --- esphome/components/logger/__init__.py | 3 ++- esphome/components/logger/logger.cpp | 7 ++----- esphome/components/logger/logger.h | 9 +++++---- esphome/components/logger/logger_esp32.cpp | 6 +++--- esphome/components/logger/logger_host.cpp | 4 ++-- esphome/core/defines.h | 1 + tests/dummy_main.cpp | 2 +- 7 files changed, 16 insertions(+), 16 deletions(-) diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index c8f3c52911..264197c175 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -329,10 +329,11 @@ async def to_code(config): level = config[CONF_LEVEL] CORE.data.setdefault(CONF_LOGGER, {})[CONF_LEVEL] = level initial_level = LOG_LEVELS[config.get(CONF_INITIAL_LEVEL, level)] + tx_buffer_size = config[CONF_TX_BUFFER_SIZE] + cg.add_define("ESPHOME_LOGGER_TX_BUFFER_SIZE", tx_buffer_size) log = cg.new_Pvariable( config[CONF_ID], baud_rate, - config[CONF_TX_BUFFER_SIZE], ) if CORE.is_esp32: cg.add(log.create_pthread_key()) diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index 22a95e4835..497809cd2e 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -152,10 +152,7 @@ inline uint8_t Logger::level_for(const char *tag) { return this->current_level_; } -Logger::Logger(uint32_t baud_rate, size_t tx_buffer_size) : baud_rate_(baud_rate), tx_buffer_size_(tx_buffer_size) { - // add 1 to buffer size for null terminator - // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) - allocated once, never freed - this->tx_buffer_ = new char[this->tx_buffer_size_ + 1]; +Logger::Logger(uint32_t baud_rate) : baud_rate_(baud_rate) { #if defined(USE_ESP32) || defined(USE_LIBRETINY) this->main_task_ = xTaskGetCurrentTaskHandle(); #elif defined(USE_ZEPHYR) @@ -196,7 +193,7 @@ void Logger::process_messages_() { uint16_t text_length; while (this->log_buffer_->borrow_message_main_loop(message, text_length)) { const char *thread_name = message->thread_name[0] != '\0' ? message->thread_name : nullptr; - LogBuffer buf{this->tx_buffer_, this->tx_buffer_size_}; + LogBuffer buf{this->tx_buffer_, ESPHOME_LOGGER_TX_BUFFER_SIZE}; this->format_buffered_message_and_notify_(message->level, message->tag, message->line, thread_name, message->text_data(), text_length, buf); // Release the message to allow other tasks to use it as soon as possible diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index 8bf1edebb8..90722ee79c 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -143,7 +143,7 @@ enum UARTSelection : uint8_t { */ class Logger : public Component { public: - explicit Logger(uint32_t baud_rate, size_t tx_buffer_size); + explicit Logger(uint32_t baud_rate); #ifdef USE_ESPHOME_TASK_LOG_BUFFER void init_log_buffer(size_t total_buffer_size); #endif @@ -281,7 +281,7 @@ class Logger : public Component { inline void HOT log_message_to_buffer_and_send_(bool &recursion_guard, uint8_t level, const char *tag, int line, FormatType format, va_list args, const char *thread_name) { RecursionGuard guard(recursion_guard); - LogBuffer buf{this->tx_buffer_, this->tx_buffer_size_}; + LogBuffer buf{this->tx_buffer_, ESPHOME_LOGGER_TX_BUFFER_SIZE}; #ifdef USE_STORE_LOG_STR_IN_FLASH if constexpr (std::is_same_v) { this->format_log_to_buffer_with_terminator_P_(level, tag, line, format, args, buf); @@ -312,7 +312,6 @@ class Logger : public Component { // Group 4-byte aligned members first uint32_t baud_rate_; - char *tx_buffer_{nullptr}; #if defined(USE_ARDUINO) && !defined(USE_ESP32) Stream *hw_serial_{nullptr}; #endif @@ -354,7 +353,6 @@ class Logger : public Component { #endif // Group smaller types together at the end - uint16_t tx_buffer_size_{0}; uint8_t current_level_{ESPHOME_LOG_LEVEL_VERY_VERBOSE}; #if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_ZEPHYR) UARTSelection uart_{UART_SELECTION_UART0}; @@ -371,6 +369,9 @@ class Logger : public Component { bool global_recursion_guard_{false}; // Simple global recursion guard for single-task platforms #endif + // Large buffer placed last to keep frequently-accessed member offsets small + char tx_buffer_[ESPHOME_LOGGER_TX_BUFFER_SIZE + 1]; // +1 for null terminator + // --- get_thread_name_ overloads (per-platform) --- #if defined(USE_ESP32) || defined(USE_LIBRETINY) diff --git a/esphome/components/logger/logger_esp32.cpp b/esphome/components/logger/logger_esp32.cpp index 9d64771aec..d6ad77ff4f 100644 --- a/esphome/components/logger/logger_esp32.cpp +++ b/esphome/components/logger/logger_esp32.cpp @@ -89,16 +89,16 @@ void Logger::pre_setup() { switch (this->uart_) { case UART_SELECTION_UART0: this->uart_num_ = UART_NUM_0; - init_uart(this->uart_num_, baud_rate_, tx_buffer_size_); + init_uart(this->uart_num_, baud_rate_, ESPHOME_LOGGER_TX_BUFFER_SIZE); break; case UART_SELECTION_UART1: this->uart_num_ = UART_NUM_1; - init_uart(this->uart_num_, baud_rate_, tx_buffer_size_); + init_uart(this->uart_num_, baud_rate_, ESPHOME_LOGGER_TX_BUFFER_SIZE); break; #ifdef USE_ESP32_VARIANT_ESP32 case UART_SELECTION_UART2: this->uart_num_ = UART_NUM_2; - init_uart(this->uart_num_, baud_rate_, tx_buffer_size_); + init_uart(this->uart_num_, baud_rate_, ESPHOME_LOGGER_TX_BUFFER_SIZE); break; #endif #ifdef USE_LOGGER_USB_CDC diff --git a/esphome/components/logger/logger_host.cpp b/esphome/components/logger/logger_host.cpp index be12b6df6a..fe094f6e9e 100644 --- a/esphome/components/logger/logger_host.cpp +++ b/esphome/components/logger/logger_host.cpp @@ -5,8 +5,8 @@ namespace esphome::logger { void HOT Logger::write_msg_(const char *msg, uint16_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]; + static constexpr size_t HEADROOM = 128; // Extra space for ANSI codes, newline, etc. + char buffer[TIMESTAMP_LEN + ESPHOME_LOGGER_TX_BUFFER_SIZE + HEADROOM]; time_t rawtime; time(&rawtime); diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 02335e2745..1128df94f0 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -21,6 +21,7 @@ // logger #define ESPHOME_LOG_LEVEL ESPHOME_LOG_LEVEL_VERY_VERBOSE +#define ESPHOME_LOGGER_TX_BUFFER_SIZE 512 #define USE_LOG_LISTENERS #define ESPHOME_LOG_MAX_LISTENERS 8 diff --git a/tests/dummy_main.cpp b/tests/dummy_main.cpp index e6fe733807..52f1fbd319 100644 --- a/tests/dummy_main.cpp +++ b/tests/dummy_main.cpp @@ -13,7 +13,7 @@ using namespace esphome; void setup() { App.pre_setup("livingroom", "LivingRoom", false); - auto *log = new logger::Logger(115200, 512); // NOLINT + auto *log = new logger::Logger(115200); // NOLINT log->pre_setup(); log->set_uart_selection(logger::UART_SELECTION_UART0); App.register_component(log); From b539a5aa51e08db837d30be5310b917fa61f631b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Feb 2026 17:07:56 -0600 Subject: [PATCH 185/197] [water_heater] Fix device_id missing from state responses (#14212) --- esphome/components/api/api_connection.cpp | 3 +- .../fixtures/device_id_in_state.yaml | 115 ++++++++++++ tests/integration/test_device_id_in_state.py | 173 ++++++++++++------ 3 files changed, 232 insertions(+), 59 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 9fc263abbd..7bc9c45c05 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1346,9 +1346,8 @@ uint16_t APIConnection::try_send_water_heater_state(EntityBase *entity, APIConne resp.target_temperature_low = wh->get_target_temperature_low(); resp.target_temperature_high = wh->get_target_temperature_high(); resp.state = wh->get_state(); - resp.key = wh->get_object_id_hash(); - return encode_message_to_buffer(resp, WaterHeaterStateResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_state(wh, resp, WaterHeaterStateResponse::MESSAGE_TYPE, conn, remaining_size); } uint16_t APIConnection::try_send_water_heater_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *wh = static_cast(entity); diff --git a/tests/integration/fixtures/device_id_in_state.yaml b/tests/integration/fixtures/device_id_in_state.yaml index c8548617b8..a5dfb7ee45 100644 --- a/tests/integration/fixtures/device_id_in_state.yaml +++ b/tests/integration/fixtures/device_id_in_state.yaml @@ -46,6 +46,7 @@ sensor: binary_sensor: - platform: template + id: motion_detected name: Motion Detected device_id: motion_sensor lambda: return true; @@ -82,3 +83,117 @@ output: write_action: - lambda: |- ESP_LOGD("test", "Light output: %d", state); + +cover: + - platform: template + name: Garage Door + device_id: motion_sensor + optimistic: true + +fan: + - platform: template + name: Ceiling Fan + device_id: humidity_monitor + speed_count: 3 + has_oscillating: false + has_direction: false + +lock: + - platform: template + name: Front Door Lock + device_id: motion_sensor + optimistic: true + +number: + - platform: template + name: Target Temperature + device_id: temperature_monitor + optimistic: true + min_value: 0 + max_value: 100 + step: 1 + +select: + - platform: template + name: Mode Select + device_id: humidity_monitor + optimistic: true + options: + - "Auto" + - "Manual" + +text: + - platform: template + name: Device Label + device_id: temperature_monitor + optimistic: true + mode: text + +valve: + - platform: template + name: Water Valve + device_id: humidity_monitor + optimistic: true + +globals: + - id: global_away + type: bool + initial_value: "false" + - id: global_is_on + type: bool + initial_value: "true" + +water_heater: + - platform: template + name: Test Boiler + device_id: temperature_monitor + optimistic: true + current_temperature: !lambda "return 45.0f;" + target_temperature: !lambda "return 60.0f;" + away: !lambda "return id(global_away);" + is_on: !lambda "return id(global_is_on);" + supported_modes: + - "off" + - electric + visual: + min_temperature: 30.0 + max_temperature: 85.0 + target_temperature_step: 0.5 + set_action: + - lambda: |- + ESP_LOGD("test", "Water heater set"); + +alarm_control_panel: + - platform: template + name: House Alarm + device_id: motion_sensor + codes: + - "1234" + restore_mode: ALWAYS_DISARMED + binary_sensors: + - input: motion_detected + +datetime: + - platform: template + name: Schedule Date + device_id: temperature_monitor + type: date + optimistic: true + - platform: template + name: Schedule Time + device_id: humidity_monitor + type: time + optimistic: true + - platform: template + name: Schedule DateTime + device_id: motion_sensor + type: datetime + optimistic: true + +event: + - platform: template + name: Doorbell + device_id: motion_sensor + event_types: + - "press" + - "double_press" diff --git a/tests/integration/test_device_id_in_state.py b/tests/integration/test_device_id_in_state.py index 51088bcbf7..48de94a85a 100644 --- a/tests/integration/test_device_id_in_state.py +++ b/tests/integration/test_device_id_in_state.py @@ -4,11 +4,80 @@ from __future__ import annotations import asyncio -from aioesphomeapi import BinarySensorState, EntityState, SensorState, TextSensorState +from aioesphomeapi import ( + AlarmControlPanelEntityState, + BinarySensorState, + CoverState, + DateState, + DateTimeState, + EntityState, + FanState, + LightState, + LockEntityState, + NumberState, + SelectState, + SensorState, + SwitchState, + TextSensorState, + TextState, + TimeState, + ValveState, + WaterHeaterState, +) import pytest from .types import APIClientConnectedFactory, RunCompiledFunction +# Mapping of entity name to device name for all entities with device_id +ENTITY_TO_DEVICE = { + # Original entities + "Temperature": "Temperature Monitor", + "Humidity": "Humidity Monitor", + "Motion Detected": "Motion Sensor", + "Temperature Monitor Power": "Temperature Monitor", + "Temperature Status": "Temperature Monitor", + "Motion Light": "Motion Sensor", + # New entity types + "Garage Door": "Motion Sensor", + "Ceiling Fan": "Humidity Monitor", + "Front Door Lock": "Motion Sensor", + "Target Temperature": "Temperature Monitor", + "Mode Select": "Humidity Monitor", + "Device Label": "Temperature Monitor", + "Water Valve": "Humidity Monitor", + "Test Boiler": "Temperature Monitor", + "House Alarm": "Motion Sensor", + "Schedule Date": "Temperature Monitor", + "Schedule Time": "Humidity Monitor", + "Schedule DateTime": "Motion Sensor", + "Doorbell": "Motion Sensor", +} + +# Entities without device_id (should have device_id 0) +NO_DEVICE_ENTITIES = {"No Device Sensor"} + +# State types that should have non-zero device_id, mapped by their aioesphomeapi class +EXPECTED_STATE_TYPES = [ + (SensorState, "sensor"), + (BinarySensorState, "binary_sensor"), + (SwitchState, "switch"), + (TextSensorState, "text_sensor"), + (LightState, "light"), + (CoverState, "cover"), + (FanState, "fan"), + (LockEntityState, "lock"), + (NumberState, "number"), + (SelectState, "select"), + (TextState, "text"), + (ValveState, "valve"), + (WaterHeaterState, "water_heater"), + (AlarmControlPanelEntityState, "alarm_control_panel"), + (DateState, "date"), + (TimeState, "time"), + (DateTimeState, "datetime"), + # Event is stateless (no initial state sent on subscribe) +] + @pytest.mark.asyncio async def test_device_id_in_state( @@ -40,34 +109,35 @@ async def test_device_id_in_state( entity_device_mapping: dict[int, int] = {} for entity in all_entities: - # All entities have name and key attributes - if entity.name == "Temperature": - entity_device_mapping[entity.key] = device_ids["Temperature Monitor"] - elif entity.name == "Humidity": - entity_device_mapping[entity.key] = device_ids["Humidity Monitor"] - elif entity.name == "Motion Detected": - entity_device_mapping[entity.key] = device_ids["Motion Sensor"] - elif entity.name in {"Temperature Monitor Power", "Temperature Status"}: - entity_device_mapping[entity.key] = device_ids["Temperature Monitor"] - elif entity.name == "Motion Light": - entity_device_mapping[entity.key] = device_ids["Motion Sensor"] - elif entity.name == "No Device Sensor": - # Entity without device_id should have device_id 0 + if entity.name in ENTITY_TO_DEVICE: + expected_device = ENTITY_TO_DEVICE[entity.name] + entity_device_mapping[entity.key] = device_ids[expected_device] + elif entity.name in NO_DEVICE_ENTITIES: entity_device_mapping[entity.key] = 0 - assert len(entity_device_mapping) >= 6, ( - f"Expected at least 6 mapped entities, got {len(entity_device_mapping)}" + expected_count = len(ENTITY_TO_DEVICE) + len(NO_DEVICE_ENTITIES) + assert len(entity_device_mapping) >= expected_count, ( + f"Expected at least {expected_count} mapped entities, " + f"got {len(entity_device_mapping)}. " + f"Missing: {set(ENTITY_TO_DEVICE) | NO_DEVICE_ENTITIES - {e.name for e in all_entities}}" + ) + + # Subscribe to states and wait for all mapped entities + # Event entities are stateless (no initial state on subscribe), + # so exclude them from the expected count + stateless_keys = {e.key for e in all_entities if e.name == "Doorbell"} + stateful_count = len(entity_device_mapping) - len( + stateless_keys & entity_device_mapping.keys() ) - # Subscribe to states loop = asyncio.get_running_loop() states: dict[int, EntityState] = {} states_future: asyncio.Future[bool] = loop.create_future() def on_state(state: EntityState) -> None: - states[state.key] = state - # Check if we have states for all mapped entities - if len(states) >= len(entity_device_mapping) and not states_future.done(): + if state.key in entity_device_mapping: + states[state.key] = state + if len(states) >= stateful_count and not states_future.done(): states_future.set_result(True) client.subscribe_states(on_state) @@ -76,9 +146,16 @@ async def test_device_id_in_state( try: await asyncio.wait_for(states_future, timeout=10.0) except TimeoutError: + received_names = {e.name for e in all_entities if e.key in states} + missing_names = ( + (set(ENTITY_TO_DEVICE) | NO_DEVICE_ENTITIES) + - received_names + - {"Doorbell"} + ) pytest.fail( f"Did not receive all entity states within 10 seconds. " - f"Received {len(states)} states, expected {len(entity_device_mapping)}" + f"Received {len(states)} states. " + f"Missing: {missing_names}" ) # Verify each state has the correct device_id @@ -86,51 +163,33 @@ async def test_device_id_in_state( for key, expected_device_id in entity_device_mapping.items(): if key in states: state = states[key] + entity_name = next( + (e.name for e in all_entities if e.key == key), f"key={key}" + ) assert state.device_id == expected_device_id, ( - f"State for key {key} has device_id {state.device_id}, " - f"expected {expected_device_id}" + f"State for '{entity_name}' (type={type(state).__name__}) " + f"has device_id {state.device_id}, expected {expected_device_id}" ) verified_count += 1 - assert verified_count >= 6, ( - f"Only verified {verified_count} states, expected at least 6" + # All stateful entities should be verified (everything except Doorbell event) + expected_verified = expected_count - 1 # exclude Doorbell + assert verified_count >= expected_verified, ( + f"Only verified {verified_count} states, expected at least {expected_verified}" ) - # Test specific state types to ensure device_id is present - # Find a sensor state with device_id - sensor_state = next( - ( + # Verify each expected state type has at least one instance with non-zero device_id + for state_type, type_name in EXPECTED_STATE_TYPES: + matching = [ s for s in states.values() - if isinstance(s, SensorState) - and isinstance(s.state, float) - and s.device_id != 0 - ), - None, - ) - assert sensor_state is not None, "No sensor state with device_id found" - assert sensor_state.device_id > 0, "Sensor state should have non-zero device_id" - - # Find a binary sensor state - binary_sensor_state = next( - (s for s in states.values() if isinstance(s, BinarySensorState)), - None, - ) - assert binary_sensor_state is not None, "No binary sensor state found" - assert binary_sensor_state.device_id > 0, ( - "Binary sensor state should have non-zero device_id" - ) - - # Find a text sensor state - text_sensor_state = next( - (s for s in states.values() if isinstance(s, TextSensorState)), - None, - ) - assert text_sensor_state is not None, "No text sensor state found" - assert text_sensor_state.device_id > 0, ( - "Text sensor state should have non-zero device_id" - ) + if isinstance(s, state_type) and s.device_id != 0 + ] + assert matching, ( + f"No {type_name} state (type={state_type.__name__}) " + f"with non-zero device_id found" + ) # Verify the "No Device Sensor" has device_id = 0 no_device_key = next( From ded457c2c1a5b27aba9af05b7b29a38dd832e7fa Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Feb 2026 18:52:46 -0600 Subject: [PATCH 186/197] [libretiny] Tune oversized lwIP defaults for ESPHome (#14186) Co-authored-by: Claude Opus 4.6 Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/api/__init__.py | 4 +- esphome/components/captive_portal/__init__.py | 6 +- esphome/components/esp32/__init__.py | 59 +++---- .../esp32_camera_web_server/__init__.py | 6 +- esphome/components/esphome/ota/__init__.py | 5 +- esphome/components/libretiny/__init__.py | 153 +++++++++++++++++- esphome/components/mdns/__init__.py | 2 +- esphome/components/socket/__init__.py | 90 ++++++++++- esphome/components/udp/__init__.py | 2 +- esphome/components/web_server/__init__.py | 6 +- .../components/web_server_base/__init__.py | 25 ++- esphome/components/wifi/__init__.py | 20 +++ .../socket/test_wake_loop_threadsafe.py | 8 +- 13 files changed, 327 insertions(+), 59 deletions(-) diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index 9bff9f5635..3f7cafb485 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -233,8 +233,8 @@ def _consume_api_sockets(config: ConfigType) -> ConfigType: # API needs 1 listening socket + typically 3 concurrent client connections # (not max_connections, which is the upper limit rarely reached) - sockets_needed = 1 + 3 - socket.consume_sockets(sockets_needed, "api")(config) + socket.consume_sockets(3, "api")(config) + socket.consume_sockets(1, "api", socket.SocketType.TCP_LISTEN)(config) return config diff --git a/esphome/components/captive_portal/__init__.py b/esphome/components/captive_portal/__init__.py index 049618219e..6c190814c0 100644 --- a/esphome/components/captive_portal/__init__.py +++ b/esphome/components/captive_portal/__init__.py @@ -76,13 +76,15 @@ def _final_validate(config: ConfigType) -> ConfigType: # Register socket needs for DNS server and additional HTTP connections # - 1 UDP socket for DNS server - # - 3 additional TCP sockets for captive portal detection probes + configuration requests + # - 3 TCP sockets for captive portal detection probes + configuration requests # OS captive portal detection makes multiple probe requests that stay in TIME_WAIT. # Need headroom for actual user configuration requests. # LRU purging will reclaim idle sockets to prevent exhaustion from repeated attempts. + # The listening socket is registered by web_server_base (shared HTTP server). from esphome.components import socket - socket.consume_sockets(4, "captive_portal")(config) + socket.consume_sockets(3, "captive_portal")(config) + socket.consume_sockets(1, "captive_portal", socket.SocketType.UDP)(config) return config diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 06677006ea..4c211b2f2a 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -1258,21 +1258,15 @@ def _configure_lwip_max_sockets(conf: dict) -> None: This function runs in to_code() after all components have registered their socket needs. User-provided sdkconfig_options take precedence. """ - from esphome.components.socket import KEY_SOCKET_CONSUMERS + from esphome.components.socket import get_socket_counts # Check if user manually specified CONFIG_LWIP_MAX_SOCKETS user_max_sockets = conf[CONF_SDKCONFIG_OPTIONS].get("CONFIG_LWIP_MAX_SOCKETS") - socket_consumers: dict[str, int] = CORE.data.get(KEY_SOCKET_CONSUMERS, {}) - total_sockets = sum(socket_consumers.values()) - - # Early return if no sockets registered and no user override - if total_sockets == 0 and user_max_sockets is None: - return - - components_list = ", ".join( - f"{name}={count}" for name, count in sorted(socket_consumers.items()) - ) + # CONFIG_LWIP_MAX_SOCKETS is a single VFS socket pool shared by all socket + # types (TCP clients, TCP listeners, and UDP). Include all three counts. + sc = get_socket_counts() + total_sockets = sc.tcp + sc.udp + sc.tcp_listen # User specified their own value - respect it but warn if insufficient if user_max_sockets is not None: @@ -1281,22 +1275,23 @@ def _configure_lwip_max_sockets(conf: dict) -> None: user_max_sockets, ) - # Warn if user's value is less than what components need - if total_sockets > 0: - user_sockets_int = 0 - with contextlib.suppress(ValueError, TypeError): - user_sockets_int = int(user_max_sockets) + user_sockets_int = 0 + with contextlib.suppress(ValueError, TypeError): + user_sockets_int = int(user_max_sockets) - if user_sockets_int < total_sockets: - _LOGGER.warning( - "CONFIG_LWIP_MAX_SOCKETS is set to %d but your configuration " - "needs %d sockets (registered: %s). You may experience socket " - "exhaustion errors. Consider increasing to at least %d.", - user_sockets_int, - total_sockets, - components_list, - total_sockets, - ) + if user_sockets_int < total_sockets: + _LOGGER.warning( + "CONFIG_LWIP_MAX_SOCKETS is set to %d but your configuration " + "needs %d sockets (%d TCP + %d UDP + %d TCP_LISTEN). You may " + "experience socket exhaustion errors. Consider increasing to " + "at least %d.", + user_sockets_int, + total_sockets, + sc.tcp, + sc.udp, + sc.tcp_listen, + total_sockets, + ) # User's value already added via sdkconfig_options processing return @@ -1305,11 +1300,19 @@ def _configure_lwip_max_sockets(conf: dict) -> None: max_sockets = max(DEFAULT_MAX_SOCKETS, total_sockets) log_level = logging.INFO if max_sockets > DEFAULT_MAX_SOCKETS else logging.DEBUG + sock_min = " (min)" if max_sockets > total_sockets else "" _LOGGER.log( log_level, - "Setting CONFIG_LWIP_MAX_SOCKETS to %d (registered: %s)", + "Setting CONFIG_LWIP_MAX_SOCKETS to %d%s " + "(TCP=%d [%s], UDP=%d [%s], TCP_LISTEN=%d [%s])", max_sockets, - components_list, + sock_min, + sc.tcp, + sc.tcp_details, + sc.udp, + sc.udp_details, + sc.tcp_listen, + sc.tcp_listen_details, ) add_idf_sdkconfig_option("CONFIG_LWIP_MAX_SOCKETS", max_sockets) diff --git a/esphome/components/esp32_camera_web_server/__init__.py b/esphome/components/esp32_camera_web_server/__init__.py index ed1aaa2e07..da260ad7a1 100644 --- a/esphome/components/esp32_camera_web_server/__init__.py +++ b/esphome/components/esp32_camera_web_server/__init__.py @@ -20,8 +20,10 @@ def _consume_camera_web_server_sockets(config: ConfigType) -> ConfigType: from esphome.components import socket # Each camera web server instance needs 1 listening socket + 2 client connections - sockets_needed = 3 - socket.consume_sockets(sockets_needed, "esp32_camera_web_server")(config) + socket.consume_sockets(2, "esp32_camera_web_server")(config) + socket.consume_sockets(1, "esp32_camera_web_server", socket.SocketType.TCP_LISTEN)( + config + ) return config diff --git a/esphome/components/esphome/ota/__init__.py b/esphome/components/esphome/ota/__init__.py index 2f637d714d..337064dd27 100644 --- a/esphome/components/esphome/ota/__init__.py +++ b/esphome/components/esphome/ota/__init__.py @@ -97,8 +97,9 @@ def _consume_ota_sockets(config: ConfigType) -> ConfigType: """Register socket needs for OTA component.""" from esphome.components import socket - # OTA needs 1 listening socket (client connections are temporary during updates) - socket.consume_sockets(1, "ota")(config) + # OTA needs 1 listening socket. The active transfer connection during an update + # uses a TCP PCB from the general pool, covered by MIN_TCP_SOCKETS headroom. + socket.consume_sockets(1, "ota", socket.SocketType.TCP_LISTEN)(config) return config diff --git a/esphome/components/libretiny/__init__.py b/esphome/components/libretiny/__init__.py index 01445da7ee..2291114d9a 100644 --- a/esphome/components/libretiny/__init__.py +++ b/esphome/components/libretiny/__init__.py @@ -275,6 +275,146 @@ BASE_SCHEMA.add_extra(_detect_variant) BASE_SCHEMA.add_extra(_update_core_data) +def _configure_lwip(config: dict) -> None: + """Configure lwIP options for LibreTiny platforms. + + The BK/RTL/LN SDKs each ship different lwIP defaults. BK72XX defaults are + wildly oversized for ESPHome's IoT use case, causing OOM on BK7231N. + RTL87XX and LN882H have more conservative defaults but still need tuning + for ESPHome's socket usage patterns. + + See https://github.com/esphome/esphome/issues/14095 + + Comparison of SDK defaults vs ESPHome targets (TCP_MSS=1460 on all LT): + + Setting ESP8266 ESP32 BK SDK RTL SDK LN SDK New + ──────────────────────────────────────────────────────────────────────────── + TCP_SND_BUF 2×MSS 4×MSS 10×MSS 5×MSS 7×MSS 4×MSS + TCP_WND 4×MSS 4×MSS 3/10×MSS 2×MSS 3×MSS 4×MSS + MEM_LIBC_MALLOC 1 1 0 0 1 1 + MEMP_MEM_MALLOC 1 1 0 0 0 1 + MEM_SIZE N/A* N/A* 16/32KB 5KB N/A* N/A* BK + PBUF_POOL_SIZE 10 16 3/10 20 20 10 BK + MAX_SOCKETS_TCP 5 16 12 —** —** dynamic + MAX_SOCKETS_UDP 4 16 22 —** —** dynamic + TCP_SND_QUEUELEN ~8 17 20 20 35 17 + MEMP_NUM_TCP_SEG 10 16 40 20 =qlen 17 + MEMP_NUM_TCP_PCB 5 16 12 10 8 =TCP + MEMP_NUM_TCP_PCB_LISTEN 4 16 4 5 3 dynamic + MEMP_NUM_UDP_PCB 4 16 25*** 7**** 7**** =UDP + MEMP_NUM_NETCONN 0 10 38 4***** =sum =sum + MEMP_NUM_NETBUF 0 2 16 2***** 8 4 + MEMP_NUM_TCPIP_MSG_INPKT 4 8 16 8***** 12 8 + + * ESP8266/ESP32/LN882H use MEM_LIBC_MALLOC=1 (system heap, no dedicated pool). + ESP8266/ESP32 also use MEMP_MEM_MALLOC=1 (MEMP pools from heap, not static). + ** RTL/LN SDKs don't define MAX_SOCKETS_TCP/UDP (LibreTiny-specific). + *** BK LT overlay: MAX_SOCKETS_UDP+2+1 = 25. + **** RTL/LN LT overlay overrides to flat 7. + ***** Not defined in RTL SDK — lwIP opt.h defaults shown. + "dynamic" = auto-calculated from component socket registrations via + socket.get_socket_counts() with minimums of 8 TCP / 6 UDP. + """ + from esphome.components.socket import ( + MIN_TCP_LISTEN_SOCKETS, + MIN_TCP_SOCKETS, + MIN_UDP_SOCKETS, + get_socket_counts, + ) + + sc = get_socket_counts() + # Apply platform minimums — ensure headroom for ESPHome's needs + tcp_sockets = max(MIN_TCP_SOCKETS, sc.tcp) + udp_sockets = max(MIN_UDP_SOCKETS, sc.udp) + # Listening sockets — registered by components (api, ota, web_server_base, etc.) + # Not all components register yet, so ensure a minimum for baseline operation. + listening_tcp = max(MIN_TCP_LISTEN_SOCKETS, sc.tcp_listen) + + # TCP_SND_BUF: ESPAsyncWebServer allocates malloc(tcp_sndbuf()) per + # response chunk. At 10×MSS=14.6KB (BK default) this causes OOM (#14095). + # 4×MSS=5,840 matches ESP32. RTL(5×) and LN(7×) are close already. + tcp_snd_buf = "(4*TCP_MSS)" # BK: 10×MSS, RTL: 5×MSS, LN: 7×MSS + + # TCP_WND: receive window. 4×MSS matches ESP32. + # RTL SDK uses only 2×MSS; increasing to 4× is safe and improves throughput. + tcp_wnd = "(4*TCP_MSS)" # BK: 10×MSS, RTL: 2×MSS, LN: 3×MSS + + # TCP_SND_QUEUELEN: max pbufs queued for send buffer + # ESP-IDF formula: (4 * TCP_SND_BUF + (TCP_MSS - 1)) / TCP_MSS + # With 4×MSS: (4*5840 + 1459) / 1460 = 17 — match ESP32 + tcp_snd_queuelen = 17 # BK: 20, RTL: 20, LN: 35 + # MEMP_NUM_TCP_SEG: segment pool, must be >= TCP_SND_QUEUELEN (lwIP sanity check) + memp_num_tcp_seg = tcp_snd_queuelen # BK: 40, RTL: 20, LN: =qlen + + lwip_opts: list[str] = [ + # Disable statistics — not needed for production, saves RAM + "LWIP_STATS=0", # BK: 1, RTL: 0 already, LN: 0 already + "MEM_STATS=0", + "MEMP_STATS=0", + # TCP send buffer — 4×MSS matches ESP32 + f"TCP_SND_BUF={tcp_snd_buf}", + # TCP receive window — 4×MSS matches ESP32 + f"TCP_WND={tcp_wnd}", + # Socket counts — auto-calculated from component registrations + f"MAX_SOCKETS_TCP={tcp_sockets}", + f"MAX_SOCKETS_UDP={udp_sockets}", + # Listening sockets — BK SDK uses this to derive MEMP_NUM_TCP_PCB_LISTEN; + # RTL/LN don't use it, but we set MEMP_NUM_TCP_PCB_LISTEN explicitly below. + f"MAX_LISTENING_SOCKETS_TCP={listening_tcp}", + # Queued segment limits — derived from 4×MSS buffer size + f"TCP_SND_QUEUELEN={tcp_snd_queuelen}", + f"MEMP_NUM_TCP_SEG={memp_num_tcp_seg}", # must be >= queuelen + # PCB pools — active connections + listening sockets + f"MEMP_NUM_TCP_PCB={tcp_sockets}", # BK: 12, RTL: 10, LN: 8 + f"MEMP_NUM_TCP_PCB_LISTEN={listening_tcp}", # BK: =MAX_LISTENING, RTL: 5, LN: 3 + # UDP PCB pool — includes wifi.lwip_internal (DHCP + DNS) + f"MEMP_NUM_UDP_PCB={udp_sockets}", # BK: 25, RTL/LN: 7 via LT + # Netconn pool — each socket (active + listening) needs a netconn + f"MEMP_NUM_NETCONN={tcp_sockets + udp_sockets + listening_tcp}", + # Netbuf pool + "MEMP_NUM_NETBUF=4", # BK: 16, RTL: 2 (opt.h), LN: 8 + # Inbound message pool + "MEMP_NUM_TCPIP_MSG_INPKT=8", # BK: 16, RTL: 8 (opt.h), LN: 12 + ] + + # Use system heap for all lwIP allocations on all LibreTiny platforms. + # - MEM_LIBC_MALLOC=1: Use system heap instead of dedicated lwIP heap. + # LN882H already ships with this. BK SDK defaults to a 16/32KB dedicated + # pool that fragments during OTA. RTL SDK defaults to a 5KB pool. + # All three SDKs wire malloc → pvPortMalloc (FreeRTOS thread-safe heap). + # - MEMP_MEM_MALLOC=1: Allocate MEMP pools from heap on demand instead + # of static arrays. Saves ~20KB RAM on BK72XX. Safe because WiFi + # receive paths run in task context, not ISR context. ESP32 and ESP8266 + # both ship with MEMP_MEM_MALLOC=1. + lwip_opts.append("MEM_LIBC_MALLOC=1") + lwip_opts.append("MEMP_MEM_MALLOC=1") + + # BK72XX-specific: PBUF_POOL_SIZE override + # BK SDK "reduced plan" sets this to only 3 — too few for multiple + # concurrent connections (API + web_server + OTA). BK default plan + # uses 10; match that. RTL(20) and LN(20) need no override. + # With MEMP_MEM_MALLOC=1, this is a max count (allocated on demand). + if CORE.is_bk72xx: + lwip_opts.append("PBUF_POOL_SIZE=10") + + tcp_min = " (min)" if tcp_sockets > sc.tcp else "" + udp_min = " (min)" if udp_sockets > sc.udp else "" + listen_min = " (min)" if listening_tcp > sc.tcp_listen else "" + _LOGGER.info( + "Configuring lwIP: TCP=%d%s [%s], UDP=%d%s [%s], TCP_LISTEN=%d%s [%s]", + tcp_sockets, + tcp_min, + sc.tcp_details, + udp_sockets, + udp_min, + sc.udp_details, + listening_tcp, + listen_min, + sc.tcp_listen_details, + ) + cg.add_platformio_option("custom_options.lwip", lwip_opts) + + # pylint: disable=use-dict-literal async def component_to_code(config): var = cg.new_Pvariable(config[CONF_ID]) @@ -389,11 +529,12 @@ async def component_to_code(config): "custom_options.sys_config#h", _BK7231N_SYS_CONFIG_OPTIONS ) - # Disable LWIP statistics to save RAM - not needed in production - # Must explicitly disable all sub-stats to avoid redefinition warnings - cg.add_platformio_option( - "custom_options.lwip", - ["LWIP_STATS=0", "MEM_STATS=0", "MEMP_STATS=0"], - ) + # Tune lwIP for ESPHome's actual needs. + # The SDK defaults (TCP_SND_BUF=10*MSS, MAX_SOCKETS_TCP=12, MEM_SIZE=32KB) + # are wildly oversized for an IoT device. ESPAsyncWebServer allocates + # malloc(tcp_sndbuf()) per response chunk — at 14.6KB this causes silent + # OOM on memory-constrained chips like BK7231N. + # See https://github.com/esphome/esphome/issues/14095 + _configure_lwip(config) await cg.register_component(var, config) diff --git a/esphome/components/mdns/__init__.py b/esphome/components/mdns/__init__.py index f87f929615..420e6a60e3 100644 --- a/esphome/components/mdns/__init__.py +++ b/esphome/components/mdns/__init__.py @@ -56,7 +56,7 @@ def _consume_mdns_sockets(config: ConfigType) -> ConfigType: from esphome.components import socket # mDNS needs 2 sockets (IPv4 + IPv6 multicast) - socket.consume_sockets(2, "mdns")(config) + socket.consume_sockets(2, "mdns", socket.SocketType.UDP)(config) return config diff --git a/esphome/components/socket/__init__.py b/esphome/components/socket/__init__.py index e364da78f8..d82f0c7aba 100644 --- a/esphome/components/socket/__init__.py +++ b/esphome/components/socket/__init__.py @@ -1,9 +1,14 @@ from collections.abc import Callable, MutableMapping +from dataclasses import dataclass +from enum import StrEnum +import logging import esphome.codegen as cg import esphome.config_validation as cv from esphome.core import CORE +_LOGGER = logging.getLogger(__name__) + CODEOWNERS = ["@esphome/core"] CONF_IMPLEMENTATION = "implementation" @@ -13,33 +18,110 @@ IMPLEMENTATION_BSD_SOCKETS = "bsd_sockets" # Socket tracking infrastructure # Components register their socket needs and platforms read this to configure appropriately -KEY_SOCKET_CONSUMERS = "socket_consumers" +KEY_SOCKET_CONSUMERS_TCP = "socket_consumers_tcp" +KEY_SOCKET_CONSUMERS_UDP = "socket_consumers_udp" +KEY_SOCKET_CONSUMERS_TCP_LISTEN = "socket_consumers_tcp_listen" + +# Recommended minimum socket counts. +# Platforms should apply these (or their own) on top of get_socket_counts(). +# These cover minimal configs (e.g. api-only without web_server). +# When web_server is present, its 5 registered sockets push past the TCP minimum. +MIN_TCP_SOCKETS = 8 +MIN_UDP_SOCKETS = 6 +# Minimum listening sockets — at least api + ota baseline. +MIN_TCP_LISTEN_SOCKETS = 2 # Wake loop threadsafe support tracking KEY_WAKE_LOOP_THREADSAFE_REQUIRED = "wake_loop_threadsafe_required" +class SocketType(StrEnum): + TCP = "tcp" + UDP = "udp" + TCP_LISTEN = "tcp_listen" + + +_SOCKET_TYPE_KEYS = { + SocketType.TCP: KEY_SOCKET_CONSUMERS_TCP, + SocketType.UDP: KEY_SOCKET_CONSUMERS_UDP, + SocketType.TCP_LISTEN: KEY_SOCKET_CONSUMERS_TCP_LISTEN, +} + + def consume_sockets( - value: int, consumer: str + value: int, consumer: str, socket_type: SocketType = SocketType.TCP ) -> Callable[[MutableMapping], MutableMapping]: """Register socket usage for a component. Args: value: Number of sockets needed by the component consumer: Name of the component consuming the sockets + socket_type: Type of socket (SocketType.TCP, SocketType.UDP, or SocketType.TCP_LISTEN) Returns: A validator function that records the socket usage """ + typed_key = _SOCKET_TYPE_KEYS[socket_type] def _consume_sockets(config: MutableMapping) -> MutableMapping: - consumers: dict[str, int] = CORE.data.setdefault(KEY_SOCKET_CONSUMERS, {}) + consumers: dict[str, int] = CORE.data.setdefault(typed_key, {}) consumers[consumer] = consumers.get(consumer, 0) + value return config return _consume_sockets +def _format_consumers(consumers: dict[str, int]) -> str: + """Format consumer dict as 'name=count, ...' or 'none'.""" + if not consumers: + return "none" + return ", ".join(f"{name}={count}" for name, count in sorted(consumers.items())) + + +@dataclass(frozen=True) +class SocketCounts: + """Socket counts and component details for platform configuration.""" + + tcp: int + udp: int + tcp_listen: int + tcp_details: str + udp_details: str + tcp_listen_details: str + + +def get_socket_counts() -> SocketCounts: + """Return socket counts and component details for platform configuration. + + Platforms call this during code generation to configure lwIP socket limits. + All components will have registered their needs by then. + + Platforms should apply their own minimums on top of these values. + """ + tcp_consumers = CORE.data.get(KEY_SOCKET_CONSUMERS_TCP, {}) + udp_consumers = CORE.data.get(KEY_SOCKET_CONSUMERS_UDP, {}) + tcp_listen_consumers = CORE.data.get(KEY_SOCKET_CONSUMERS_TCP_LISTEN, {}) + tcp = sum(tcp_consumers.values()) + udp = sum(udp_consumers.values()) + tcp_listen = sum(tcp_listen_consumers.values()) + + tcp_details = _format_consumers(tcp_consumers) + udp_details = _format_consumers(udp_consumers) + tcp_listen_details = _format_consumers(tcp_listen_consumers) + _LOGGER.debug( + "Socket counts: TCP=%d (%s), UDP=%d (%s), TCP_LISTEN=%d (%s)", + tcp, + tcp_details, + udp, + udp_details, + tcp_listen, + tcp_listen_details, + ) + return SocketCounts( + tcp, udp, tcp_listen, tcp_details, udp_details, tcp_listen_details + ) + + def require_wake_loop_threadsafe() -> None: """Mark that wake_loop_threadsafe support is required by a component. @@ -66,7 +148,7 @@ def require_wake_loop_threadsafe() -> None: CORE.data[KEY_WAKE_LOOP_THREADSAFE_REQUIRED] = True cg.add_define("USE_WAKE_LOOP_THREADSAFE") # Consume 1 socket for the shared wake notification socket - consume_sockets(1, "socket.wake_loop_threadsafe")({}) + consume_sockets(1, "socket.wake_loop_threadsafe", SocketType.UDP)({}) CONFIG_SCHEMA = cv.Schema( diff --git a/esphome/components/udp/__init__.py b/esphome/components/udp/__init__.py index c9586d0b95..37dd871a6c 100644 --- a/esphome/components/udp/__init__.py +++ b/esphome/components/udp/__init__.py @@ -73,7 +73,7 @@ def _consume_udp_sockets(config: ConfigType) -> ConfigType: # UDP uses up to 2 sockets: 1 broadcast + 1 listen # Whether each is used depends on code generation, so register worst case - socket.consume_sockets(2, "udp")(config) + socket.consume_sockets(2, "udp", socket.SocketType.UDP)(config) return config diff --git a/esphome/components/web_server/__init__.py b/esphome/components/web_server/__init__.py index 9305a2de61..84910b6f90 100644 --- a/esphome/components/web_server/__init__.py +++ b/esphome/components/web_server/__init__.py @@ -144,11 +144,11 @@ def _consume_web_server_sockets(config: ConfigType) -> ConfigType: """Register socket needs for web_server component.""" from esphome.components import socket - # Web server needs 1 listening socket + typically 5 concurrent client connections + # Web server needs typically 5 concurrent client connections # (browser opens connections for page resources, SSE event stream, and POST # requests for entity control which may linger before closing) - sockets_needed = 6 - socket.consume_sockets(sockets_needed, "web_server")(config) + # The listening socket is registered by web_server_base (shared with captive_portal) + socket.consume_sockets(5, "web_server")(config) return config diff --git a/esphome/components/web_server_base/__init__.py b/esphome/components/web_server_base/__init__.py index 183b907ae6..b587841dfd 100644 --- a/esphome/components/web_server_base/__init__.py +++ b/esphome/components/web_server_base/__init__.py @@ -23,10 +23,27 @@ web_server_base_ns = cg.esphome_ns.namespace("web_server_base") WebServerBase = web_server_base_ns.class_("WebServerBase") CONF_WEB_SERVER_BASE_ID = "web_server_base_id" -CONFIG_SCHEMA = cv.Schema( - { - cv.GenerateID(): cv.declare_id(WebServerBase), - } + + +def _consume_web_server_base_sockets(config): + """Register the shared listening socket for the HTTP server. + + web_server_base is the shared HTTP server used by web_server and captive_portal. + The listening socket is registered here rather than in each consumer. + """ + from esphome.components import socket + + socket.consume_sockets(1, "web_server_base", socket.SocketType.TCP_LISTEN)(config) + return config + + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(WebServerBase), + } + ), + _consume_web_server_base_sockets, ) diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index 8c1deb6217..b89245b10e 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -58,6 +58,7 @@ from esphome.const import ( ) from esphome.core import CORE, CoroPriority, HexInt, coroutine_with_priority import esphome.final_validate as fv +from esphome.types import ConfigType from . import wpa2_eap @@ -269,9 +270,28 @@ def final_validate(config): ) +def _consume_wifi_sockets(config: ConfigType) -> ConfigType: + """Register UDP PCBs used internally by lwIP for DHCP and DNS. + + Only needed on LibreTiny where we directly set MEMP_NUM_UDP_PCB (the raw + PCB pool shared by both application sockets and lwIP internals like DHCP/DNS). + On ESP32, CONFIG_LWIP_MAX_SOCKETS only controls the POSIX socket layer — + DHCP/DNS use raw udp_new() which bypasses it entirely. + """ + if not (CORE.is_bk72xx or CORE.is_rtl87xx or CORE.is_ln882x): + return config + from esphome.components import socket + + # lwIP allocates UDP PCBs for DHCP client and DNS resolver internally. + # These are not application sockets but consume MEMP_NUM_UDP_PCB pool entries. + socket.consume_sockets(2, "wifi.lwip_internal", socket.SocketType.UDP)(config) + return config + + FINAL_VALIDATE_SCHEMA = cv.All( final_validate, validate_variant, + _consume_wifi_sockets, ) diff --git a/tests/components/socket/test_wake_loop_threadsafe.py b/tests/components/socket/test_wake_loop_threadsafe.py index b4bc95176d..a40b6068a8 100644 --- a/tests/components/socket/test_wake_loop_threadsafe.py +++ b/tests/components/socket/test_wake_loop_threadsafe.py @@ -66,12 +66,12 @@ def test_require_wake_loop_threadsafe__no_networking_does_not_consume_socket() - CORE.config = {"logger": {}} # Track initial socket consumer state - initial_consumers = CORE.data.get(socket.KEY_SOCKET_CONSUMERS, {}) + initial_udp = CORE.data.get(socket.KEY_SOCKET_CONSUMERS_UDP, {}) # Call require_wake_loop_threadsafe socket.require_wake_loop_threadsafe() # Verify no socket was consumed - consumers = CORE.data.get(socket.KEY_SOCKET_CONSUMERS, {}) - assert "socket.wake_loop_threadsafe" not in consumers - assert consumers == initial_consumers + udp_consumers = CORE.data.get(socket.KEY_SOCKET_CONSUMERS_UDP, {}) + assert "socket.wake_loop_threadsafe" not in udp_consumers + assert udp_consumers == initial_udp From 263fff0ba2fc5ea7b9c849da88600f17852a86aa Mon Sep 17 00:00:00 2001 From: schrob <83939986+schdro@users.noreply.github.com> Date: Mon, 23 Feb 2026 04:35:00 +0100 Subject: [PATCH 187/197] Move CONF_OUTPUT_POWER into const.py (#14201) --- esphome/components/cc1101/__init__.py | 2 +- esphome/components/wifi/__init__.py | 2 +- esphome/const.py | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/esphome/components/cc1101/__init__.py b/esphome/components/cc1101/__init__.py index 14b92a18a4..e2e5986daf 100644 --- a/esphome/components/cc1101/__init__.py +++ b/esphome/components/cc1101/__init__.py @@ -9,6 +9,7 @@ from esphome.const import ( CONF_DATA, CONF_FREQUENCY, CONF_ID, + CONF_OUTPUT_POWER, CONF_VALUE, CONF_WAIT_TIME, ) @@ -22,7 +23,6 @@ ns = cg.esphome_ns.namespace("cc1101") CC1101Component = ns.class_("CC1101Component", cg.Component, spi.SPIDevice) # Config keys -CONF_OUTPUT_POWER = "output_power" CONF_RX_ATTENUATION = "rx_attenuation" CONF_DC_BLOCKING_FILTER = "dc_blocking_filter" CONF_IF_FREQUENCY = "if_frequency" diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index b89245b10e..2aa63b87cc 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -42,6 +42,7 @@ from esphome.const import ( CONF_ON_CONNECT, CONF_ON_DISCONNECT, CONF_ON_ERROR, + CONF_OUTPUT_POWER, CONF_PASSWORD, CONF_POWER_SAVE_MODE, CONF_PRIORITY, @@ -344,7 +345,6 @@ def _validate(config): return config -CONF_OUTPUT_POWER = "output_power" CONF_PASSIVE_SCAN = "passive_scan" CONFIG_SCHEMA = cv.All( cv.Schema( diff --git a/esphome/const.py b/esphome/const.py index 0179aa129e..ccc9d56dbb 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -748,6 +748,7 @@ CONF_OTA = "ota" CONF_OUTDOOR_TEMPERATURE = "outdoor_temperature" CONF_OUTPUT = "output" CONF_OUTPUT_ID = "output_id" +CONF_OUTPUT_POWER = "output_power" CONF_OUTPUT_SPEAKER = "output_speaker" CONF_OUTPUTS = "outputs" CONF_OVERSAMPLING = "oversampling" From 6e70987451dfce381645e06ec61e87fab745b900 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Feb 2026 21:35:30 -0600 Subject: [PATCH 188/197] [binary_sensor] Conditionally compile filter infrastructure (#14215) --- esphome/components/binary_sensor/__init__.py | 1 + esphome/components/binary_sensor/binary_sensor.cpp | 6 ++++++ esphome/components/binary_sensor/binary_sensor.h | 6 ++++++ esphome/components/binary_sensor/filter.cpp | 5 +++++ esphome/components/binary_sensor/filter.h | 5 +++++ esphome/core/defines.h | 1 + 6 files changed, 24 insertions(+) diff --git a/esphome/components/binary_sensor/__init__.py b/esphome/components/binary_sensor/__init__.py index c38d6b78d3..4500168cb1 100644 --- a/esphome/components/binary_sensor/__init__.py +++ b/esphome/components/binary_sensor/__init__.py @@ -562,6 +562,7 @@ async def setup_binary_sensor_core_(var, config): if inverted := config.get(CONF_INVERTED): cg.add(var.set_inverted(inverted)) if filters_config := config.get(CONF_FILTERS): + cg.add_define("USE_BINARY_SENSOR_FILTER") filters = await cg.build_registry_list(FILTER_REGISTRY, filters_config) cg.add(var.add_filters(filters)) diff --git a/esphome/components/binary_sensor/binary_sensor.cpp b/esphome/components/binary_sensor/binary_sensor.cpp index 7c3b06970d..c4d3a29a1e 100644 --- a/esphome/components/binary_sensor/binary_sensor.cpp +++ b/esphome/components/binary_sensor/binary_sensor.cpp @@ -18,11 +18,15 @@ void log_binary_sensor(const char *tag, const char *prefix, const char *type, Bi } void BinarySensor::publish_state(bool new_state) { +#ifdef USE_BINARY_SENSOR_FILTER if (this->filter_list_ == nullptr) { +#endif this->send_state_internal(new_state); +#ifdef USE_BINARY_SENSOR_FILTER } else { this->filter_list_->input(new_state); } +#endif } void BinarySensor::publish_initial_state(bool new_state) { this->invalidate_state(); @@ -47,6 +51,7 @@ bool BinarySensor::set_new_state(const optional &new_state) { return false; } +#ifdef USE_BINARY_SENSOR_FILTER void BinarySensor::add_filter(Filter *filter) { filter->parent_ = this; if (this->filter_list_ == nullptr) { @@ -63,6 +68,7 @@ void BinarySensor::add_filters(std::initializer_list filters) { this->add_filter(filter); } } +#endif // USE_BINARY_SENSOR_FILTER bool BinarySensor::is_status_binary_sensor() const { return false; } } // namespace esphome::binary_sensor diff --git a/esphome/components/binary_sensor/binary_sensor.h b/esphome/components/binary_sensor/binary_sensor.h index 83c992bfed..4b655e1bd1 100644 --- a/esphome/components/binary_sensor/binary_sensor.h +++ b/esphome/components/binary_sensor/binary_sensor.h @@ -2,7 +2,9 @@ #include "esphome/core/entity_base.h" #include "esphome/core/helpers.h" +#ifdef USE_BINARY_SENSOR_FILTER #include "esphome/components/binary_sensor/filter.h" +#endif #include @@ -45,8 +47,10 @@ class BinarySensor : public StatefulEntityBase, public EntityBase_DeviceCl */ void publish_initial_state(bool new_state); +#ifdef USE_BINARY_SENSOR_FILTER void add_filter(Filter *filter); void add_filters(std::initializer_list filters); +#endif // ========== INTERNAL METHODS ========== // (In most use cases you won't need these) @@ -60,7 +64,9 @@ class BinarySensor : public StatefulEntityBase, public EntityBase_DeviceCl bool state{}; protected: +#ifdef USE_BINARY_SENSOR_FILTER Filter *filter_list_{nullptr}; +#endif bool set_new_state(const optional &new_state) override; }; diff --git a/esphome/components/binary_sensor/filter.cpp b/esphome/components/binary_sensor/filter.cpp index d69671c5bf..25a69c413a 100644 --- a/esphome/components/binary_sensor/filter.cpp +++ b/esphome/components/binary_sensor/filter.cpp @@ -1,3 +1,6 @@ +#include "esphome/core/defines.h" +#ifdef USE_BINARY_SENSOR_FILTER + #include "filter.h" #include "binary_sensor.h" @@ -142,3 +145,5 @@ optional SettleFilter::new_value(bool value) { float SettleFilter::get_setup_priority() const { return setup_priority::HARDWARE; } } // namespace esphome::binary_sensor + +#endif // USE_BINARY_SENSOR_FILTER diff --git a/esphome/components/binary_sensor/filter.h b/esphome/components/binary_sensor/filter.h index 59bc43eeba..2735a32ab0 100644 --- a/esphome/components/binary_sensor/filter.h +++ b/esphome/components/binary_sensor/filter.h @@ -1,5 +1,8 @@ #pragma once +#include "esphome/core/defines.h" +#ifdef USE_BINARY_SENSOR_FILTER + #include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/helpers.h" @@ -138,3 +141,5 @@ class SettleFilter : public Filter, public Component { }; } // namespace esphome::binary_sensor + +#endif // USE_BINARY_SENSOR_FILTER diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 1128df94f0..99e80785ec 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -29,6 +29,7 @@ #define USE_ALARM_CONTROL_PANEL #define USE_AREAS #define USE_BINARY_SENSOR +#define USE_BINARY_SENSOR_FILTER #define USE_BUTTON #define USE_CAMERA #define USE_CLIMATE From 93ce582ad30d1059cf59335882a6bd231a93127e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Feb 2026 21:35:51 -0600 Subject: [PATCH 189/197] [sensor] Conditionally compile filter infrastructure (#14214) Co-authored-by: Claude Opus 4.6 --- esphome/components/bme680_bsec/bme680_bsec.h | 1 + esphome/components/haier/hon_climate.h | 1 + esphome/components/kamstrup_kmp/kamstrup_kmp.h | 1 + esphome/components/sensor/__init__.py | 1 + esphome/components/sensor/filter.cpp | 5 +++++ esphome/components/sensor/filter.h | 6 +++++- esphome/components/sensor/sensor.cpp | 6 ++++++ esphome/components/sensor/sensor.h | 8 ++++++++ esphome/components/sound_level/sound_level.h | 1 + esphome/components/wireguard/wireguard.h | 1 + esphome/core/defines.h | 1 + 11 files changed, 31 insertions(+), 1 deletion(-) diff --git a/esphome/components/bme680_bsec/bme680_bsec.h b/esphome/components/bme680_bsec/bme680_bsec.h index ec919f31df..22aa2789e6 100644 --- a/esphome/components/bme680_bsec/bme680_bsec.h +++ b/esphome/components/bme680_bsec/bme680_bsec.h @@ -7,6 +7,7 @@ #include "esphome/core/preferences.h" #include "esphome/core/defines.h" #include +#include #ifdef USE_BSEC #include diff --git a/esphome/components/haier/hon_climate.h b/esphome/components/haier/hon_climate.h index 608d5e7f21..4565ed2981 100644 --- a/esphome/components/haier/hon_climate.h +++ b/esphome/components/haier/hon_climate.h @@ -1,6 +1,7 @@ #pragma once #include +#include #ifdef USE_SENSOR #include "esphome/components/sensor/sensor.h" #endif diff --git a/esphome/components/kamstrup_kmp/kamstrup_kmp.h b/esphome/components/kamstrup_kmp/kamstrup_kmp.h index f84e360132..725cf20abf 100644 --- a/esphome/components/kamstrup_kmp/kamstrup_kmp.h +++ b/esphome/components/kamstrup_kmp/kamstrup_kmp.h @@ -1,5 +1,6 @@ #pragma once +#include #include "esphome/components/sensor/sensor.h" #include "esphome/components/uart/uart.h" #include "esphome/core/component.h" diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index ebbe0fbccc..b0e0c28bda 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -903,6 +903,7 @@ async def setup_sensor_core_(var, config): if config[CONF_FORCE_UPDATE]: cg.add(var.set_force_update(True)) if config.get(CONF_FILTERS): # must exist and not be empty + cg.add_define("USE_SENSOR_FILTER") filters = await build_filters(config[CONF_FILTERS]) cg.add(var.set_filters(filters)) diff --git a/esphome/components/sensor/filter.cpp b/esphome/components/sensor/filter.cpp index ea0e2f0d7c..cd4db98457 100644 --- a/esphome/components/sensor/filter.cpp +++ b/esphome/components/sensor/filter.cpp @@ -1,3 +1,6 @@ +#include "esphome/core/defines.h" +#ifdef USE_SENSOR_FILTER + #include "filter.h" #include #include "esphome/core/application.h" @@ -580,3 +583,5 @@ void StreamingMovingAverageFilter::reset_batch() { } } // namespace esphome::sensor + +#endif // USE_SENSOR_FILTER diff --git a/esphome/components/sensor/filter.h b/esphome/components/sensor/filter.h index 573b916a5d..8bfcdb37cf 100644 --- a/esphome/components/sensor/filter.h +++ b/esphome/components/sensor/filter.h @@ -1,6 +1,8 @@ #pragma once -#include +#include "esphome/core/defines.h" +#ifdef USE_SENSOR_FILTER + #include #include #include "esphome/core/automation.h" @@ -638,3 +640,5 @@ class StreamingMovingAverageFilter : public StreamingFilter { }; } // namespace esphome::sensor + +#endif // USE_SENSOR_FILTER diff --git a/esphome/components/sensor/sensor.cpp b/esphome/components/sensor/sensor.cpp index ae2ee3e3d1..a7af6403ef 100644 --- a/esphome/components/sensor/sensor.cpp +++ b/esphome/components/sensor/sensor.cpp @@ -68,11 +68,15 @@ void Sensor::publish_state(float state) { ESP_LOGV(TAG, "'%s': Received new state %f", this->name_.c_str(), state); +#ifdef USE_SENSOR_FILTER if (this->filter_list_ == nullptr) { +#endif this->internal_send_state_to_frontend(state); +#ifdef USE_SENSOR_FILTER } else { this->filter_list_->input(state); } +#endif } void Sensor::add_on_state_callback(std::function &&callback) { this->callback_.add(std::move(callback)); } @@ -80,6 +84,7 @@ void Sensor::add_on_raw_state_callback(std::function &&callback) { this->raw_callback_.add(std::move(callback)); } +#ifdef USE_SENSOR_FILTER void Sensor::add_filter(Filter *filter) { // inefficient, but only happens once on every sensor setup and nobody's going to have massive amounts of // filters @@ -109,6 +114,7 @@ void Sensor::clear_filters() { } this->filter_list_ = nullptr; } +#endif // USE_SENSOR_FILTER float Sensor::get_state() const { return this->state; } float Sensor::get_raw_state() const { return this->raw_state; } diff --git a/esphome/components/sensor/sensor.h b/esphome/components/sensor/sensor.h index 80981b8e28..54e75ee2a1 100644 --- a/esphome/components/sensor/sensor.h +++ b/esphome/components/sensor/sensor.h @@ -4,13 +4,17 @@ #include "esphome/core/entity_base.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" +#ifdef USE_SENSOR_FILTER #include "esphome/components/sensor/filter.h" +#endif #include #include namespace esphome::sensor { +class Sensor; + void log_sensor(const char *tag, const char *prefix, const char *type, Sensor *obj); #define LOG_SENSOR(prefix, type, obj) log_sensor(TAG, prefix, LOG_STR_LITERAL(type), obj) @@ -67,6 +71,7 @@ class Sensor : public EntityBase, public EntityBase_DeviceClass, public EntityBa /// Set force update mode. void set_force_update(bool force_update) { sensor_flags_.force_update = force_update; } +#ifdef USE_SENSOR_FILTER /// Add a filter to the filter chain. Will be appended to the back. void add_filter(Filter *filter); @@ -87,6 +92,7 @@ class Sensor : public EntityBase, public EntityBase_DeviceClass, public EntityBa /// Clear the entire filter chain. void clear_filters(); +#endif /// Getter-syntax for .state. float get_state() const; @@ -130,7 +136,9 @@ class Sensor : public EntityBase, public EntityBase_DeviceClass, public EntityBa LazyCallbackManager raw_callback_; ///< Storage for raw state callbacks. LazyCallbackManager callback_; ///< Storage for filtered state callbacks. +#ifdef USE_SENSOR_FILTER Filter *filter_list_{nullptr}; ///< Store all active filters. +#endif // Group small members together to avoid padding int8_t accuracy_decimals_{-1}; ///< Accuracy in decimals (-1 = not set) diff --git a/esphome/components/sound_level/sound_level.h b/esphome/components/sound_level/sound_level.h index dc35f69fe2..a1021eb1e8 100644 --- a/esphome/components/sound_level/sound_level.h +++ b/esphome/components/sound_level/sound_level.h @@ -6,6 +6,7 @@ #include "esphome/components/microphone/microphone_source.h" #include "esphome/components/sensor/sensor.h" +#include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/ring_buffer.h" diff --git a/esphome/components/wireguard/wireguard.h b/esphome/components/wireguard/wireguard.h index e8470c75cd..c11d592cd1 100644 --- a/esphome/components/wireguard/wireguard.h +++ b/esphome/components/wireguard/wireguard.h @@ -4,6 +4,7 @@ #include #include +#include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/helpers.h" #include "esphome/components/time/real_time_clock.h" diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 99e80785ec..ff32edff16 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -112,6 +112,7 @@ #define USE_SAFE_MODE_CALLBACK #define USE_SELECT #define USE_SENSOR +#define USE_SENSOR_FILTER #define USE_SETUP_PRIORITY_OVERRIDE #define USE_STATUS_LED #define USE_STATUS_SENSOR From d239a2400dddfb9854ea5e6c7f476002b6bf22c4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Feb 2026 21:36:21 -0600 Subject: [PATCH 190/197] [text_sensor] Conditionally compile filter infrastructure (#14213) --- esphome/components/text_sensor/__init__.py | 1 + esphome/components/text_sensor/filter.cpp | 5 +++++ esphome/components/text_sensor/filter.h | 5 +++++ esphome/components/text_sensor/text_sensor.cpp | 18 +++++++++++++----- esphome/components/text_sensor/text_sensor.h | 8 ++++++++ esphome/core/defines.h | 1 + 6 files changed, 33 insertions(+), 5 deletions(-) diff --git a/esphome/components/text_sensor/__init__.py b/esphome/components/text_sensor/__init__.py index 0d22400a8e..2e8edb43c9 100644 --- a/esphome/components/text_sensor/__init__.py +++ b/esphome/components/text_sensor/__init__.py @@ -204,6 +204,7 @@ async def setup_text_sensor_core_(var, config): cg.add(var.set_device_class(device_class)) if config.get(CONF_FILTERS): # must exist and not be empty + cg.add_define("USE_TEXT_SENSOR_FILTER") filters = await build_filters(config[CONF_FILTERS]) cg.add(var.set_filters(filters)) diff --git a/esphome/components/text_sensor/filter.cpp b/esphome/components/text_sensor/filter.cpp index 4ee12e8602..f6552c7c66 100644 --- a/esphome/components/text_sensor/filter.cpp +++ b/esphome/components/text_sensor/filter.cpp @@ -1,3 +1,6 @@ +#include "esphome/core/defines.h" +#ifdef USE_TEXT_SENSOR_FILTER + #include "filter.h" #include "text_sensor.h" #include "esphome/core/log.h" @@ -106,3 +109,5 @@ bool MapFilter::new_value(std::string &value) { } // namespace text_sensor } // namespace esphome + +#endif // USE_TEXT_SENSOR_FILTER diff --git a/esphome/components/text_sensor/filter.h b/esphome/components/text_sensor/filter.h index 1922b503ca..f88e8645cc 100644 --- a/esphome/components/text_sensor/filter.h +++ b/esphome/components/text_sensor/filter.h @@ -1,5 +1,8 @@ #pragma once +#include "esphome/core/defines.h" +#ifdef USE_TEXT_SENSOR_FILTER + #include "esphome/core/component.h" #include "esphome/core/helpers.h" @@ -164,3 +167,5 @@ class MapFilter : public Filter { } // namespace text_sensor } // namespace esphome + +#endif // USE_TEXT_SENSOR_FILTER diff --git a/esphome/components/text_sensor/text_sensor.cpp b/esphome/components/text_sensor/text_sensor.cpp index c48bdf4b82..c66d08ec40 100644 --- a/esphome/components/text_sensor/text_sensor.cpp +++ b/esphome/components/text_sensor/text_sensor.cpp @@ -24,7 +24,9 @@ void TextSensor::publish_state(const std::string &state) { this->publish_state(s void TextSensor::publish_state(const char *state) { this->publish_state(state, strlen(state)); } void TextSensor::publish_state(const char *state, size_t len) { +#ifdef USE_TEXT_SENSOR_FILTER if (this->filter_list_ == nullptr) { +#endif // No filters: raw_state == state, store once and use for both callbacks // Only assign if changed to avoid heap allocation if (len != this->state.size() || memcmp(state, this->state.data(), len) != 0) { @@ -33,6 +35,7 @@ void TextSensor::publish_state(const char *state, size_t len) { this->raw_callback_.call(this->state); ESP_LOGV(TAG, "'%s': Received new state %s", this->name_.c_str(), this->state.c_str()); this->notify_frontend_(); +#ifdef USE_TEXT_SENSOR_FILTER } else { // Has filters: need separate raw storage #pragma GCC diagnostic push @@ -46,8 +49,10 @@ void TextSensor::publish_state(const char *state, size_t len) { this->filter_list_->input(this->raw_state); #pragma GCC diagnostic pop } +#endif } +#ifdef USE_TEXT_SENSOR_FILTER void TextSensor::add_filter(Filter *filter) { // inefficient, but only happens once on every sensor setup and nobody's going to have massive amounts of // filters @@ -77,6 +82,7 @@ void TextSensor::clear_filters() { } this->filter_list_ = nullptr; } +#endif // USE_TEXT_SENSOR_FILTER void TextSensor::add_on_state_callback(std::function callback) { this->callback_.add(std::move(callback)); @@ -87,14 +93,16 @@ void TextSensor::add_on_raw_state_callback(std::functionstate; } const std::string &TextSensor::get_raw_state() const { - if (this->filter_list_ == nullptr) { - return this->state; // No filters, raw == filtered - } -// Suppress deprecation warning - get_raw_state() is the replacement API +#ifdef USE_TEXT_SENSOR_FILTER + if (this->filter_list_ != nullptr) { + // Suppress deprecation warning - get_raw_state() is the replacement API #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wdeprecated-declarations" - return this->raw_state; + return this->raw_state; #pragma GCC diagnostic pop + } +#endif + return this->state; // No filters, raw == filtered } void TextSensor::internal_send_state_to_frontend(const std::string &state) { this->internal_send_state_to_frontend(state.data(), state.size()); diff --git a/esphome/components/text_sensor/text_sensor.h b/esphome/components/text_sensor/text_sensor.h index 1352a8c1e4..97373dc716 100644 --- a/esphome/components/text_sensor/text_sensor.h +++ b/esphome/components/text_sensor/text_sensor.h @@ -3,7 +3,9 @@ #include "esphome/core/component.h" #include "esphome/core/entity_base.h" #include "esphome/core/helpers.h" +#ifdef USE_TEXT_SENSOR_FILTER #include "esphome/components/text_sensor/filter.h" +#endif #include #include @@ -11,6 +13,8 @@ namespace esphome { namespace text_sensor { +class TextSensor; + void log_text_sensor(const char *tag, const char *prefix, const char *type, TextSensor *obj); #define LOG_TEXT_SENSOR(prefix, type, obj) log_text_sensor(TAG, prefix, LOG_STR_LITERAL(type), obj) @@ -45,6 +49,7 @@ class TextSensor : public EntityBase, public EntityBase_DeviceClass { void publish_state(const char *state); void publish_state(const char *state, size_t len); +#ifdef USE_TEXT_SENSOR_FILTER /// Add a filter to the filter chain. Will be appended to the back. void add_filter(Filter *filter); @@ -56,6 +61,7 @@ class TextSensor : public EntityBase, public EntityBase_DeviceClass { /// Clear the entire filter chain. void clear_filters(); +#endif void add_on_state_callback(std::function callback); /// Add a callback that will be called every time the sensor sends a raw value. @@ -73,7 +79,9 @@ class TextSensor : public EntityBase, public EntityBase_DeviceClass { LazyCallbackManager raw_callback_; ///< Storage for raw state callbacks. LazyCallbackManager callback_; ///< Storage for filtered state callbacks. +#ifdef USE_TEXT_SENSOR_FILTER Filter *filter_list_{nullptr}; ///< Store all active filters. +#endif }; } // namespace text_sensor diff --git a/esphome/core/defines.h b/esphome/core/defines.h index ff32edff16..a1e3d1707f 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -119,6 +119,7 @@ #define USE_SWITCH #define USE_TEXT #define USE_TEXT_SENSOR +#define USE_TEXT_SENSOR_FILTER #define USE_TIME #define USE_TOUCHSCREEN #define USE_UART_DEBUGGER From 5c388a5200b65a9e4470d4f4a681e41c49517915 Mon Sep 17 00:00:00 2001 From: schrob <83939986+schdro@users.noreply.github.com> Date: Mon, 23 Feb 2026 04:39:36 +0100 Subject: [PATCH 191/197] [openthread_info] Optimize: Devirtualize/unify (#14208) --- .../openthread_info_text_sensor.h | 28 +++++++------------ 1 file changed, 10 insertions(+), 18 deletions(-) diff --git a/esphome/components/openthread_info/openthread_info_text_sensor.h b/esphome/components/openthread_info/openthread_info_text_sensor.h index ac5623e0c1..10e83281f0 100644 --- a/esphome/components/openthread_info/openthread_info_text_sensor.h +++ b/esphome/components/openthread_info/openthread_info_text_sensor.h @@ -25,7 +25,7 @@ class OpenThreadInstancePollingComponent : public PollingComponent { virtual void update_instance(otInstance *instance) = 0; }; -class IPAddressOpenThreadInfo : public PollingComponent, public text_sensor::TextSensor { +class IPAddressOpenThreadInfo final : public PollingComponent, public text_sensor::TextSensor { public: void update() override { std::optional address = openthread::global_openthread_component->get_omr_address(); @@ -48,7 +48,7 @@ class IPAddressOpenThreadInfo : public PollingComponent, public text_sensor::Tex std::string last_ip_; }; -class RoleOpenThreadInfo : public OpenThreadInstancePollingComponent, public text_sensor::TextSensor { +class RoleOpenThreadInfo final : public OpenThreadInstancePollingComponent, public text_sensor::TextSensor { public: void update_instance(otInstance *instance) override { otDeviceRole role = otThreadGetDeviceRole(instance); @@ -64,7 +64,7 @@ class RoleOpenThreadInfo : public OpenThreadInstancePollingComponent, public tex otDeviceRole last_role_; }; -class Rloc16OpenThreadInfo : public OpenThreadInstancePollingComponent, public text_sensor::TextSensor { +class Rloc16OpenThreadInfo final : public OpenThreadInstancePollingComponent, public text_sensor::TextSensor { public: void update_instance(otInstance *instance) override { uint16_t rloc16 = otThreadGetRloc16(instance); @@ -75,14 +75,13 @@ class Rloc16OpenThreadInfo : public OpenThreadInstancePollingComponent, public t this->publish_state(buf); } } - float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } void dump_config() override; protected: uint16_t last_rloc16_; }; -class ExtAddrOpenThreadInfo : public OpenThreadInstancePollingComponent, public text_sensor::TextSensor { +class ExtAddrOpenThreadInfo final : public OpenThreadInstancePollingComponent, public text_sensor::TextSensor { public: void update_instance(otInstance *instance) override { const auto *extaddr = otLinkGetExtendedAddress(instance); @@ -93,14 +92,13 @@ class ExtAddrOpenThreadInfo : public OpenThreadInstancePollingComponent, public this->publish_state(buf); } } - float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } void dump_config() override; protected: std::array last_extaddr_{}; }; -class Eui64OpenThreadInfo : public OpenThreadInstancePollingComponent, public text_sensor::TextSensor { +class Eui64OpenThreadInfo final : public OpenThreadInstancePollingComponent, public text_sensor::TextSensor { public: void update_instance(otInstance *instance) override { otExtAddress addr; @@ -113,14 +111,13 @@ class Eui64OpenThreadInfo : public OpenThreadInstancePollingComponent, public te this->publish_state(buf); } } - float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } void dump_config() override; protected: std::array last_eui64_{}; }; -class ChannelOpenThreadInfo : public OpenThreadInstancePollingComponent, public text_sensor::TextSensor { +class ChannelOpenThreadInfo final : public OpenThreadInstancePollingComponent, public text_sensor::TextSensor { public: void update_instance(otInstance *instance) override { uint8_t channel = otLinkGetChannel(instance); @@ -131,7 +128,6 @@ class ChannelOpenThreadInfo : public OpenThreadInstancePollingComponent, public this->publish_state(buf); } } - float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } void dump_config() override; protected: @@ -153,7 +149,7 @@ class DatasetOpenThreadInfo : public OpenThreadInstancePollingComponent { virtual void update_dataset(otOperationalDataset *dataset) = 0; }; -class NetworkNameOpenThreadInfo : public DatasetOpenThreadInfo, public text_sensor::TextSensor { +class NetworkNameOpenThreadInfo final : public DatasetOpenThreadInfo, public text_sensor::TextSensor { public: void update_dataset(otOperationalDataset *dataset) override { if (this->last_network_name_ != dataset->mNetworkName.m8) { @@ -161,14 +157,13 @@ class NetworkNameOpenThreadInfo : public DatasetOpenThreadInfo, public text_sens this->publish_state(this->last_network_name_); } } - float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } void dump_config() override; protected: std::string last_network_name_; }; -class NetworkKeyOpenThreadInfo : public DatasetOpenThreadInfo, public text_sensor::TextSensor { +class NetworkKeyOpenThreadInfo final : public DatasetOpenThreadInfo, public text_sensor::TextSensor { public: void update_dataset(otOperationalDataset *dataset) override { if (!std::equal(this->last_key_.begin(), this->last_key_.end(), dataset->mNetworkKey.m8)) { @@ -178,14 +173,13 @@ class NetworkKeyOpenThreadInfo : public DatasetOpenThreadInfo, public text_senso this->publish_state(buf); } } - float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } void dump_config() override; protected: std::array last_key_{}; }; -class PanIdOpenThreadInfo : public DatasetOpenThreadInfo, public text_sensor::TextSensor { +class PanIdOpenThreadInfo final : public DatasetOpenThreadInfo, public text_sensor::TextSensor { public: void update_dataset(otOperationalDataset *dataset) override { uint16_t panid = dataset->mPanId; @@ -196,14 +190,13 @@ class PanIdOpenThreadInfo : public DatasetOpenThreadInfo, public text_sensor::Te this->publish_state(buf); } } - float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } void dump_config() override; protected: uint16_t last_panid_; }; -class ExtPanIdOpenThreadInfo : public DatasetOpenThreadInfo, public text_sensor::TextSensor { +class ExtPanIdOpenThreadInfo final : public DatasetOpenThreadInfo, public text_sensor::TextSensor { public: void update_dataset(otOperationalDataset *dataset) override { if (!std::equal(this->last_extpanid_.begin(), this->last_extpanid_.end(), dataset->mExtendedPanId.m8)) { @@ -214,7 +207,6 @@ class ExtPanIdOpenThreadInfo : public DatasetOpenThreadInfo, public text_sensor: } } - float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } void dump_config() override; protected: From 680160453355210018b073e0036c256d45a143ce Mon Sep 17 00:00:00 2001 From: schrob <83939986+schdro@users.noreply.github.com> Date: Mon, 23 Feb 2026 04:40:40 +0100 Subject: [PATCH 192/197] [openthread] Add Thread version DEBUG trace (#14196) --- esphome/components/openthread/openthread_esp.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/components/openthread/openthread_esp.cpp b/esphome/components/openthread/openthread_esp.cpp index ec212d1f68..4f8db0634a 100644 --- a/esphome/components/openthread/openthread_esp.cpp +++ b/esphome/components/openthread/openthread_esp.cpp @@ -104,6 +104,8 @@ void OpenThreadComponent::ot_main() { esp_cli_custom_command_init(); #endif // CONFIG_OPENTHREAD_CLI_ESP_EXTENSION + ESP_LOGD(TAG, "Thread Version: %" PRIu16, otThreadGetVersion()); + otLinkModeConfig link_mode_config{}; #if CONFIG_OPENTHREAD_FTD link_mode_config.mRxOnWhenIdle = true; From ee94bc471541a460c989b51cf317530b7ffa4611 Mon Sep 17 00:00:00 2001 From: schrob <83939986+schdro@users.noreply.github.com> Date: Mon, 23 Feb 2026 04:43:42 +0100 Subject: [PATCH 193/197] [openthread] Refactor to optimize and match code rules (#14156) --- esphome/components/openthread/openthread.cpp | 4 +-- esphome/components/openthread/openthread.h | 10 +++---- .../components/openthread/openthread_esp.cpp | 29 ++++++++++--------- 3 files changed, 23 insertions(+), 20 deletions(-) diff --git a/esphome/components/openthread/openthread.cpp b/esphome/components/openthread/openthread.cpp index 90da17e2d3..d22a14aeae 100644 --- a/esphome/components/openthread/openthread.cpp +++ b/esphome/components/openthread/openthread.cpp @@ -35,9 +35,9 @@ void OpenThreadComponent::dump_config() { #elif CONFIG_OPENTHREAD_MTD ESP_LOGCONFIG(TAG, " Device Type: MTD"); // TBD: Synchronized Sleepy End Device - if (this->poll_period > 0) { + if (this->poll_period_ > 0) { ESP_LOGCONFIG(TAG, " Device is configured as Sleepy End Device (SED)"); - uint32_t duration = this->poll_period / 1000; + uint32_t duration = this->poll_period_ / 1000; ESP_LOGCONFIG(TAG, " Poll Period: %" PRIu32 "s", duration); } else { ESP_LOGCONFIG(TAG, " Device is configured as Minimal End Device (MED)"); diff --git a/esphome/components/openthread/openthread.h b/esphome/components/openthread/openthread.h index 3c60acaadd..9e429f289b 100644 --- a/esphome/components/openthread/openthread.h +++ b/esphome/components/openthread/openthread.h @@ -36,22 +36,22 @@ class OpenThreadComponent : public Component { const char *get_use_address() const; void set_use_address(const char *use_address); #if CONFIG_OPENTHREAD_MTD - void set_poll_period(uint32_t poll_period) { this->poll_period = poll_period; } + void set_poll_period(uint32_t poll_period) { this->poll_period_ = poll_period; } #endif protected: std::optional get_omr_address_(InstanceLock &lock); + std::function factory_reset_external_callback_; +#if CONFIG_OPENTHREAD_MTD + uint32_t poll_period_{0}; +#endif bool teardown_started_{false}; bool teardown_complete_{false}; - std::function factory_reset_external_callback_; private: // Stores a pointer to a string literal (static storage duration). // ONLY set from Python-generated code with string literals - never dynamic strings. const char *use_address_{""}; -#if CONFIG_OPENTHREAD_MTD - uint32_t poll_period{0}; -#endif }; extern OpenThreadComponent *global_openthread_component; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) diff --git a/esphome/components/openthread/openthread_esp.cpp b/esphome/components/openthread/openthread_esp.cpp index 4f8db0634a..bb70701471 100644 --- a/esphome/components/openthread/openthread_esp.cpp +++ b/esphome/components/openthread/openthread_esp.cpp @@ -81,9 +81,11 @@ void OpenThreadComponent::ot_main() { // Initialize the OpenThread stack // otLoggingSetLevel(OT_LOG_LEVEL_DEBG); ESP_ERROR_CHECK(esp_openthread_init(&config)); + // Fetch OT instance once to avoid repeated call into OT stack + otInstance *instance = esp_openthread_get_instance(); #if CONFIG_OPENTHREAD_STATE_INDICATOR_ENABLE - ESP_ERROR_CHECK(esp_openthread_state_indicator_init(esp_openthread_get_instance())); + ESP_ERROR_CHECK(esp_openthread_state_indicator_init(instance)); #endif #if CONFIG_OPENTHREAD_LOG_LEVEL_DYNAMIC @@ -112,28 +114,29 @@ void OpenThreadComponent::ot_main() { link_mode_config.mDeviceType = true; link_mode_config.mNetworkData = true; #elif CONFIG_OPENTHREAD_MTD - if (this->poll_period > 0) { - if (otLinkSetPollPeriod(esp_openthread_get_instance(), this->poll_period) != OT_ERROR_NONE) { - ESP_LOGE(TAG, "Failed to set OpenThread pollperiod."); + if (this->poll_period_ > 0) { + if (otLinkSetPollPeriod(instance, this->poll_period_) != OT_ERROR_NONE) { + ESP_LOGE(TAG, "Failed to set pollperiod"); } - uint32_t link_polling_period = otLinkGetPollPeriod(esp_openthread_get_instance()); - ESP_LOGD(TAG, "Link Polling Period: %" PRIu32, link_polling_period); + ESP_LOGD(TAG, "Link Polling Period: %" PRIu32, otLinkGetPollPeriod(instance)); } - link_mode_config.mRxOnWhenIdle = this->poll_period == 0; + link_mode_config.mRxOnWhenIdle = this->poll_period_ == 0; link_mode_config.mDeviceType = false; link_mode_config.mNetworkData = false; #endif - if (otThreadSetLinkMode(esp_openthread_get_instance(), link_mode_config) != OT_ERROR_NONE) { - ESP_LOGE(TAG, "Failed to set OpenThread linkmode."); + if (otThreadSetLinkMode(instance, link_mode_config) != OT_ERROR_NONE) { + ESP_LOGE(TAG, "Failed to set linkmode"); } - link_mode_config = otThreadGetLinkMode(esp_openthread_get_instance()); +#ifdef ESPHOME_LOG_HAS_DEBUG // Fetch link mode from OT only when DEBUG + link_mode_config = otThreadGetLinkMode(instance); ESP_LOGD(TAG, "Link Mode Device Type: %s\n" "Link Mode Network Data: %s\n" "Link Mode RX On When Idle: %s", - link_mode_config.mDeviceType ? "true" : "false", link_mode_config.mNetworkData ? "true" : "false", - link_mode_config.mRxOnWhenIdle ? "true" : "false"); + TRUEFALSE(link_mode_config.mDeviceType), TRUEFALSE(link_mode_config.mNetworkData), + TRUEFALSE(link_mode_config.mRxOnWhenIdle)); +#endif // Run the main loop #if CONFIG_OPENTHREAD_CLI @@ -144,7 +147,7 @@ void OpenThreadComponent::ot_main() { #ifndef USE_OPENTHREAD_FORCE_DATASET // Check if openthread has a valid dataset from a previous execution - otError error = otDatasetGetActiveTlvs(esp_openthread_get_instance(), &dataset); + otError error = otDatasetGetActiveTlvs(instance, &dataset); if (error != OT_ERROR_NONE) { // Make sure the length is 0 so we fallback to the configuration dataset.mLength = 0; From 417f4535af983e90d5ac2407847a29f15cb4905e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Feb 2026 22:25:20 -0600 Subject: [PATCH 194/197] [logger] Use subtraction-based line number formatting to avoid division (#14219) --- esphome/components/logger/log_buffer.h | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/esphome/components/logger/log_buffer.h b/esphome/components/logger/log_buffer.h index 3d87278248..a56276f732 100644 --- a/esphome/components/logger/log_buffer.h +++ b/esphome/components/logger/log_buffer.h @@ -75,18 +75,13 @@ struct LogBuffer { *p++ = ':'; - // Format line number without modulo operations + // Format line number using subtraction loops (no division - important for ESP8266 which lacks hardware divider) if (line > 999) [[unlikely]] { - int thousands = line / 1000; - *p++ = '0' + thousands; - line -= thousands * 1000; + write_digit(p, line, 1000); } - int hundreds = line / 100; - int remainder = line - hundreds * 100; - int tens = remainder / 10; - *p++ = '0' + hundreds; - *p++ = '0' + tens; - *p++ = '0' + (remainder - tens * 10); + write_digit(p, line, 100); + write_digit(p, line, 10); + *p++ = '0' + line; *p++ = ']'; #if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) || defined(USE_HOST) @@ -162,6 +157,15 @@ struct LogBuffer { this->process_vsnprintf_result_(vsnprintf_P(this->current_(), this->remaining_(), format, args)); } #endif + // Extract one decimal digit via subtraction (no division - important for ESP8266) + static inline void ESPHOME_ALWAYS_INLINE write_digit(char *&p, int &value, int divisor) { + char d = '0'; + while (value >= divisor) { + d++; + value -= divisor; + } + *p++ = d; + } // Write ANSI color escape sequence to buffer, updates pointer in place // Caller is responsible for ensuring buffer has sufficient space void write_ansi_color_(char *&p, uint8_t level) { From fb6c7d81d5b3832c19eb1dd7b61c70a944742d8a Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 23 Feb 2026 00:08:40 -0500 Subject: [PATCH 195/197] [core] Fix multiline log continuations without leading whitespace (#14217) Co-authored-by: Claude Opus 4.6 --- .../absolute_humidity/absolute_humidity.cpp | 5 +- esphome/components/anova/anova.cpp | 6 +- esphome/components/as3935/as3935.cpp | 6 +- esphome/components/at581x/at581x.cpp | 16 ++--- .../components/binary_sensor/automation.cpp | 6 +- esphome/components/bl0940/bl0940.cpp | 5 +- .../current_based/current_based_cover.cpp | 8 +-- esphome/components/debug/debug_esp32.cpp | 60 ++++++++++--------- esphome/components/debug/debug_esp8266.cpp | 18 +++--- esphome/components/debug/debug_libretiny.cpp | 14 +++-- esphome/components/debug/debug_zephyr.cpp | 42 +++++++------ .../components/dfrobot_sen0395/commands.cpp | 16 ++--- esphome/components/emmeti/emmeti.cpp | 19 ++---- .../components/ens160_base/ens160_base.cpp | 13 ++-- esphome/components/es8388/es8388.cpp | 7 ++- .../esp32_ble_client/ble_client_base.cpp | 6 +- .../update/esp32_hosted_update.cpp | 11 ++-- .../packet_transport/espnow_transport.cpp | 8 +-- .../ethernet/ethernet_component.cpp | 5 +- esphome/components/ezo_pmp/ezo_pmp.cpp | 6 +- esphome/components/gcja5/gcja5.cpp | 8 +-- .../graphical_display_menu.cpp | 14 ++--- .../components/honeywellabp/honeywellabp.cpp | 5 +- .../http_request/ota/ota_http_request.cpp | 3 +- .../components/ina2xx_base/ina2xx_base.cpp | 6 +- esphome/components/ld2410/ld2410.cpp | 7 +-- esphome/components/ld2412/ld2412.cpp | 5 +- esphome/components/ledc/ledc_output.cpp | 6 +- esphome/components/max6956/max6956.cpp | 6 +- .../components/mqtt/mqtt_backend_esp32.cpp | 5 +- .../nextion/nextion_upload_arduino.cpp | 22 ++----- .../nextion/nextion_upload_esp32.cpp | 30 ++-------- .../components/openthread/openthread_esp.cpp | 8 +-- .../components/pi4ioe5v6408/pi4ioe5v6408.cpp | 7 ++- esphome/components/pipsolar/pipsolar.cpp | 3 +- esphome/components/pn532/pn532.cpp | 5 +- esphome/components/pn7150/pn7150.cpp | 15 +++-- esphome/components/pn7160/pn7160.cpp | 16 +++-- esphome/components/qmp6988/qmp6988.cpp | 26 ++++---- .../remote_base/pronto_protocol.cpp | 5 +- esphome/components/safe_mode/safe_mode.cpp | 10 ++-- esphome/components/sim800l/sim800l.cpp | 2 +- esphome/components/sonoff_d1/sonoff_d1.cpp | 10 +--- esphome/components/sun/sun.cpp | 29 ++++----- .../components/usb_host/usb_host_client.cpp | 44 +++++++------- esphome/components/usb_uart/cp210x.cpp | 6 +- .../voice_assistant/voice_assistant.cpp | 4 +- esphome/components/wl_134/wl_134.cpp | 13 ++-- esphome/components/xgzp68xx/xgzp68xx.cpp | 6 +- script/ci-custom.py | 33 ++++++++++ 50 files changed, 291 insertions(+), 345 deletions(-) diff --git a/esphome/components/absolute_humidity/absolute_humidity.cpp b/esphome/components/absolute_humidity/absolute_humidity.cpp index 9c66531d05..40676f8655 100644 --- a/esphome/components/absolute_humidity/absolute_humidity.cpp +++ b/esphome/components/absolute_humidity/absolute_humidity.cpp @@ -92,10 +92,7 @@ void AbsoluteHumidityComponent::loop() { // Calculate absolute humidity const float absolute_humidity = vapor_density(es, hr, temperature_k); - ESP_LOGD(TAG, - "Saturation vapor pressure %f kPa\n" - "Publishing absolute humidity %f g/m³", - es, absolute_humidity); + ESP_LOGD(TAG, "Saturation vapor pressure %f kPa, absolute humidity %f g/m³", es, absolute_humidity); // Publish absolute humidity this->status_clear_warning(); diff --git a/esphome/components/anova/anova.cpp b/esphome/components/anova/anova.cpp index 5054488089..2693224a97 100644 --- a/esphome/components/anova/anova.cpp +++ b/esphome/components/anova/anova.cpp @@ -67,10 +67,8 @@ void Anova::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_ case ESP_GATTC_SEARCH_CMPL_EVT: { auto *chr = this->parent_->get_characteristic(ANOVA_SERVICE_UUID, ANOVA_CHARACTERISTIC_UUID); if (chr == nullptr) { - ESP_LOGW(TAG, - "[%s] No control service found at device, not an Anova..?\n" - "[%s] Note, this component does not currently support Anova Nano.", - this->get_name().c_str(), this->get_name().c_str()); + ESP_LOGW(TAG, "[%s] No control service found at device, not an Anova..?", this->get_name().c_str()); + ESP_LOGW(TAG, "[%s] Note, this component does not currently support Anova Nano.", this->get_name().c_str()); break; } this->char_handle_ = chr->handle; diff --git a/esphome/components/as3935/as3935.cpp b/esphome/components/as3935/as3935.cpp index dd0ab714f7..c4dc0466a0 100644 --- a/esphome/components/as3935/as3935.cpp +++ b/esphome/components/as3935/as3935.cpp @@ -307,9 +307,9 @@ void AS3935Component::tune_antenna() { uint8_t tune_val = this->read_capacitance(); ESP_LOGI(TAG, "Starting antenna tuning\n" - "Division Ratio is set to: %d\n" - "Internal Capacitor is set to: %d\n" - "Displaying oscillator on INT pin. Measure its frequency - multiply value by Division Ratio", + " Division Ratio is set to: %d\n" + " Internal Capacitor is set to: %d\n" + " Displaying oscillator on INT pin. Measure its frequency - multiply value by Division Ratio", div_ratio, tune_val); this->display_oscillator(true, ANTFREQ); } diff --git a/esphome/components/at581x/at581x.cpp b/esphome/components/at581x/at581x.cpp index 6804a7f4b5..728fbe20c6 100644 --- a/esphome/components/at581x/at581x.cpp +++ b/esphome/components/at581x/at581x.cpp @@ -77,14 +77,14 @@ void AT581XComponent::dump_config() { LOG_I2C_DEVICE(this); } bool AT581XComponent::i2c_write_config() { ESP_LOGCONFIG(TAG, "Writing new config for AT581X\n" - "Frequency: %dMHz\n" - "Sensing distance: %d\n" - "Power: %dµA\n" - "Gain: %d\n" - "Trigger base time: %dms\n" - "Trigger keep time: %dms\n" - "Protect time: %dms\n" - "Self check time: %dms", + " Frequency: %dMHz\n" + " Sensing distance: %d\n" + " Power: %dµA\n" + " Gain: %d\n" + " Trigger base time: %dms\n" + " Trigger keep time: %dms\n" + " Protect time: %dms\n" + " Self check time: %dms", this->freq_, this->delta_, this->power_, this->gain_, this->trigger_base_time_ms_, this->trigger_keep_time_ms_, this->protect_time_ms_, this->self_check_time_ms_); diff --git a/esphome/components/binary_sensor/automation.cpp b/esphome/components/binary_sensor/automation.cpp index faebe7e88f..7e43d42357 100644 --- a/esphome/components/binary_sensor/automation.cpp +++ b/esphome/components/binary_sensor/automation.cpp @@ -29,10 +29,8 @@ void MultiClickTrigger::on_state_(bool state) { // Start matching MultiClickTriggerEvent evt = this->timing_[0]; if (evt.state == state) { - ESP_LOGV(TAG, - "START min=%" PRIu32 " max=%" PRIu32 "\n" - "Multi Click: Starting multi click action!", - evt.min_length, evt.max_length); + ESP_LOGV(TAG, "START min=%" PRIu32 " max=%" PRIu32, evt.min_length, evt.max_length); + ESP_LOGV(TAG, "Multi Click: Starting multi click action!"); this->at_index_ = 1; if (this->timing_.size() == 1 && evt.max_length == 4294967294UL) { this->set_timeout(MULTICLICK_TRIGGER_ID, evt.min_length, [this]() { this->trigger_(); }); diff --git a/esphome/components/bl0940/bl0940.cpp b/esphome/components/bl0940/bl0940.cpp index 42e20eb69b..31625ebf6d 100644 --- a/esphome/components/bl0940/bl0940.cpp +++ b/esphome/components/bl0940/bl0940.cpp @@ -182,7 +182,10 @@ void BL0940::recalibrate_() { ESP_LOGD(TAG, "Recalibrated reference values:\n" - "Voltage: %f\n, Current: %f\n, Power: %f\n, Energy: %f\n", + " Voltage: %f\n" + " Current: %f\n" + " Power: %f\n" + " Energy: %f", this->voltage_reference_cal_, this->current_reference_cal_, this->power_reference_cal_, this->energy_reference_cal_); } diff --git a/esphome/components/current_based/current_based_cover.cpp b/esphome/components/current_based/current_based_cover.cpp index 5dfaeeff39..58ae7cbc34 100644 --- a/esphome/components/current_based/current_based_cover.cpp +++ b/esphome/components/current_based/current_based_cover.cpp @@ -148,14 +148,14 @@ void CurrentBasedCover::dump_config() { } ESP_LOGCONFIG(TAG, " Close Duration: %.1fs\n" - "Obstacle Rollback: %.1f%%", + " Obstacle Rollback: %.1f%%", this->close_duration_ / 1e3f, this->obstacle_rollback_ * 100); if (this->max_duration_ != UINT32_MAX) { - ESP_LOGCONFIG(TAG, "Maximum duration: %.1fs", this->max_duration_ / 1e3f); + ESP_LOGCONFIG(TAG, " Maximum duration: %.1fs", this->max_duration_ / 1e3f); } ESP_LOGCONFIG(TAG, - "Start sensing delay: %.1fs\n" - "Malfunction detection: %s", + " Start sensing delay: %.1fs\n" + " Malfunction detection: %s", this->start_sensing_delay_ / 1e3f, YESNO(this->malfunction_detection_)); } diff --git a/esphome/components/debug/debug_esp32.cpp b/esphome/components/debug/debug_esp32.cpp index aad4c7426c..6898621dd0 100644 --- a/esphome/components/debug/debug_esp32.cpp +++ b/esphome/components/debug/debug_esp32.cpp @@ -79,7 +79,6 @@ const char *DebugComponent::get_reset_reason_(std::span } uint32_t flash_size = ESP.getFlashChipSize() / 1024; // NOLINT uint32_t flash_speed = ESP.getFlashChipSpeed() / 1000000; // NOLINT - ESP_LOGD(TAG, "Flash Chip: Size=%" PRIu32 "kB Speed=%" PRIu32 "MHz Mode=%s", flash_size, flash_speed, flash_mode); pos = buf_append_printf(buf, size, pos, "|Flash: %" PRIu32 "kB Speed:%" PRIu32 "MHz Mode:%s", flash_size, flash_speed, flash_mode); #endif @@ -194,39 +191,46 @@ size_t DebugComponent::get_device_info_(std::span if (info.features != 0) { pos = buf_append_printf(buf, size, pos, "%sOther:0x%" PRIx32, first_feature ? "" : ", ", info.features); } - ESP_LOGD(TAG, "Chip: Model=%s, Cores=%u, Revision=%u", model, info.cores, info.revision); pos = buf_append_printf(buf, size, pos, " Cores:%u Revision:%u", info.cores, info.revision); uint32_t cpu_freq_mhz = arch_get_cpu_freq_hz() / 1000000; - ESP_LOGD(TAG, "CPU Frequency: %" PRIu32 " MHz", cpu_freq_mhz); pos = buf_append_printf(buf, size, pos, "|CPU Frequency: %" PRIu32 " MHz", cpu_freq_mhz); - // Framework detection -#ifdef USE_ARDUINO - ESP_LOGD(TAG, "Framework: Arduino"); - pos = buf_append_printf(buf, size, pos, "|Framework: Arduino"); -#elif defined(USE_ESP32) - ESP_LOGD(TAG, "Framework: ESP-IDF"); - pos = buf_append_printf(buf, size, pos, "|Framework: ESP-IDF"); -#else - ESP_LOGW(TAG, "Framework: UNKNOWN"); - pos = buf_append_printf(buf, size, pos, "|Framework: UNKNOWN"); -#endif - - ESP_LOGD(TAG, "ESP-IDF Version: %s", esp_get_idf_version()); - pos = buf_append_printf(buf, size, pos, "|ESP-IDF: %s", esp_get_idf_version()); - - uint8_t mac[6]; - get_mac_address_raw(mac); - ESP_LOGD(TAG, "EFuse MAC: %02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); - pos = buf_append_printf(buf, size, pos, "|EFuse MAC: %02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], - mac[4], mac[5]); - char reason_buffer[RESET_REASON_BUFFER_SIZE]; const char *reset_reason = get_reset_reason_(std::span(reason_buffer)); - pos = buf_append_printf(buf, size, pos, "|Reset: %s", reset_reason); - const char *wakeup_cause = get_wakeup_cause_(std::span(reason_buffer)); + + uint8_t mac[6]; + get_mac_address_raw(mac); + + ESP_LOGD(TAG, + "ESP32 debug info:\n" + " Chip: %s\n" + " Cores: %u\n" + " Revision: %u\n" + " CPU Frequency: %" PRIu32 " MHz\n" + " ESP-IDF Version: %s\n" + " EFuse MAC: %02X:%02X:%02X:%02X:%02X:%02X\n" + " Reset Reason: %s\n" + " Wakeup Cause: %s", + model, info.cores, info.revision, cpu_freq_mhz, esp_get_idf_version(), mac[0], mac[1], mac[2], mac[3], + mac[4], mac[5], reset_reason, wakeup_cause); +#if defined(USE_ARDUINO) + ESP_LOGD(TAG, " Flash: Size=%" PRIu32 "kB Speed=%" PRIu32 "MHz Mode=%s", flash_size, flash_speed, flash_mode); +#endif + // Framework detection +#ifdef USE_ARDUINO + ESP_LOGD(TAG, " Framework: Arduino"); + pos = buf_append_printf(buf, size, pos, "|Framework: Arduino"); +#else + ESP_LOGD(TAG, " Framework: ESP-IDF"); + pos = buf_append_printf(buf, size, pos, "|Framework: ESP-IDF"); +#endif + + pos = buf_append_printf(buf, size, pos, "|ESP-IDF: %s", esp_get_idf_version()); + pos = buf_append_printf(buf, size, pos, "|EFuse MAC: %02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], + mac[4], mac[5]); + pos = buf_append_printf(buf, size, pos, "|Reset: %s", reset_reason); pos = buf_append_printf(buf, size, pos, "|Wakeup: %s", wakeup_cause); return pos; diff --git a/esphome/components/debug/debug_esp8266.cpp b/esphome/components/debug/debug_esp8266.cpp index 1a07ec4f3a..4df4aaa851 100644 --- a/esphome/components/debug/debug_esp8266.cpp +++ b/esphome/components/debug/debug_esp8266.cpp @@ -128,14 +128,16 @@ size_t DebugComponent::get_device_info_(std::span // NOLINTEND(readability-static-accessed-through-instance) ESP_LOGD(TAG, - "Chip ID: 0x%08" PRIX32 "\n" - "SDK Version: %s\n" - "Core Version: %s\n" - "Boot Version=%u Mode=%u\n" - "CPU Frequency: %u\n" - "Flash Chip ID=0x%08" PRIX32 "\n" - "Reset Reason: %s\n" - "Reset Info: %s", + "ESP8266 debug info:\n" + " Chip ID: 0x%08" PRIX32 "\n" + " SDK Version: %s\n" + " Core Version: %s\n" + " Boot Version: %u\n" + " Boot Mode: %u\n" + " CPU Frequency: %u\n" + " Flash Chip ID: 0x%08" PRIX32 "\n" + " Reset Reason: %s\n" + " Reset Info: %s", chip_id, sdk_version, get_core_version_str(core_version_buffer), boot_version, boot_mode, cpu_freq, flash_chip_id, reset_reason, get_reset_info_str(reset_info_buffer, resetInfo.reason)); diff --git a/esphome/components/debug/debug_libretiny.cpp b/esphome/components/debug/debug_libretiny.cpp index 14bbdb945a..39269d6f2f 100644 --- a/esphome/components/debug/debug_libretiny.cpp +++ b/esphome/components/debug/debug_libretiny.cpp @@ -27,12 +27,14 @@ size_t DebugComponent::get_device_info_(std::span uint32_t mac_id = lt_cpu_get_mac_id(); ESP_LOGD(TAG, - "LibreTiny Version: %s\n" - "Chip: %s (%04x) @ %u MHz\n" - "Chip ID: 0x%06" PRIX32 "\n" - "Board: %s\n" - "Flash: %" PRIu32 " KiB / RAM: %" PRIu32 " KiB\n" - "Reset Reason: %s", + "LibreTiny debug info:\n" + " Version: %s\n" + " Chip: %s (%04x) @ %u MHz\n" + " Chip ID: 0x%06" PRIX32 "\n" + " Board: %s\n" + " Flash: %" PRIu32 " KiB\n" + " RAM: %" PRIu32 " KiB\n" + " Reset Reason: %s", lt_get_version(), lt_cpu_get_model_name(), lt_cpu_get_model(), lt_cpu_get_freq_mhz(), mac_id, lt_get_board_code(), flash_kib, ram_kib, reset_reason); diff --git a/esphome/components/debug/debug_zephyr.cpp b/esphome/components/debug/debug_zephyr.cpp index ecca7150bd..bd6432e949 100644 --- a/esphome/components/debug/debug_zephyr.cpp +++ b/esphome/components/debug/debug_zephyr.cpp @@ -79,13 +79,13 @@ static void fa_cb(const struct flash_area *fa, void *user_data) { void DebugComponent::log_partition_info_() { #if CONFIG_FLASH_MAP_LABELS ESP_LOGCONFIG(TAG, "ID | Device | Device Name " - "| Label | Offset | Size\n" - "--------------------------------------------" + "| Label | Offset | Size"); + ESP_LOGCONFIG(TAG, "--------------------------------------------" "-----------------------------------------------"); #else ESP_LOGCONFIG(TAG, "ID | Device | Device Name " - "| Offset | Size\n" - "-----------------------------------------" + "| Offset | Size"); + ESP_LOGCONFIG(TAG, "-----------------------------------------" "------------------------------"); #endif flash_area_foreach(fa_cb, nullptr); @@ -284,11 +284,12 @@ size_t DebugComponent::get_device_info_(std::span char mac_pretty[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; get_mac_address_pretty_into_buffer(mac_pretty); ESP_LOGD(TAG, - "Code page size: %u, code size: %u, device id: 0x%08x%08x\n" - "Encryption root: 0x%08x%08x%08x%08x, Identity Root: 0x%08x%08x%08x%08x\n" - "Device address type: %s, address: %s\n" - "Part code: nRF%x, version: %c%c%c%c, package: %s\n" - "RAM: %ukB, Flash: %ukB, production test: %sdone", + "nRF debug info:\n" + " Code page size: %u, code size: %u, device id: 0x%08x%08x\n" + " Encryption root: 0x%08x%08x%08x%08x, Identity Root: 0x%08x%08x%08x%08x\n" + " Device address type: %s, address: %s\n" + " Part code: nRF%x, version: %c%c%c%c, package: %s\n" + " RAM: %ukB, Flash: %ukB, production test: %sdone", NRF_FICR->CODEPAGESIZE, NRF_FICR->CODESIZE, NRF_FICR->DEVICEID[1], NRF_FICR->DEVICEID[0], NRF_FICR->ER[0], NRF_FICR->ER[1], NRF_FICR->ER[2], NRF_FICR->ER[3], NRF_FICR->IR[0], NRF_FICR->IR[1], NRF_FICR->IR[2], NRF_FICR->IR[3], (NRF_FICR->DEVICEADDRTYPE & 0x1 ? "Random" : "Public"), mac_pretty, NRF_FICR->INFO.PART, @@ -299,23 +300,22 @@ size_t DebugComponent::get_device_info_(std::span (NRF_UICR->PSELRESET[0] & UICR_PSELRESET_CONNECT_Msk) == UICR_PSELRESET_CONNECT_Connected << UICR_PSELRESET_CONNECT_Pos; ESP_LOGD( - TAG, "GPIO as NFC pins: %s, GPIO as nRESET pin: %s", + TAG, " GPIO as NFC pins: %s, GPIO as nRESET pin: %s", YESNO((NRF_UICR->NFCPINS & UICR_NFCPINS_PROTECT_Msk) == (UICR_NFCPINS_PROTECT_NFC << UICR_NFCPINS_PROTECT_Pos)), YESNO(n_reset_enabled)); if (n_reset_enabled) { uint8_t port = (NRF_UICR->PSELRESET[0] & UICR_PSELRESET_PORT_Msk) >> UICR_PSELRESET_PORT_Pos; uint8_t pin = (NRF_UICR->PSELRESET[0] & UICR_PSELRESET_PIN_Msk) >> UICR_PSELRESET_PIN_Pos; - ESP_LOGD(TAG, "nRESET port P%u.%02u", port, pin); + ESP_LOGD(TAG, " nRESET port P%u.%02u", port, pin); } #ifdef USE_BOOTLOADER_MCUBOOT - ESP_LOGD(TAG, "bootloader: mcuboot"); + ESP_LOGD(TAG, " Bootloader: mcuboot"); #else - ESP_LOGD(TAG, "bootloader: Adafruit, version %u.%u.%u", (BOOTLOADER_VERSION_REGISTER >> 16) & 0xFF, + ESP_LOGD(TAG, " Bootloader: Adafruit, version %u.%u.%u", (BOOTLOADER_VERSION_REGISTER >> 16) & 0xFF, (BOOTLOADER_VERSION_REGISTER >> 8) & 0xFF, BOOTLOADER_VERSION_REGISTER & 0xFF); - ESP_LOGD(TAG, - "MBR bootloader addr 0x%08x, UICR bootloader addr 0x%08x\n" - "MBR param page addr 0x%08x, UICR param page addr 0x%08x", - read_mem_u32(MBR_BOOTLOADER_ADDR), NRF_UICR->NRFFW[0], read_mem_u32(MBR_PARAM_PAGE_ADDR), + ESP_LOGD(TAG, " MBR bootloader addr 0x%08x, UICR bootloader addr 0x%08x", read_mem_u32(MBR_BOOTLOADER_ADDR), + NRF_UICR->NRFFW[0]); + ESP_LOGD(TAG, " MBR param page addr 0x%08x, UICR param page addr 0x%08x", read_mem_u32(MBR_PARAM_PAGE_ADDR), NRF_UICR->NRFFW[1]); if (is_sd_present()) { uint32_t const sd_id = sd_id_get(); @@ -326,7 +326,7 @@ size_t DebugComponent::get_device_info_(std::span ver[1] = (sd_version - ver[0] * 1000000) / 1000; ver[2] = (sd_version - ver[0] * 1000000 - ver[1] * 1000); - ESP_LOGD(TAG, "SoftDevice: S%u %u.%u.%u", sd_id, ver[0], ver[1], ver[2]); + ESP_LOGD(TAG, " SoftDevice: S%u %u.%u.%u", sd_id, ver[0], ver[1], ver[2]); #ifdef USE_SOFTDEVICE_ID #ifdef USE_SOFTDEVICE_VERSION if (USE_SOFTDEVICE_ID != sd_id || USE_SOFTDEVICE_VERSION != ver[0]) { @@ -352,10 +352,8 @@ size_t DebugComponent::get_device_info_(std::span } return res; }; - ESP_LOGD(TAG, - "NRFFW %s\n" - "NRFHW %s", - uicr(NRF_UICR->NRFFW, 13).c_str(), uicr(NRF_UICR->NRFHW, 12).c_str()); + ESP_LOGD(TAG, " NRFFW %s", uicr(NRF_UICR->NRFFW, 13).c_str()); + ESP_LOGD(TAG, " NRFHW %s", uicr(NRF_UICR->NRFHW, 12).c_str()); return pos; } diff --git a/esphome/components/dfrobot_sen0395/commands.cpp b/esphome/components/dfrobot_sen0395/commands.cpp index 2c44c6fba9..0f69c82f39 100644 --- a/esphome/components/dfrobot_sen0395/commands.cpp +++ b/esphome/components/dfrobot_sen0395/commands.cpp @@ -187,18 +187,18 @@ uint8_t DetRangeCfgCommand::on_message(std::string &message) { } else if (message == "Done") { ESP_LOGI(TAG, "Updated detection area config:\n" - "Detection area 1 from %.02fm to %.02fm.", + " Detection area 1 from %.02fm to %.02fm.", this->min1_, this->max1_); if (this->min2_ >= 0 && this->max2_ >= 0) { - ESP_LOGI(TAG, "Detection area 2 from %.02fm to %.02fm.", this->min2_, this->max2_); + ESP_LOGI(TAG, " Detection area 2 from %.02fm to %.02fm.", this->min2_, this->max2_); } if (this->min3_ >= 0 && this->max3_ >= 0) { - ESP_LOGI(TAG, "Detection area 3 from %.02fm to %.02fm.", this->min3_, this->max3_); + ESP_LOGI(TAG, " Detection area 3 from %.02fm to %.02fm.", this->min3_, this->max3_); } if (this->min4_ >= 0 && this->max4_ >= 0) { - ESP_LOGI(TAG, "Detection area 4 from %.02fm to %.02fm.", this->min4_, this->max4_); + ESP_LOGI(TAG, " Detection area 4 from %.02fm to %.02fm.", this->min4_, this->max4_); } - ESP_LOGD(TAG, "Used command: %s", this->cmd_.c_str()); + ESP_LOGD(TAG, " Used command: %s", this->cmd_.c_str()); return 1; // Command done } return 0; // Command not done yet. @@ -222,10 +222,10 @@ uint8_t SetLatencyCommand::on_message(std::string &message) { } else if (message == "Done") { ESP_LOGI(TAG, "Updated output latency config:\n" - "Signal that someone was detected is delayed by %.03f s.\n" - "Signal that nobody is detected anymore is delayed by %.03f s.", + " Signal that someone was detected is delayed by %.03f s.\n" + " Signal that nobody is detected anymore is delayed by %.03f s.", this->delay_after_detection_, this->delay_after_disappear_); - ESP_LOGD(TAG, "Used command: %s", this->cmd_.c_str()); + ESP_LOGD(TAG, " Used command: %s", this->cmd_.c_str()); return 1; // Command done } return 0; // Command not done yet diff --git a/esphome/components/emmeti/emmeti.cpp b/esphome/components/emmeti/emmeti.cpp index 5286f962b8..d3e923cbef 100644 --- a/esphome/components/emmeti/emmeti.cpp +++ b/esphome/components/emmeti/emmeti.cpp @@ -153,10 +153,7 @@ void EmmetiClimate::reverse_add_(T val, size_t len, esphome::remote_base::Remote bool EmmetiClimate::check_checksum_(uint8_t checksum) { uint8_t expected = this->gen_checksum_(); - ESP_LOGV(TAG, - "Expected checksum: %X\n" - "Checksum received: %X", - expected, checksum); + ESP_LOGV(TAG, "Expected checksum: %X, Checksum received: %X", expected, checksum); return checksum == expected; } @@ -266,10 +263,7 @@ bool EmmetiClimate::on_receive(remote_base::RemoteReceiveData data) { } } - ESP_LOGD(TAG, - "Swing: %d\n" - "Sleep: %d", - (curr_state.bitmap >> 1) & 0x01, (curr_state.bitmap >> 2) & 0x01); + ESP_LOGD(TAG, "Swing: %d, Sleep: %d", (curr_state.bitmap >> 1) & 0x01, (curr_state.bitmap >> 2) & 0x01); for (size_t pos = 0; pos < 4; pos++) { if (data.expect_item(EMMETI_BIT_MARK, EMMETI_ONE_SPACE)) { @@ -295,13 +289,8 @@ bool EmmetiClimate::on_receive(remote_base::RemoteReceiveData data) { } } - ESP_LOGD(TAG, - "Turbo: %d\n" - "Light: %d\n" - "Tree: %d\n" - "Blow: %d", - (curr_state.bitmap >> 3) & 0x01, (curr_state.bitmap >> 4) & 0x01, (curr_state.bitmap >> 5) & 0x01, - (curr_state.bitmap >> 6) & 0x01); + ESP_LOGD(TAG, "Turbo: %d, Light: %d, Tree: %d, Blow: %d", (curr_state.bitmap >> 3) & 0x01, + (curr_state.bitmap >> 4) & 0x01, (curr_state.bitmap >> 5) & 0x01, (curr_state.bitmap >> 6) & 0x01); uint16_t control_data = 0; for (size_t pos = 0; pos < 11; pos++) { diff --git a/esphome/components/ens160_base/ens160_base.cpp b/esphome/components/ens160_base/ens160_base.cpp index 785b053f04..e1cee5005c 100644 --- a/esphome/components/ens160_base/ens160_base.cpp +++ b/esphome/components/ens160_base/ens160_base.cpp @@ -152,12 +152,13 @@ void ENS160Component::update() { // verbose status logging ESP_LOGV(TAG, - "Status: ENS160 STATAS bit 0x%x\n" - "Status: ENS160 STATER bit 0x%x\n" - "Status: ENS160 VALIDITY FLAG 0x%02x\n" - "Status: ENS160 NEWDAT bit 0x%x\n" - "Status: ENS160 NEWGPR bit 0x%x", - (ENS160_DATA_STATUS_STATAS & (status_value)) == ENS160_DATA_STATUS_STATAS, + "ENS160 Status Register: 0x%02x\n" + " STATAS bit 0x%x\n" + " STATER bit 0x%x\n" + " VALIDITY FLAG 0x%02x\n" + " NEWDAT bit 0x%x\n" + " NEWGPR bit 0x%x", + status_value, (ENS160_DATA_STATUS_STATAS & (status_value)) == ENS160_DATA_STATUS_STATAS, (ENS160_DATA_STATUS_STATER & (status_value)) == ENS160_DATA_STATUS_STATER, (ENS160_DATA_STATUS_VALIDITY & status_value) >> 2, (ENS160_DATA_STATUS_NEWDAT & (status_value)) == ENS160_DATA_STATUS_NEWDAT, diff --git a/esphome/components/es8388/es8388.cpp b/esphome/components/es8388/es8388.cpp index 9deb29416f..72026a2a84 100644 --- a/esphome/components/es8388/es8388.cpp +++ b/esphome/components/es8388/es8388.cpp @@ -209,9 +209,10 @@ bool ES8388::set_dac_output(DacOutputLine line) { }; ESP_LOGV(TAG, - "Setting ES8388_DACPOWER to 0x%02X\n" - "Setting ES8388_DACCONTROL24 / ES8388_DACCONTROL25 to 0x%02X\n" - "Setting ES8388_DACCONTROL26 / ES8388_DACCONTROL27 to 0x%02X", + "DAC output config:\n" + " DACPOWER: 0x%02X\n" + " DACCONTROL24/25: 0x%02X\n" + " DACCONTROL26/27: 0x%02X", dac_power, reg_out1, reg_out2); ES8388_ERROR_CHECK(this->write_byte(ES8388_DACCONTROL24, reg_out1)); // LOUT1VOL diff --git a/esphome/components/esp32_ble_client/ble_client_base.cpp b/esphome/components/esp32_ble_client/ble_client_base.cpp index 3f0eeeab4a..e6a85c784a 100644 --- a/esphome/components/esp32_ble_client/ble_client_base.cpp +++ b/esphome/components/esp32_ble_client/ble_client_base.cpp @@ -423,10 +423,8 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ for (auto &svc : this->services_) { char uuid_buf[espbt::UUID_STR_LEN]; svc->uuid.to_str(uuid_buf); - ESP_LOGV(TAG, - "[%d] [%s] Service UUID: %s\n" - "[%d] [%s] start_handle: 0x%x end_handle: 0x%x", - this->connection_index_, this->address_str_, uuid_buf, this->connection_index_, this->address_str_, + ESP_LOGV(TAG, "[%d] [%s] Service UUID: %s", this->connection_index_, this->address_str_, uuid_buf); + ESP_LOGV(TAG, "[%d] [%s] start_handle: 0x%x end_handle: 0x%x", this->connection_index_, this->address_str_, svc->start_handle, svc->end_handle); } #endif diff --git a/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp b/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp index c8e2e879d4..dcd6e643c2 100644 --- a/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp +++ b/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp @@ -106,11 +106,12 @@ void Esp32HostedUpdate::setup() { esp_app_desc_t *app_desc = (esp_app_desc_t *) (this->firmware_data_ + app_desc_offset); if (app_desc->magic_word == ESP_APP_DESC_MAGIC_WORD) { ESP_LOGD(TAG, - "Firmware version: %s\n" - "Project name: %s\n" - "Build date: %s\n" - "Build time: %s\n" - "IDF version: %s", + "ESP32 Hosted firmware:\n" + " Firmware version: %s\n" + " Project name: %s\n" + " Build date: %s\n" + " Build time: %s\n" + " IDF version: %s", app_desc->version, app_desc->project_name, app_desc->date, app_desc->time, app_desc->idf_ver); this->update_info_.latest_version = app_desc->version; if (this->update_info_.latest_version != this->update_info_.current_version) { diff --git a/esphome/components/espnow/packet_transport/espnow_transport.cpp b/esphome/components/espnow/packet_transport/espnow_transport.cpp index 3d16f28c7d..6e4f606466 100644 --- a/esphome/components/espnow/packet_transport/espnow_transport.cpp +++ b/esphome/components/espnow/packet_transport/espnow_transport.cpp @@ -21,11 +21,9 @@ void ESPNowTransport::setup() { return; } - ESP_LOGI(TAG, - "Registering ESP-NOW handlers\n" - "Peer address: %02X:%02X:%02X:%02X:%02X:%02X", - this->peer_address_[0], this->peer_address_[1], this->peer_address_[2], this->peer_address_[3], - this->peer_address_[4], this->peer_address_[5]); + ESP_LOGI(TAG, "Registering ESP-NOW handlers, peer: %02X:%02X:%02X:%02X:%02X:%02X", this->peer_address_[0], + this->peer_address_[1], this->peer_address_[2], this->peer_address_[3], this->peer_address_[4], + this->peer_address_[5]); // Register received handler this->parent_->register_received_handler(this); diff --git a/esphome/components/ethernet/ethernet_component.cpp b/esphome/components/ethernet/ethernet_component.cpp index f9d98ad51b..fcd32223e4 100644 --- a/esphome/components/ethernet/ethernet_component.cpp +++ b/esphome/components/ethernet/ethernet_component.cpp @@ -866,10 +866,7 @@ void EthernetComponent::write_phy_register_(esp_eth_mac_t *mac, PHYRegister regi } #endif - ESP_LOGD(TAG, - "Writing to PHY Register Address: 0x%02" PRIX32 "\n" - "Writing to PHY Register Value: 0x%04" PRIX32, - register_data.address, register_data.value); + ESP_LOGD(TAG, "Writing PHY reg 0x%02" PRIX32 " = 0x%04" PRIX32, register_data.address, register_data.value); err = mac->write_phy_reg(mac, this->phy_addr_, register_data.address, register_data.value); ESPHL_ERROR_CHECK(err, "Writing PHY Register failed"); diff --git a/esphome/components/ezo_pmp/ezo_pmp.cpp b/esphome/components/ezo_pmp/ezo_pmp.cpp index 9d2f4fc687..bf6e3926b8 100644 --- a/esphome/components/ezo_pmp/ezo_pmp.cpp +++ b/esphome/components/ezo_pmp/ezo_pmp.cpp @@ -150,9 +150,9 @@ void EzoPMP::read_command_result_() { if (current_char == '\0') { ESP_LOGV(TAG, "Read Response from device: %s\n" - "First Component: %s\n" - "Second Component: %s\n" - "Third Component: %s", + " First Component: %s\n" + " Second Component: %s\n" + " Third Component: %s", (char *) response_buffer, (char *) first_parameter_buffer, (char *) second_parameter_buffer, (char *) third_parameter_buffer); diff --git a/esphome/components/gcja5/gcja5.cpp b/esphome/components/gcja5/gcja5.cpp index f7f7f8d02c..43b2fa20d3 100644 --- a/esphome/components/gcja5/gcja5.cpp +++ b/esphome/components/gcja5/gcja5.cpp @@ -97,10 +97,10 @@ void GCJA5Component::parse_data_() { ESP_LOGI(TAG, "GCJA5 Status\n" - "Overall Status : %i\n" - "PD Status : %i\n" - "LD Status : %i\n" - "Fan Status : %i", + " Overall Status : %i\n" + " PD Status : %i\n" + " LD Status : %i\n" + " Fan Status : %i", (status >> 6) & 0x03, (status >> 4) & 0x03, (status >> 2) & 0x03, (status >> 0) & 0x03); } } diff --git a/esphome/components/graphical_display_menu/graphical_display_menu.cpp b/esphome/components/graphical_display_menu/graphical_display_menu.cpp index 2b120a746f..cf1672f217 100644 --- a/esphome/components/graphical_display_menu/graphical_display_menu.cpp +++ b/esphome/components/graphical_display_menu/graphical_display_menu.cpp @@ -38,13 +38,13 @@ void GraphicalDisplayMenu::setup() { void GraphicalDisplayMenu::dump_config() { ESP_LOGCONFIG(TAG, "Graphical Display Menu\n" - "Has Display: %s\n" - "Popup Mode: %s\n" - "Advanced Drawing Mode: %s\n" - "Has Font: %s\n" - "Mode: %s\n" - "Active: %s\n" - "Menu items:", + " Has Display: %s\n" + " Popup Mode: %s\n" + " Advanced Drawing Mode: %s\n" + " Has Font: %s\n" + " Mode: %s\n" + " Active: %s\n" + " Menu items:", YESNO(this->display_ != nullptr), YESNO(this->display_ != nullptr), YESNO(this->display_ == nullptr), YESNO(this->font_ != nullptr), this->mode_ == display_menu_base::MENU_MODE_ROTARY ? "Rotary" : "Joystick", YESNO(this->active_)); diff --git a/esphome/components/honeywellabp/honeywellabp.cpp b/esphome/components/honeywellabp/honeywellabp.cpp index c204325dfc..58c5df230f 100644 --- a/esphome/components/honeywellabp/honeywellabp.cpp +++ b/esphome/components/honeywellabp/honeywellabp.cpp @@ -35,10 +35,7 @@ uint8_t HONEYWELLABPSensor::readsensor_() { pressure_count_ = ((uint16_t) (buf_[0]) << 8 & 0x3F00) | ((uint16_t) (buf_[1]) & 0xFF); // 11 - bit temperature is all of byte 2 (lowest 8 bits) and the first three bits of byte 3 temperature_count_ = (((uint16_t) (buf_[2]) << 3) & 0x7F8) | (((uint16_t) (buf_[3]) >> 5) & 0x7); - ESP_LOGV(TAG, - "Sensor pressure_count_ %d\n" - "Sensor temperature_count_ %d", - pressure_count_, temperature_count_); + ESP_LOGV(TAG, "Sensor pressure_count_ %d, temperature_count_ %d", pressure_count_, temperature_count_); } return status_; } diff --git a/esphome/components/http_request/ota/ota_http_request.cpp b/esphome/components/http_request/ota/ota_http_request.cpp index 882def4d7f..d77a768211 100644 --- a/esphome/components/http_request/ota/ota_http_request.cpp +++ b/esphome/components/http_request/ota/ota_http_request.cpp @@ -105,8 +105,7 @@ uint8_t OtaHttpRequestComponent::do_ota_() { // we will compute MD5 on the fly for verification -- Arduino OTA seems to ignore it md5_receive.init(); - ESP_LOGV(TAG, "MD5Digest initialized\n" - "OTA backend begin"); + ESP_LOGV(TAG, "MD5Digest initialized, OTA backend begin"); auto backend = ota::make_ota_backend(); auto error_code = backend->begin(container->content_length); if (error_code != ota::OTA_RESPONSE_OK) { diff --git a/esphome/components/ina2xx_base/ina2xx_base.cpp b/esphome/components/ina2xx_base/ina2xx_base.cpp index de01c99a19..8a20192c1e 100644 --- a/esphome/components/ina2xx_base/ina2xx_base.cpp +++ b/esphome/components/ina2xx_base/ina2xx_base.cpp @@ -362,10 +362,8 @@ bool INA2XX::configure_shunt_() { ESP_LOGW(TAG, "Shunt value too high"); } this->shunt_cal_ &= 0x7FFF; - ESP_LOGV(TAG, - "Given Rshunt=%f Ohm and Max_current=%.3f\n" - "New CURRENT_LSB=%f, SHUNT_CAL=%u", - this->shunt_resistance_ohm_, this->max_current_a_, this->current_lsb_, this->shunt_cal_); + ESP_LOGV(TAG, "Rshunt=%f Ohm, max current=%.3f A, current LSB=%f, shunt cal=%u", this->shunt_resistance_ohm_, + this->max_current_a_, this->current_lsb_, this->shunt_cal_); return this->write_unsigned_16_(RegisterMap::REG_SHUNT_CAL, this->shunt_cal_); } diff --git a/esphome/components/ld2410/ld2410.cpp b/esphome/components/ld2410/ld2410.cpp index f8f782f804..dd1d53857d 100644 --- a/esphome/components/ld2410/ld2410.cpp +++ b/esphome/components/ld2410/ld2410.cpp @@ -489,11 +489,8 @@ bool LD2410Component::handle_ack_data_() { this->out_pin_level_ = this->buffer_data_[12]; const auto *light_function_str = find_str(LIGHT_FUNCTIONS_BY_UINT, this->light_function_); const auto *out_pin_level_str = find_str(OUT_PIN_LEVELS_BY_UINT, this->out_pin_level_); - ESP_LOGV(TAG, - "Light function: %s\n" - "Light threshold: %u\n" - "Out pin level: %s", - light_function_str, this->light_threshold_, out_pin_level_str); + ESP_LOGV(TAG, "Light function: %s, threshold: %u, out pin level: %s", light_function_str, this->light_threshold_, + out_pin_level_str); #ifdef USE_SELECT if (this->light_function_select_ != nullptr) { this->light_function_select_->publish_state(light_function_str); diff --git a/esphome/components/ld2412/ld2412.cpp b/esphome/components/ld2412/ld2412.cpp index 95e19e0d5f..484d5bd281 100644 --- a/esphome/components/ld2412/ld2412.cpp +++ b/esphome/components/ld2412/ld2412.cpp @@ -530,10 +530,7 @@ bool LD2412Component::handle_ack_data_() { this->light_function_ = this->buffer_data_[10]; this->light_threshold_ = this->buffer_data_[11]; const auto *light_function_str = find_str(LIGHT_FUNCTIONS_BY_UINT, this->light_function_); - ESP_LOGV(TAG, - "Light function: %s\n" - "Light threshold: %u", - light_function_str, this->light_threshold_); + ESP_LOGV(TAG, "Light function: %s, threshold: %u", light_function_str, this->light_threshold_); #ifdef USE_SELECT if (this->light_function_select_ != nullptr) { this->light_function_select_->publish_state(light_function_str); diff --git a/esphome/components/ledc/ledc_output.cpp b/esphome/components/ledc/ledc_output.cpp index a203dde115..a01d42ac8b 100644 --- a/esphome/components/ledc/ledc_output.cpp +++ b/esphome/components/ledc/ledc_output.cpp @@ -130,10 +130,8 @@ void LEDCOutput::setup() { } int hpoint = ledc_angle_to_htop(this->phase_angle_, this->bit_depth_); - ESP_LOGV(TAG, - "Configured frequency %f with a bit depth of %u bits\n" - "Angle of %.1f° results in hpoint %u", - this->frequency_, this->bit_depth_, this->phase_angle_, hpoint); + ESP_LOGV(TAG, "Configured frequency %f with bit depth %u, angle %.1f° hpoint %u", this->frequency_, this->bit_depth_, + this->phase_angle_, hpoint); ledc_channel_config_t chan_conf{}; chan_conf.gpio_num = this->pin_->get_pin(); diff --git a/esphome/components/max6956/max6956.cpp b/esphome/components/max6956/max6956.cpp index 6ba17f11d1..a350e66ee0 100644 --- a/esphome/components/max6956/max6956.cpp +++ b/esphome/components/max6956/max6956.cpp @@ -146,11 +146,11 @@ void MAX6956::dump_config() { if (brightness_mode_ == MAX6956CURRENTMODE::GLOBAL) { ESP_LOGCONFIG(TAG, - "current mode: global\n" - "global brightness: %u", + " Current mode: global\n" + " Brightness: %u", global_brightness_); } else { - ESP_LOGCONFIG(TAG, "current mode: segment"); + ESP_LOGCONFIG(TAG, " Current mode: segment"); } } diff --git a/esphome/components/mqtt/mqtt_backend_esp32.cpp b/esphome/components/mqtt/mqtt_backend_esp32.cpp index c12c79499f..8a7fb965e9 100644 --- a/esphome/components/mqtt/mqtt_backend_esp32.cpp +++ b/esphome/components/mqtt/mqtt_backend_esp32.cpp @@ -165,10 +165,7 @@ void MQTTBackendESP32::mqtt_event_handler_(const Event &event) { case MQTT_EVENT_ERROR: ESP_LOGE(TAG, "MQTT_EVENT_ERROR"); if (event.error_handle.error_type == MQTT_ERROR_TYPE_TCP_TRANSPORT) { - ESP_LOGE(TAG, - "Last error code reported from esp-tls: 0x%x\n" - "Last tls stack error number: 0x%x\n" - "Last captured errno : %d (%s)", + ESP_LOGE(TAG, "Last esp-tls error: 0x%x, tls stack error: 0x%x, socket errno: %d (%s)", event.error_handle.esp_tls_last_esp_err, event.error_handle.esp_tls_stack_err, event.error_handle.esp_transport_sock_errno, strerror(event.error_handle.esp_transport_sock_errno)); } else if (event.error_handle.error_type == MQTT_ERROR_TYPE_CONNECTION_REFUSED) { diff --git a/esphome/components/nextion/nextion_upload_arduino.cpp b/esphome/components/nextion/nextion_upload_arduino.cpp index a433eff883..46a04c1b2e 100644 --- a/esphome/components/nextion/nextion_upload_arduino.cpp +++ b/esphome/components/nextion/nextion_upload_arduino.cpp @@ -25,11 +25,7 @@ int Nextion::upload_by_chunks_(HTTPClient &http_client, uint32_t &range_start) { uint32_t range_end = ((upload_first_chunk_sent_ or this->tft_size_ < 4096) ? this->tft_size_ : 4096) - 1; ESP_LOGD(TAG, "Range start: %" PRIu32, range_start); if (range_size <= 0 or range_end <= range_start) { - ESP_LOGE(TAG, "Invalid range"); - ESP_LOGD(TAG, - "Range end: %" PRIu32 "\n" - "Range size: %" PRIu32, - range_end, range_size); + ESP_LOGE(TAG, "Invalid range end: %" PRIu32 ", size: %" PRIu32, range_end, range_size); return -1; } @@ -138,11 +134,7 @@ int Nextion::upload_by_chunks_(HTTPClient &http_client, uint32_t &range_start) { } bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { - ESP_LOGD(TAG, - "TFT upload requested\n" - "Exit reparse: %s\n" - "URL: %s", - YESNO(exit_reparse), this->tft_url_.c_str()); + ESP_LOGD(TAG, "TFT upload requested, exit reparse: %s, URL: %s", YESNO(exit_reparse), this->tft_url_.c_str()); if (this->connection_state_.is_updating_) { ESP_LOGW(TAG, "Upload in progress"); @@ -172,10 +164,7 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { ESP_LOGD(TAG, "Baud rate: %" PRIu32, baud_rate); // Define the configuration for the HTTP client - ESP_LOGV(TAG, - "Init HTTP client\n" - "Heap: %" PRIu32, - EspClass::getFreeHeap()); + ESP_LOGV(TAG, "Init HTTP client, heap: %" PRIu32, EspClass::getFreeHeap()); HTTPClient http_client; http_client.setTimeout(15000); // Yes 15 seconds.... Helps 8266s along @@ -262,10 +251,7 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { this->reset_(false); delay(250); // NOLINT - ESP_LOGV(TAG, - "Heap: %" PRIu32 "\n" - "Upload cmd: %s", - EspClass::getFreeHeap(), command); + ESP_LOGV(TAG, "Heap: %" PRIu32 ", upload cmd: %s", EspClass::getFreeHeap(), command); this->send_command_(command); if (baud_rate != this->original_baud_rate_) { diff --git a/esphome/components/nextion/nextion_upload_esp32.cpp b/esphome/components/nextion/nextion_upload_esp32.cpp index 46352afd75..43f59a8d4b 100644 --- a/esphome/components/nextion/nextion_upload_esp32.cpp +++ b/esphome/components/nextion/nextion_upload_esp32.cpp @@ -27,11 +27,7 @@ int Nextion::upload_by_chunks_(esp_http_client_handle_t http_client, uint32_t &r uint32_t range_end = ((upload_first_chunk_sent_ or this->tft_size_ < 4096) ? this->tft_size_ : 4096) - 1; ESP_LOGD(TAG, "Range start: %" PRIu32, range_start); if (range_size <= 0 or range_end <= range_start) { - ESP_LOGD(TAG, - "Range end: %" PRIu32 "\n" - "Range size: %" PRIu32, - range_end, range_size); - ESP_LOGE(TAG, "Invalid range"); + ESP_LOGE(TAG, "Invalid range end: %" PRIu32 ", size: %" PRIu32, range_end, range_size); return -1; } @@ -159,11 +155,7 @@ int Nextion::upload_by_chunks_(esp_http_client_handle_t http_client, uint32_t &r } bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { - ESP_LOGD(TAG, - "TFT upload requested\n" - "Exit reparse: %s\n" - "URL: %s", - YESNO(exit_reparse), this->tft_url_.c_str()); + ESP_LOGD(TAG, "TFT upload requested, exit reparse: %s, URL: %s", YESNO(exit_reparse), this->tft_url_.c_str()); if (this->connection_state_.is_updating_) { ESP_LOGW(TAG, "Upload in progress"); @@ -193,10 +185,7 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { ESP_LOGD(TAG, "Baud rate: %" PRIu32, baud_rate); // Define the configuration for the HTTP client - ESP_LOGV(TAG, - "Init HTTP client\n" - "Heap: %" PRIu32, - esp_get_free_heap_size()); + ESP_LOGV(TAG, "Init HTTP client, heap: %" PRIu32, esp_get_free_heap_size()); esp_http_client_config_t config = { .url = this->tft_url_.c_str(), .cert_pem = nullptr, @@ -220,10 +209,7 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { } // Perform the HTTP request - ESP_LOGV(TAG, - "Check connection\n" - "Heap: %" PRIu32, - esp_get_free_heap_size()); + ESP_LOGV(TAG, "Check connection, heap: %" PRIu32, esp_get_free_heap_size()); err = esp_http_client_perform(http_client); if (err != ESP_OK) { ESP_LOGE(TAG, "HTTP failed: %s", esp_err_to_name(err)); @@ -232,10 +218,7 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { } // Check the HTTP Status Code - ESP_LOGV(TAG, - "Check status\n" - "Heap: %" PRIu32, - esp_get_free_heap_size()); + ESP_LOGV(TAG, "Check status, heap: %" PRIu32, esp_get_free_heap_size()); int status_code = esp_http_client_get_status_code(http_client); if (status_code != 200 && status_code != 206) { ESP_LOGE(TAG, "HTTP request failed with status %d", status_code); @@ -344,8 +327,7 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { ESP_LOGV(TAG, "Heap: %" PRIu32 " left: %" PRIu32, esp_get_free_heap_size(), this->content_length_); } - ESP_LOGD(TAG, "TFT upload complete\n" - "Close HTTP"); + ESP_LOGD(TAG, "TFT upload complete, closing HTTP"); esp_http_client_close(http_client); esp_http_client_cleanup(http_client); ESP_LOGV(TAG, "Connection closed"); diff --git a/esphome/components/openthread/openthread_esp.cpp b/esphome/components/openthread/openthread_esp.cpp index bb70701471..9dd68a1ccc 100644 --- a/esphome/components/openthread/openthread_esp.cpp +++ b/esphome/components/openthread/openthread_esp.cpp @@ -130,10 +130,7 @@ void OpenThreadComponent::ot_main() { } #ifdef ESPHOME_LOG_HAS_DEBUG // Fetch link mode from OT only when DEBUG link_mode_config = otThreadGetLinkMode(instance); - ESP_LOGD(TAG, - "Link Mode Device Type: %s\n" - "Link Mode Network Data: %s\n" - "Link Mode RX On When Idle: %s", + ESP_LOGD(TAG, "Link Mode Device Type: %s, Network Data: %s, RX On When Idle: %s", TRUEFALSE(link_mode_config.mDeviceType), TRUEFALSE(link_mode_config.mNetworkData), TRUEFALSE(link_mode_config.mRxOnWhenIdle)); #endif @@ -152,8 +149,7 @@ void OpenThreadComponent::ot_main() { // Make sure the length is 0 so we fallback to the configuration dataset.mLength = 0; } else { - ESP_LOGI(TAG, "Found OpenThread-managed dataset, ignoring esphome configuration\n" - "(set force_dataset: true to override)"); + ESP_LOGI(TAG, "Found existing dataset, ignoring config (force_dataset: true to override)"); } #endif diff --git a/esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp b/esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp index fdff11dedb..9247e114f0 100644 --- a/esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp +++ b/esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp @@ -144,9 +144,10 @@ bool PI4IOE5V6408Component::write_gpio_modes_() { } #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE ESP_LOGV(TAG, - "Wrote GPIO modes: 0b" BYTE_TO_BINARY_PATTERN "\n" - "Wrote GPIO pullup/pulldown: 0b" BYTE_TO_BINARY_PATTERN "\n" - "Wrote GPIO pull enable: 0b" BYTE_TO_BINARY_PATTERN, + "Wrote GPIO config:\n" + " modes: 0b" BYTE_TO_BINARY_PATTERN "\n" + " pullup/pulldown: 0b" BYTE_TO_BINARY_PATTERN "\n" + " pull enable: 0b" BYTE_TO_BINARY_PATTERN, BYTE_TO_BINARY(this->mode_mask_), BYTE_TO_BINARY(this->pull_up_down_mask_), BYTE_TO_BINARY(this->pull_enable_mask_)); #endif diff --git a/esphome/components/pipsolar/pipsolar.cpp b/esphome/components/pipsolar/pipsolar.cpp index e6831ad19e..f95bf4aedb 100644 --- a/esphome/components/pipsolar/pipsolar.cpp +++ b/esphome/components/pipsolar/pipsolar.cpp @@ -748,8 +748,7 @@ esphome::optional Pipsolar::get_bit_(std::string bits, uint8_t bit_pos) { } void Pipsolar::dump_config() { - ESP_LOGCONFIG(TAG, "Pipsolar:\n" - "enabled polling commands:"); + ESP_LOGCONFIG(TAG, "Pipsolar enabled polling commands:"); for (auto &enabled_polling_command : this->enabled_polling_commands_) { if (enabled_polling_command.length != 0) { ESP_LOGCONFIG(TAG, "%s", enabled_polling_command.command); diff --git a/esphome/components/pn532/pn532.cpp b/esphome/components/pn532/pn532.cpp index 5366aab54e..1ab0da3df7 100644 --- a/esphome/components/pn532/pn532.cpp +++ b/esphome/components/pn532/pn532.cpp @@ -31,10 +31,7 @@ void PN532::setup() { this->mark_failed(); return; } - ESP_LOGD(TAG, - "Found chip PN5%02X\n" - "Firmware ver. %d.%d", - version_data[0], version_data[1], version_data[2]); + ESP_LOGD(TAG, "Found chip PN5%02X, Firmware v%d.%d", version_data[0], version_data[1], version_data[2]); if (!this->write_command_({ PN532_COMMAND_SAMCONFIGURATION, diff --git a/esphome/components/pn7150/pn7150.cpp b/esphome/components/pn7150/pn7150.cpp index 7bec1e08a9..8c76c8b88c 100644 --- a/esphome/components/pn7150/pn7150.cpp +++ b/esphome/components/pn7150/pn7150.cpp @@ -243,9 +243,7 @@ uint8_t PN7150::reset_core_(const bool reset_config, const bool power) { return nfc::STATUS_FAILED; } - ESP_LOGD(TAG, - "Configuration %s\n" - "NCI version: %s", + ESP_LOGD(TAG, "Configuration %s, NCI version: %s", rx.get_message()[nfc::NCI_PKT_PAYLOAD_OFFSET + 2] ? "reset" : "retained", rx.get_message()[nfc::NCI_PKT_PAYLOAD_OFFSET + 1] == 0x20 ? "2.0" : "1.0"); @@ -274,11 +272,12 @@ uint8_t PN7150::init_core_() { uint8_t flash_minor_version = rx.get_message()[19 + rx.get_message()[8]]; ESP_LOGD(TAG, - "Manufacturer ID: 0x%02X\n" - "Hardware version: 0x%02X\n" - "ROM code version: 0x%02X\n" - "FLASH major version: 0x%02X\n" - "FLASH minor version: 0x%02X", + "PN7150 chip info:\n" + " Manufacturer ID: 0x%02X\n" + " Hardware version: 0x%02X\n" + " ROM code version: 0x%02X\n" + " FLASH major version: 0x%02X\n" + " FLASH minor version: 0x%02X", manf_id, hw_version, rom_code_version, flash_major_version, flash_minor_version); return rx.get_simple_status_response(); diff --git a/esphome/components/pn7160/pn7160.cpp b/esphome/components/pn7160/pn7160.cpp index 28907b8e30..3fcd1221a7 100644 --- a/esphome/components/pn7160/pn7160.cpp +++ b/esphome/components/pn7160/pn7160.cpp @@ -265,10 +265,7 @@ uint8_t PN7160::reset_core_(const bool reset_config, const bool power) { return nfc::STATUS_FAILED; } - ESP_LOGD(TAG, - "Configuration %s\n" - "NCI version: %s\n" - "Manufacturer ID: 0x%02X", + ESP_LOGD(TAG, "Configuration %s, NCI version: %s, Manufacturer ID: 0x%02X", rx.get_message()[4] ? "reset" : "retained", rx.get_message()[5] == 0x20 ? "2.0" : "1.0", rx.get_message()[6]); rx.get_message().erase(rx.get_message().begin(), rx.get_message().begin() + 8); @@ -301,11 +298,12 @@ uint8_t PN7160::init_core_() { char feat_buf[nfc::FORMAT_BYTES_BUFFER_SIZE]; ESP_LOGD(TAG, - "Hardware version: %u\n" - "ROM code version: %u\n" - "FLASH major version: %u\n" - "FLASH minor version: %u\n" - "Features: %s", + "PN7160 chip info:\n" + " Hardware version: %u\n" + " ROM code version: %u\n" + " FLASH major version: %u\n" + " FLASH minor version: %u\n" + " Features: %s", hw_version, rom_code_version, flash_major_version, flash_minor_version, nfc::format_bytes_to(feat_buf, features)); diff --git a/esphome/components/qmp6988/qmp6988.cpp b/esphome/components/qmp6988/qmp6988.cpp index 4e1ef27d5e..24fe34e785 100644 --- a/esphome/components/qmp6988/qmp6988.cpp +++ b/esphome/components/qmp6988/qmp6988.cpp @@ -128,15 +128,14 @@ bool QMP6988Component::get_calibration_data_() { qmp6988_data_.qmp6988_cali.COE_bp3 = (int16_t) encode_uint16(a_data_uint8_tr[16], a_data_uint8_tr[17]); ESP_LOGV(TAG, - "<-----------calibration data-------------->\n" - "COE_a0[%d] COE_a1[%d] COE_a2[%d] COE_b00[%d]", + "Calibration data:\n" + " COE_a0[%d] COE_a1[%d] COE_a2[%d] COE_b00[%d]\n" + " COE_bt1[%d] COE_bt2[%d] COE_bp1[%d] COE_b11[%d]\n" + " COE_bp2[%d] COE_b12[%d] COE_b21[%d] COE_bp3[%d]", qmp6988_data_.qmp6988_cali.COE_a0, qmp6988_data_.qmp6988_cali.COE_a1, qmp6988_data_.qmp6988_cali.COE_a2, - qmp6988_data_.qmp6988_cali.COE_b00); - ESP_LOGV(TAG, "COE_bt1[%d] COE_bt2[%d] COE_bp1[%d] COE_b11[%d]\r\n", qmp6988_data_.qmp6988_cali.COE_bt1, - qmp6988_data_.qmp6988_cali.COE_bt2, qmp6988_data_.qmp6988_cali.COE_bp1, qmp6988_data_.qmp6988_cali.COE_b11); - ESP_LOGV(TAG, "COE_bp2[%d] COE_b12[%d] COE_b21[%d] COE_bp3[%d]\r\n", qmp6988_data_.qmp6988_cali.COE_bp2, + qmp6988_data_.qmp6988_cali.COE_b00, qmp6988_data_.qmp6988_cali.COE_bt1, qmp6988_data_.qmp6988_cali.COE_bt2, + qmp6988_data_.qmp6988_cali.COE_bp1, qmp6988_data_.qmp6988_cali.COE_b11, qmp6988_data_.qmp6988_cali.COE_bp2, qmp6988_data_.qmp6988_cali.COE_b12, qmp6988_data_.qmp6988_cali.COE_b21, qmp6988_data_.qmp6988_cali.COE_bp3); - ESP_LOGV(TAG, "<-----------calibration data-------------->\r\n"); qmp6988_data_.ik.a0 = qmp6988_data_.qmp6988_cali.COE_a0; // 20Q4 qmp6988_data_.ik.b00 = qmp6988_data_.qmp6988_cali.COE_b00; // 20Q4 @@ -153,14 +152,13 @@ bool QMP6988Component::get_calibration_data_() { qmp6988_data_.ik.b21 = 13836L * (int64_t) qmp6988_data_.qmp6988_cali.COE_b21 + 79333336L; // 29Q60 qmp6988_data_.ik.bp3 = 2915L * (int64_t) qmp6988_data_.qmp6988_cali.COE_bp3 + 157155561L; // 28Q65 ESP_LOGV(TAG, - "<----------- int calibration data -------------->\n" - "a0[%d] a1[%d] a2[%d] b00[%d]", - qmp6988_data_.ik.a0, qmp6988_data_.ik.a1, qmp6988_data_.ik.a2, qmp6988_data_.ik.b00); - ESP_LOGV(TAG, "bt1[%lld] bt2[%lld] bp1[%lld] b11[%lld]\r\n", qmp6988_data_.ik.bt1, qmp6988_data_.ik.bt2, - qmp6988_data_.ik.bp1, qmp6988_data_.ik.b11); - ESP_LOGV(TAG, "bp2[%lld] b12[%lld] b21[%lld] bp3[%lld]\r\n", qmp6988_data_.ik.bp2, qmp6988_data_.ik.b12, + "Int calibration data:\n" + " a0[%d] a1[%d] a2[%d] b00[%d]\n" + " bt1[%lld] bt2[%lld] bp1[%lld] b11[%lld]\n" + " bp2[%lld] b12[%lld] b21[%lld] bp3[%lld]", + qmp6988_data_.ik.a0, qmp6988_data_.ik.a1, qmp6988_data_.ik.a2, qmp6988_data_.ik.b00, qmp6988_data_.ik.bt1, + qmp6988_data_.ik.bt2, qmp6988_data_.ik.bp1, qmp6988_data_.ik.b11, qmp6988_data_.ik.bp2, qmp6988_data_.ik.b12, qmp6988_data_.ik.b21, qmp6988_data_.ik.bp3); - ESP_LOGV(TAG, "<----------- int calibration data -------------->\r\n"); return true; } diff --git a/esphome/components/remote_base/pronto_protocol.cpp b/esphome/components/remote_base/pronto_protocol.cpp index cff3145199..43029cbc2f 100644 --- a/esphome/components/remote_base/pronto_protocol.cpp +++ b/esphome/components/remote_base/pronto_protocol.cpp @@ -104,10 +104,7 @@ void ProntoProtocol::send_pronto_(RemoteTransmitData *dst, const std::vectorlabel); + ESP_LOGW(TAG, "OTA rollback detected! Rolled back from partition '%s'", last_invalid->label); + ESP_LOGW(TAG, "The device reset before the boot was marked successful"); if (esp_reset_reason() == ESP_RST_BROWNOUT) { - ESP_LOGW(TAG, "Last reset was due to brownout - check your power supply!\n" - "See https://esphome.io/guides/faq.html#brownout-detector-was-triggered"); + ESP_LOGW(TAG, "Last reset was due to brownout - check your power supply!"); + ESP_LOGW(TAG, "See https://esphome.io/guides/faq.html#brownout-detector-was-triggered"); } } #endif diff --git a/esphome/components/sim800l/sim800l.cpp b/esphome/components/sim800l/sim800l.cpp index 251e18648b..2115c72cef 100644 --- a/esphome/components/sim800l/sim800l.cpp +++ b/esphome/components/sim800l/sim800l.cpp @@ -326,7 +326,7 @@ void Sim800LComponent::parse_cmd_(std::string message) { if (ok || message.compare(0, 6, "+CMGL:") == 0) { ESP_LOGD(TAG, "Received SMS from: %s\n" - "%s", + " %s", this->sender_.c_str(), this->message_.c_str()); this->sms_received_callback_.call(this->message_, this->sender_); this->state_ = STATE_RECEIVED_SMS; diff --git a/esphome/components/sonoff_d1/sonoff_d1.cpp b/esphome/components/sonoff_d1/sonoff_d1.cpp index 7b99086546..03586b6398 100644 --- a/esphome/components/sonoff_d1/sonoff_d1.cpp +++ b/esphome/components/sonoff_d1/sonoff_d1.cpp @@ -93,10 +93,7 @@ bool SonoffD1Output::read_command_(uint8_t *cmd, size_t &len) { if (this->read_array(cmd, 6)) { #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE char hex_buf[format_hex_pretty_size(6)]; - ESP_LOGV(TAG, - "[%04d] Reading from dimmer:\n" - "[%04d] %s", - this->write_count_, this->write_count_, format_hex_pretty_to(hex_buf, cmd, 6)); + ESP_LOGV(TAG, "[%04d] Reading from dimmer: %s", this->write_count_, format_hex_pretty_to(hex_buf, cmd, 6)); #endif if (cmd[0] != 0xAA || cmd[1] != 0x55) { @@ -190,10 +187,7 @@ bool SonoffD1Output::write_command_(uint8_t *cmd, const size_t len, bool needs_a do { #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE char hex_buf[format_hex_pretty_size(SONOFF_D1_MAX_CMD_SIZE)]; - ESP_LOGV(TAG, - "[%04d] Writing to the dimmer:\n" - "[%04d] %s", - this->write_count_, this->write_count_, format_hex_pretty_to(hex_buf, cmd, len)); + ESP_LOGV(TAG, "[%04d] Writing to the dimmer: %s", this->write_count_, format_hex_pretty_to(hex_buf, cmd, len)); #endif this->write_array(cmd, len); this->write_count_++; diff --git a/esphome/components/sun/sun.cpp b/esphome/components/sun/sun.cpp index e8fc4e44d1..d55a14f192 100644 --- a/esphome/components/sun/sun.cpp +++ b/esphome/components/sun/sun.cpp @@ -174,20 +174,21 @@ struct SunAtTime { // debug output like in example 25.a, p. 165 auto eq = equatorial_coordinate(); ESP_LOGV(TAG, - "jde: %f\n" - "T: %f\n" - "L_0: %f\n" - "M: %f\n" - "e: %f\n" - "C: %f\n" - "Odot: %f\n" - "Omega: %f\n" - "lambda: %f\n" - "epsilon_0: %f\n" - "epsilon: %f\n" - "v: %f\n" - "right_ascension: %f\n" - "declination: %f", + "Sun position:\n" + " jde: %f\n" + " T: %f\n" + " L_0: %f\n" + " M: %f\n" + " e: %f\n" + " C: %f\n" + " Odot: %f\n" + " Omega: %f\n" + " lambda: %f\n" + " epsilon_0: %f\n" + " epsilon: %f\n" + " v: %f\n" + " right_ascension: %f\n" + " declination: %f", jde, t, mean_longitude(), mean_anomaly(), eccentricity(), equation_of_center(), true_longitude(), omega(), apparent_longitude(), mean_obliquity(), true_obliquity(), true_anomaly(), eq.right_ascension, eq.declination); diff --git a/esphome/components/usb_host/usb_host_client.cpp b/esphome/components/usb_host/usb_host_client.cpp index a9be38fb03..422d74095c 100644 --- a/esphome/components/usb_host/usb_host_client.cpp +++ b/esphome/components/usb_host/usb_host_client.cpp @@ -70,14 +70,14 @@ static void usbh_print_intf_desc(const usb_intf_desc_t *intf_desc) { static void usbh_print_cfg_desc(const usb_config_desc_t *cfg_desc) { ESP_LOGV(TAG, "*** Configuration descriptor ***\n" - "bLength %d\n" - "bDescriptorType %d\n" - "wTotalLength %d\n" - "bNumInterfaces %d\n" - "bConfigurationValue %d\n" - "iConfiguration %d\n" - "bmAttributes 0x%x\n" - "bMaxPower %dmA", + " bLength %d\n" + " bDescriptorType %d\n" + " wTotalLength %d\n" + " bNumInterfaces %d\n" + " bConfigurationValue %d\n" + " iConfiguration %d\n" + " bmAttributes 0x%x\n" + " bMaxPower %dmA", cfg_desc->bLength, cfg_desc->bDescriptorType, cfg_desc->wTotalLength, cfg_desc->bNumInterfaces, cfg_desc->bConfigurationValue, cfg_desc->iConfiguration, cfg_desc->bmAttributes, cfg_desc->bMaxPower * 2); } @@ -89,20 +89,20 @@ static void usb_client_print_device_descriptor(const usb_device_desc_t *devc_des ESP_LOGV(TAG, "*** Device descriptor ***\n" - "bLength %d\n" - "bDescriptorType %d\n" - "bcdUSB %d.%d0\n" - "bDeviceClass 0x%x\n" - "bDeviceSubClass 0x%x\n" - "bDeviceProtocol 0x%x\n" - "bMaxPacketSize0 %d\n" - "idVendor 0x%x\n" - "idProduct 0x%x\n" - "bcdDevice %d.%d0\n" - "iManufacturer %d\n" - "iProduct %d\n" - "iSerialNumber %d\n" - "bNumConfigurations %d", + " bLength %d\n" + " bDescriptorType %d\n" + " bcdUSB %d.%d0\n" + " bDeviceClass 0x%x\n" + " bDeviceSubClass 0x%x\n" + " bDeviceProtocol 0x%x\n" + " bMaxPacketSize0 %d\n" + " idVendor 0x%x\n" + " idProduct 0x%x\n" + " bcdDevice %d.%d0\n" + " iManufacturer %d\n" + " iProduct %d\n" + " iSerialNumber %d\n" + " bNumConfigurations %d", devc_desc->bLength, devc_desc->bDescriptorType, ((devc_desc->bcdUSB >> 8) & 0xF), ((devc_desc->bcdUSB >> 4) & 0xF), devc_desc->bDeviceClass, devc_desc->bDeviceSubClass, devc_desc->bDeviceProtocol, devc_desc->bMaxPacketSize0, devc_desc->idVendor, devc_desc->idProduct, diff --git a/esphome/components/usb_uart/cp210x.cpp b/esphome/components/usb_uart/cp210x.cpp index 483286560a..fa8c2331b2 100644 --- a/esphome/components/usb_uart/cp210x.cpp +++ b/esphome/components/usb_uart/cp210x.cpp @@ -58,10 +58,8 @@ std::vector USBUartTypeCP210X::parse_descriptors(usb_device_handle_t dev ESP_LOGE(TAG, "get_active_config_descriptor failed"); return {}; } - ESP_LOGD(TAG, - "bDeviceClass: %u, bDeviceSubClass: %u\n" - "bNumInterfaces: %u", - device_desc->bDeviceClass, device_desc->bDeviceSubClass, config_desc->bNumInterfaces); + ESP_LOGD(TAG, "bDeviceClass: %u, bDeviceSubClass: %u, bNumInterfaces: %u", device_desc->bDeviceClass, + device_desc->bDeviceSubClass, config_desc->bNumInterfaces); if (device_desc->bDeviceClass != 0) { ESP_LOGE(TAG, "bDeviceClass != 0"); return {}; diff --git a/esphome/components/voice_assistant/voice_assistant.cpp b/esphome/components/voice_assistant/voice_assistant.cpp index 641d4d6ff8..d6cbfd4b21 100644 --- a/esphome/components/voice_assistant/voice_assistant.cpp +++ b/esphome/components/voice_assistant/voice_assistant.cpp @@ -434,8 +434,8 @@ void VoiceAssistant::client_subscription(api::APIConnection *client, bool subscr char new_peername[socket::SOCKADDR_STR_LEN]; ESP_LOGE(TAG, "Multiple API Clients attempting to connect to Voice Assistant\n" - "Current client: %s (%s)\n" - "New client: %s (%s)", + " Current client: %s (%s)\n" + " New client: %s (%s)", this->api_client_->get_name(), this->api_client_->get_peername_to(current_peername), client->get_name(), client->get_peername_to(new_peername)); return; diff --git a/esphome/components/wl_134/wl_134.cpp b/esphome/components/wl_134/wl_134.cpp index a589f71c84..a902adfddd 100644 --- a/esphome/components/wl_134/wl_134.cpp +++ b/esphome/components/wl_134/wl_134.cpp @@ -70,12 +70,13 @@ Wl134Component::Rfid134Error Wl134Component::read_packet_() { RFID134_PACKET_CHECKSUM - RFID134_PACKET_RESERVED1); ESP_LOGV(TAG, - "Tag id: %012lld\n" - "Country: %03d\n" - "isData: %s\n" - "isAnimal: %s\n" - "Reserved0: %d\n" - "Reserved1: %" PRId32, + "RFID134 Tag:\n" + " Tag id: %012lld\n" + " Country: %03d\n" + " isData: %s\n" + " isAnimal: %s\n" + " Reserved0: %d\n" + " Reserved1: %" PRId32, reading.id, reading.country, reading.isData ? "true" : "false", reading.isAnimal ? "true" : "false", reading.reserved0, reading.reserved1); diff --git a/esphome/components/xgzp68xx/xgzp68xx.cpp b/esphome/components/xgzp68xx/xgzp68xx.cpp index b5b786c105..5e816469ac 100644 --- a/esphome/components/xgzp68xx/xgzp68xx.cpp +++ b/esphome/components/xgzp68xx/xgzp68xx.cpp @@ -72,10 +72,8 @@ void XGZP68XXComponent::update() { temperature_raw = encode_uint16(data[3], data[4]); // Convert the pressure data to hPa - ESP_LOGV(TAG, - "Got raw pressure=%" PRIu32 ", raw temperature=%u\n" - "K value is %u", - pressure_raw, temperature_raw, this->k_value_); + ESP_LOGV(TAG, "Got raw pressure=%" PRIu32 ", raw temperature=%u, K value=%u", pressure_raw, temperature_raw, + this->k_value_); // Sign extend the pressure float pressure_in_pa = (float) (((int32_t) pressure_raw << 8) >> 8); diff --git a/script/ci-custom.py b/script/ci-custom.py index df819e0f04..85a446ba0d 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -841,6 +841,39 @@ def lint_no_scanf(fname, match): ) +LOG_MULTILINE_RE = re.compile(r"ESP_LOG\w+\s*\(.*?;", re.DOTALL) +LOG_BAD_CONTINUATION_RE = re.compile(r'\\n(?:[^ \\"\r\n\t]|"\s*\n\s*"[^ \\])') +LOG_PERCENT_S_CONTINUATION_RE = re.compile(r'\\n(?:%s|"\s*\n\s*"%s)') + + +@lint_content_check(include=cpp_include) +def lint_log_multiline_continuation(fname, content): + errs = [] + for log_match in LOG_MULTILINE_RE.finditer(content): + log_text = log_match.group(0) + for bad_match in LOG_BAD_CONTINUATION_RE.finditer(log_text): + # %s may expand to a whitespace prefix at runtime, skip those + if LOG_PERCENT_S_CONTINUATION_RE.match(log_text, bad_match.start()): + continue + # Calculate line number from position in full content + abs_pos = log_match.start() + bad_match.start() + lineno = content.count("\n", 0, abs_pos) + 1 + col = abs_pos - content.rfind("\n", 0, abs_pos) + errs.append( + ( + lineno, + col, + "Multi-line log message has a continuation line that does " + "not start with a space. The log viewer uses leading " + "whitespace to detect continuation lines and re-add the " + f"log tag prefix (e.g. {highlight('[C][component:042]:')}).\n" + "Either start the continuation with a space/indent, or " + "split into separate ESP_LOG* calls.", + ) + ) + return errs + + @lint_content_find_check( "ESP_LOG", include=["*.h", "*.tcc"], From 1f945a334a472420d3f3e62c453f3ab3c5246c27 Mon Sep 17 00:00:00 2001 From: Joshua Sing Date: Tue, 24 Feb 2026 04:01:23 +1100 Subject: [PATCH 196/197] [hdc302x] Add new component (#10160) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- CODEOWNERS | 1 + esphome/components/hdc302x/__init__.py | 1 + esphome/components/hdc302x/hdc302x.cpp | 171 ++++++++++++++++++ esphome/components/hdc302x/hdc302x.h | 68 +++++++ esphome/components/hdc302x/sensor.py | 135 ++++++++++++++ tests/components/hdc302x/common.yaml | 19 ++ tests/components/hdc302x/test.esp32-idf.yaml | 4 + .../components/hdc302x/test.esp8266-ard.yaml | 4 + tests/components/hdc302x/test.rp2040-ard.yaml | 4 + 9 files changed, 407 insertions(+) create mode 100644 esphome/components/hdc302x/__init__.py create mode 100644 esphome/components/hdc302x/hdc302x.cpp create mode 100644 esphome/components/hdc302x/hdc302x.h create mode 100644 esphome/components/hdc302x/sensor.py create mode 100644 tests/components/hdc302x/common.yaml create mode 100644 tests/components/hdc302x/test.esp32-idf.yaml create mode 100644 tests/components/hdc302x/test.esp8266-ard.yaml create mode 100644 tests/components/hdc302x/test.rp2040-ard.yaml diff --git a/CODEOWNERS b/CODEOWNERS index 2aa0656343..6728e76bba 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -213,6 +213,7 @@ esphome/components/hbridge/light/* @DotNetDann esphome/components/hbridge/switch/* @dwmw2 esphome/components/hc8/* @omartijn esphome/components/hdc2010/* @optimusprimespace @ssieb +esphome/components/hdc302x/* @joshuasing esphome/components/he60r/* @clydebarrow esphome/components/heatpumpir/* @rob-deutsch esphome/components/hitachi_ac424/* @sourabhjaiswal diff --git a/esphome/components/hdc302x/__init__.py b/esphome/components/hdc302x/__init__.py new file mode 100644 index 0000000000..d8165281bf --- /dev/null +++ b/esphome/components/hdc302x/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@joshuasing"] diff --git a/esphome/components/hdc302x/hdc302x.cpp b/esphome/components/hdc302x/hdc302x.cpp new file mode 100644 index 0000000000..b50d34169a --- /dev/null +++ b/esphome/components/hdc302x/hdc302x.cpp @@ -0,0 +1,171 @@ +#include "hdc302x.h" + +#include "esphome/core/hal.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome::hdc302x { + +static const char *const TAG = "hdc302x.sensor"; + +// Commands (per datasheet Table 7-4) +static const uint8_t HDC302X_CMD_SOFT_RESET[2] = {0x30, 0xa2}; +static const uint8_t HDC302X_CMD_CLEAR_STATUS_REGISTER[2] = {0x30, 0x41}; + +static const uint8_t HDC302X_CMD_TRIGGER_MSB = 0x24; + +static const uint8_t HDC302X_CMD_HEATER_ENABLE[2] = {0x30, 0x6d}; +static const uint8_t HDC302X_CMD_HEATER_DISABLE[2] = {0x30, 0x66}; +static const uint8_t HDC302X_CMD_HEATER_CONFIGURE[2] = {0x30, 0x6e}; + +void HDC302XComponent::setup() { + // Soft reset the device + if (this->write(HDC302X_CMD_SOFT_RESET, 2) != i2c::ERROR_OK) { + this->mark_failed(LOG_STR("Soft reset failed")); + return; + } + // Delay SensorRR (reset ready), per datasheet, 6.5. + delay(3); + + // Clear status register + if (this->write(HDC302X_CMD_CLEAR_STATUS_REGISTER, 2) != i2c::ERROR_OK) { + this->mark_failed(LOG_STR("Clear status failed")); + return; + } +} + +void HDC302XComponent::dump_config() { + ESP_LOGCONFIG(TAG, + "HDC302x:\n" + " Heater: %s", + this->heater_active_ ? "active" : "inactive"); + LOG_I2C_DEVICE(this); + LOG_UPDATE_INTERVAL(this); + LOG_SENSOR(" ", "Temperature", this->temp_sensor_); + LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); +} + +void HDC302XComponent::update() { + uint8_t cmd[] = { + HDC302X_CMD_TRIGGER_MSB, + this->power_mode_, + }; + if (this->write(cmd, 2) != i2c::ERROR_OK) { + this->status_set_warning(LOG_STR(ESP_LOG_MSG_COMM_FAIL)); + return; + } + + // Read data after ADC conversion has completed + this->set_timeout(this->conversion_delay_ms_(), [this]() { this->read_data_(); }); +} + +void HDC302XComponent::start_heater(uint16_t power, uint32_t duration_ms) { + if (!this->disable_heater_()) { + ESP_LOGD(TAG, "Heater disable before start failed"); + } + if (!this->configure_heater_(power) || !this->enable_heater_()) { + ESP_LOGW(TAG, "Heater start failed"); + return; + } + this->heater_active_ = true; + this->cancel_timeout("heater_off"); + if (duration_ms > 0) { + this->set_timeout("heater_off", duration_ms, [this]() { this->stop_heater(); }); + } +} + +void HDC302XComponent::stop_heater() { + this->cancel_timeout("heater_off"); + if (!this->disable_heater_()) { + ESP_LOGW(TAG, "Heater stop failed"); + } + this->heater_active_ = false; +} + +bool HDC302XComponent::enable_heater_() { + if (this->write(HDC302X_CMD_HEATER_ENABLE, 2) != i2c::ERROR_OK) { + ESP_LOGE(TAG, "Enable heater failed"); + return false; + } + return true; +} + +bool HDC302XComponent::configure_heater_(uint16_t power_level) { + if (power_level > 0x3fff) { + ESP_LOGW(TAG, "Heater power 0x%04x exceeds max 0x3fff", power_level); + return false; + } + + // Heater current level config. + uint8_t config[] = { + static_cast((power_level >> 8) & 0xff), // MSB + static_cast(power_level & 0xff) // LSB + }; + + // Configure level of heater current (per datasheet 7.5.7.8). + uint8_t cmd[] = { + HDC302X_CMD_HEATER_CONFIGURE[0], HDC302X_CMD_HEATER_CONFIGURE[1], config[0], config[1], + crc8(config, 2, 0xff, 0x31, true), + }; + if (this->write(cmd, sizeof(cmd)) != i2c::ERROR_OK) { + ESP_LOGE(TAG, "Configure heater failed"); + return false; + } + + return true; +} + +bool HDC302XComponent::disable_heater_() { + if (this->write(HDC302X_CMD_HEATER_DISABLE, 2) != i2c::ERROR_OK) { + ESP_LOGE(TAG, "Disable heater failed"); + return false; + } + return true; +} + +void HDC302XComponent::read_data_() { + uint8_t buf[6]; + if (this->read(buf, 6) != i2c::ERROR_OK) { + this->status_set_warning(LOG_STR(ESP_LOG_MSG_COMM_FAIL)); + return; + } + + // Check checksums + if (crc8(buf, 2, 0xff, 0x31, true) != buf[2] || crc8(buf + 3, 2, 0xff, 0x31, true) != buf[5]) { + this->status_set_warning(LOG_STR("Read data: invalid CRC")); + return; + } + + this->status_clear_warning(); + + if (this->temp_sensor_ != nullptr) { + uint16_t raw_t = encode_uint16(buf[0], buf[1]); + // Calculate temperature in Celsius per datasheet section 7.3.3. + float temp = -45 + 175 * (float(raw_t) / 65535.0f); + this->temp_sensor_->publish_state(temp); + } + + if (this->humidity_sensor_ != nullptr) { + uint16_t raw_rh = encode_uint16(buf[3], buf[4]); + // Calculate RH% per datasheet section 7.3.3. + float humidity = 100 * (float(raw_rh) / 65535.0f); + this->humidity_sensor_->publish_state(humidity); + } +} + +uint32_t HDC302XComponent::conversion_delay_ms_() { + // ADC conversion delay per datasheet, Table 7-5. - Trigger on Demand + switch (this->power_mode_) { + case HDC302XPowerMode::BALANCED: + return 8; + case HDC302XPowerMode::LOW_POWER: + return 5; + case HDC302XPowerMode::ULTRA_LOW_POWER: + return 4; + case HDC302XPowerMode::HIGH_ACCURACY: + default: + return 13; + } +} + +} // namespace esphome::hdc302x diff --git a/esphome/components/hdc302x/hdc302x.h b/esphome/components/hdc302x/hdc302x.h new file mode 100644 index 0000000000..6afea0a8c0 --- /dev/null +++ b/esphome/components/hdc302x/hdc302x.h @@ -0,0 +1,68 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/automation.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome::hdc302x { + +enum HDC302XPowerMode : uint8_t { + HIGH_ACCURACY = 0x00, + BALANCED = 0x0b, + LOW_POWER = 0x16, + ULTRA_LOW_POWER = 0xff, +}; + +/** + HDC302x Temperature and humidity sensor. + + Datasheet: + https://www.ti.com/lit/ds/symlink/hdc3020.pdf + */ +class HDC302XComponent : public PollingComponent, public i2c::I2CDevice { + public: + void setup() override; + void dump_config() override; + void update() override; + + void start_heater(uint16_t power, uint32_t duration_ms); + void stop_heater(); + + void set_temp_sensor(sensor::Sensor *temp_sensor) { this->temp_sensor_ = temp_sensor; } + void set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; } + + void set_power_mode(HDC302XPowerMode power_mode) { this->power_mode_ = power_mode; } + + protected: + sensor::Sensor *temp_sensor_{nullptr}; + sensor::Sensor *humidity_sensor_{nullptr}; + + HDC302XPowerMode power_mode_{HDC302XPowerMode::HIGH_ACCURACY}; + bool heater_active_{false}; + + bool enable_heater_(); + bool configure_heater_(uint16_t power_level); + bool disable_heater_(); + void read_data_(); + uint32_t conversion_delay_ms_(); +}; + +template class HeaterOnAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(uint16_t, power) + TEMPLATABLE_VALUE(uint32_t, duration) + + void play(const Ts &...x) override { + auto power_val = this->power_.value(x...); + auto duration_val = this->duration_.value(x...); + this->parent_->start_heater(power_val, duration_val); + } +}; + +template class HeaterOffAction : public Action, public Parented { + public: + void play(const Ts &...x) override { this->parent_->stop_heater(); } +}; + +} // namespace esphome::hdc302x diff --git a/esphome/components/hdc302x/sensor.py b/esphome/components/hdc302x/sensor.py new file mode 100644 index 0000000000..7215a4cfb7 --- /dev/null +++ b/esphome/components/hdc302x/sensor.py @@ -0,0 +1,135 @@ +from esphome import automation +from esphome.automation import maybe_simple_id +import esphome.codegen as cg +from esphome.components import i2c, sensor +import esphome.config_validation as cv +from esphome.const import ( + CONF_DURATION, + CONF_HUMIDITY, + CONF_ID, + CONF_POWER, + CONF_POWER_MODE, + CONF_TEMPERATURE, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_PERCENT, +) + +DEPENDENCIES = ["i2c"] + +hdc302x_ns = cg.esphome_ns.namespace("hdc302x") +HDC302XComponent = hdc302x_ns.class_( + "HDC302XComponent", cg.PollingComponent, i2c.I2CDevice +) + +HDC302XPowerMode = hdc302x_ns.enum("HDC302XPowerMode") +POWER_MODE_OPTIONS = { + "HIGH_ACCURACY": HDC302XPowerMode.HIGH_ACCURACY, + "BALANCED": HDC302XPowerMode.BALANCED, + "LOW_POWER": HDC302XPowerMode.LOW_POWER, + "ULTRA_LOW_POWER": HDC302XPowerMode.ULTRA_LOW_POWER, +} + +# Actions +HeaterOnAction = hdc302x_ns.class_("HeaterOnAction", automation.Action) +HeaterOffAction = hdc302x_ns.class_("HeaterOffAction", automation.Action) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(HDC302XComponent), + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=2, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_POWER_MODE, default="HIGH_ACCURACY"): cv.enum( + POWER_MODE_OPTIONS, upper=True + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x44)) # Default address per datasheet, Table 7-2. +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) + + if temp_config := config.get(CONF_TEMPERATURE): + sens = await sensor.new_sensor(temp_config) + cg.add(var.set_temp_sensor(sens)) + + if humidity_config := config.get(CONF_HUMIDITY): + sens = await sensor.new_sensor(humidity_config) + cg.add(var.set_humidity_sensor(sens)) + + cg.add(var.set_power_mode(config[CONF_POWER_MODE])) + + +# HDC302x heater power configs, per datasheet Table 7-15. +HDC302X_HEATER_POWER_MAP = { + "QUARTER": 0x009F, + "HALF": 0x03FF, + "FULL": 0x3FFF, +} + + +def heater_power_value(value): + """Accept enum names or raw uint16 values""" + if isinstance(value, cv.Lambda): + return value + if isinstance(value, str): + upper = value.upper() + if upper in HDC302X_HEATER_POWER_MAP: + return HDC302X_HEATER_POWER_MAP[upper] + raise cv.Invalid( + f"Unknown heater power preset: {value}. Use QUARTER, HALF, FULL, or a raw value 0-16383" + ) + return cv.int_range(min=0, max=0x3FFF)(value) + + +HDC302X_ACTION_SCHEMA = maybe_simple_id({cv.GenerateID(): cv.use_id(HDC302XComponent)}) + +HDC302X_HEATER_ON_ACTION_SCHEMA = maybe_simple_id( + { + cv.GenerateID(): cv.use_id(HDC302XComponent), + cv.Optional(CONF_POWER, default="QUARTER"): cv.templatable(heater_power_value), + cv.Optional(CONF_DURATION, default="5s"): cv.templatable( + cv.positive_time_period_milliseconds + ), + } +) + + +@automation.register_action( + "hdc302x.heater_on", HeaterOnAction, HDC302X_HEATER_ON_ACTION_SCHEMA +) +async def hdc302x_heater_on_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + template_ = await cg.templatable(config[CONF_POWER], args, cg.uint16) + cg.add(var.set_power(template_)) + template_ = await cg.templatable(config[CONF_DURATION], args, cg.uint32) + cg.add(var.set_duration(template_)) + return var + + +@automation.register_action( + "hdc302x.heater_off", HeaterOffAction, HDC302X_ACTION_SCHEMA +) +async def hdc302x_heater_off_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var diff --git a/tests/components/hdc302x/common.yaml b/tests/components/hdc302x/common.yaml new file mode 100644 index 0000000000..56bb31effe --- /dev/null +++ b/tests/components/hdc302x/common.yaml @@ -0,0 +1,19 @@ +esphome: + on_boot: + then: + - hdc302x.heater_on: + id: hdc302x_sensor + power: QUARTER + duration: 5s + - hdc302x.heater_off: + id: hdc302x_sensor + +sensor: + - platform: hdc302x + id: hdc302x_sensor + i2c_id: i2c_bus + temperature: + name: Temperature + humidity: + name: Humidity + update_interval: 15s diff --git a/tests/components/hdc302x/test.esp32-idf.yaml b/tests/components/hdc302x/test.esp32-idf.yaml new file mode 100644 index 0000000000..b47e39c389 --- /dev/null +++ b/tests/components/hdc302x/test.esp32-idf.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml + +<<: !include common.yaml diff --git a/tests/components/hdc302x/test.esp8266-ard.yaml b/tests/components/hdc302x/test.esp8266-ard.yaml new file mode 100644 index 0000000000..4a98b9388a --- /dev/null +++ b/tests/components/hdc302x/test.esp8266-ard.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml + +<<: !include common.yaml diff --git a/tests/components/hdc302x/test.rp2040-ard.yaml b/tests/components/hdc302x/test.rp2040-ard.yaml new file mode 100644 index 0000000000..319a7c71a6 --- /dev/null +++ b/tests/components/hdc302x/test.rp2040-ard.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml + +<<: !include common.yaml From e199145f1c3b168e8584c4613f1d814c1694a4f8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Feb 2026 12:20:55 -0600 Subject: [PATCH 197/197] [core] Avoid expensive modulo in LockFreeQueue for non-power-of-2 sizes (#14221) --- esphome/core/lock_free_queue.h | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/esphome/core/lock_free_queue.h b/esphome/core/lock_free_queue.h index e96b739b58..522fbd36e1 100644 --- a/esphome/core/lock_free_queue.h +++ b/esphome/core/lock_free_queue.h @@ -38,13 +38,27 @@ template class LockFreeQueue { } protected: + // Advance ring buffer index by one, wrapping at SIZE. + // Power-of-2 sizes use modulo (compiler emits single mask instruction). + // Non-power-of-2 sizes use comparison to avoid expensive multiply-shift sequences. + static constexpr uint8_t next_index(uint8_t index) { + if constexpr ((SIZE & (SIZE - 1)) == 0) { + return (index + 1) % SIZE; + } else { + uint8_t next = index + 1; + if (next >= SIZE) [[unlikely]] + next = 0; + return next; + } + } + // Internal push that reports queue state - for use by derived classes bool push_internal_(T *element, bool &was_empty, uint8_t &old_tail) { if (element == nullptr) return false; uint8_t current_tail = tail_.load(std::memory_order_relaxed); - uint8_t next_tail = (current_tail + 1) % SIZE; + uint8_t next_tail = next_index(current_tail); // Read head before incrementing tail uint8_t head_before = head_.load(std::memory_order_acquire); @@ -73,14 +87,21 @@ template class LockFreeQueue { } T *element = buffer_[current_head]; - head_.store((current_head + 1) % SIZE, std::memory_order_release); + head_.store(next_index(current_head), std::memory_order_release); return element; } size_t size() const { uint8_t tail = tail_.load(std::memory_order_acquire); uint8_t head = head_.load(std::memory_order_acquire); - return (tail - head + SIZE) % SIZE; + if constexpr ((SIZE & (SIZE - 1)) == 0) { + return (tail - head + SIZE) % SIZE; + } else { + int diff = static_cast(tail) - static_cast(head); + if (diff < 0) + diff += SIZE; + return static_cast(diff); + } } uint16_t get_and_reset_dropped_count() { return dropped_count_.exchange(0, std::memory_order_relaxed); } @@ -90,7 +111,7 @@ template class LockFreeQueue { bool empty() const { return head_.load(std::memory_order_acquire) == tail_.load(std::memory_order_acquire); } bool full() const { - uint8_t next_tail = (tail_.load(std::memory_order_relaxed) + 1) % SIZE; + uint8_t next_tail = next_index(tail_.load(std::memory_order_relaxed)); return next_tail == head_.load(std::memory_order_acquire); }