From 91df0548efc10423b5785d45d6c830178ed3dc56 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Fri, 28 Nov 2025 02:30:03 +1100 Subject: [PATCH 01/57] [wifi] Restore blocking setup until connected for RP2040 (#12142) --- esphome/components/wifi/wifi_component.cpp | 14 ++++++++++++++ esphome/components/wifi/wifi_component.h | 4 ++++ 2 files changed, 18 insertions(+) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index d53de83bd3b..e67493aa4d5 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -1612,6 +1612,20 @@ void WiFiComponent::retry_connect() { } } +#ifdef USE_RP2040 +// 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, ensuring mDNS.begin() is called with an active connection. + +bool WiFiComponent::can_proceed() { + if (!this->has_sta() || this->state_ == WIFI_COMPONENT_STATE_DISABLED || this->ap_setup_) { + return true; + } + return this->is_connected(); +} +#endif + void WiFiComponent::set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeout_ = reboot_timeout; } bool WiFiComponent::is_connected() { return this->state_ == WIFI_COMPONENT_STATE_STA_CONNECTED && diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index b6b956a12d6..a9b03a8b8d7 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -284,6 +284,10 @@ class WiFiComponent : public Component { void retry_connect(); +#ifdef USE_RP2040 + bool can_proceed() override; +#endif + void set_reboot_timeout(uint32_t reboot_timeout); bool is_connected(); From 1fadd1227d531ddb611ec58d9c189f0821cfe796 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 27 Nov 2025 10:50:21 -0600 Subject: [PATCH 02/57] [scheduler] Fix use-after-move crash in heap operations (#12124) --- esphome/core/scheduler.cpp | 26 +++++++++++++------------- esphome/core/scheduler.h | 4 +++- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index 09d50ee7c81..352587bf10b 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -359,8 +359,7 @@ void HOT Scheduler::call(uint32_t now) { std::unique_ptr item; { LockGuard guard{this->lock_}; - item = std::move(this->items_[0]); - this->pop_raw_(); + item = this->pop_raw_locked_(); } const char *name = item->get_name(); @@ -401,7 +400,7 @@ void HOT Scheduler::call(uint32_t now) { // Don't run on failed components if (item->component != nullptr && item->component->is_failed()) { LockGuard guard{this->lock_}; - this->pop_raw_(); + this->recycle_item_(this->pop_raw_locked_()); continue; } @@ -414,7 +413,7 @@ void HOT Scheduler::call(uint32_t now) { { LockGuard guard{this->lock_}; if (is_item_removed_(item.get())) { - this->pop_raw_(); + this->recycle_item_(this->pop_raw_locked_()); this->to_remove_--; continue; } @@ -423,7 +422,7 @@ void HOT Scheduler::call(uint32_t now) { // Single-threaded or multi-threaded with atomics: can check without lock if (is_item_removed_(item.get())) { LockGuard guard{this->lock_}; - this->pop_raw_(); + this->recycle_item_(this->pop_raw_locked_()); this->to_remove_--; continue; } @@ -443,14 +442,14 @@ void HOT Scheduler::call(uint32_t now) { LockGuard guard{this->lock_}; - auto executed_item = std::move(this->items_[0]); // Only pop after function call, this ensures we were reachable // during the function call and know if we were cancelled. - this->pop_raw_(); + auto executed_item = this->pop_raw_locked_(); if (executed_item->remove) { - // We were removed/cancelled in the function call, stop + // We were removed/cancelled in the function call, recycle and continue this->to_remove_--; + this->recycle_item_(std::move(executed_item)); continue; } @@ -497,7 +496,7 @@ size_t HOT Scheduler::cleanup_() { return this->items_.size(); // We must hold the lock for the entire cleanup operation because: - // 1. We're modifying items_ (via pop_raw_) which requires exclusive access + // 1. We're modifying items_ (via pop_raw_locked_) which requires exclusive access // 2. We're decrementing to_remove_ which is also modified by other threads // (though all modifications are already under lock) // 3. Other threads read items_ when searching for items to cancel in cancel_item_locked_() @@ -510,17 +509,18 @@ size_t HOT Scheduler::cleanup_() { if (!item->remove) break; this->to_remove_--; - this->pop_raw_(); + this->recycle_item_(this->pop_raw_locked_()); } return this->items_.size(); } -void HOT Scheduler::pop_raw_() { +std::unique_ptr HOT Scheduler::pop_raw_locked_() { std::pop_heap(this->items_.begin(), this->items_.end(), SchedulerItem::cmp); - // Instead of destroying, recycle the item - this->recycle_item_(std::move(this->items_.back())); + // Move the item out before popping - this is the item that was at the front of the heap + auto item = std::move(this->items_.back()); this->items_.pop_back(); + return item; } // Helper to execute a scheduler item diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index bea1503df0e..08e003c9fbc 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -219,7 +219,9 @@ class Scheduler { // Returns the number of items remaining after cleanup // IMPORTANT: This method should only be called from the main thread (loop task). size_t cleanup_(); - void pop_raw_(); + // Remove and return the front item from the heap + // IMPORTANT: Caller must hold the scheduler lock before calling this function. + std::unique_ptr pop_raw_locked_(); private: // Helper to cancel items by name - must be called with lock held From 9289fc36f79222af346394b096ad629b683234d8 Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Tue, 25 Nov 2025 13:48:32 +0100 Subject: [PATCH 03/57] [nextion] Do not set alternative baud rate when not specified or `<= 0` (#12097) --- esphome/components/nextion/nextion_upload_arduino.cpp | 3 +++ esphome/components/nextion/nextion_upload_idf.cpp | 3 +++ 2 files changed, 6 insertions(+) diff --git a/esphome/components/nextion/nextion_upload_arduino.cpp b/esphome/components/nextion/nextion_upload_arduino.cpp index b4d217d7aac..baea9387293 100644 --- a/esphome/components/nextion/nextion_upload_arduino.cpp +++ b/esphome/components/nextion/nextion_upload_arduino.cpp @@ -174,6 +174,9 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { // Check if baud rate is supported this->original_baud_rate_ = this->parent_->get_baud_rate(); + if (baud_rate <= 0) { + baud_rate = this->original_baud_rate_; + } ESP_LOGD(TAG, "Baud rate: %" PRIu32, baud_rate); // Define the configuration for the HTTP client diff --git a/esphome/components/nextion/nextion_upload_idf.cpp b/esphome/components/nextion/nextion_upload_idf.cpp index 3b0d65643d8..942e3dd6c31 100644 --- a/esphome/components/nextion/nextion_upload_idf.cpp +++ b/esphome/components/nextion/nextion_upload_idf.cpp @@ -177,6 +177,9 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { // Check if baud rate is supported this->original_baud_rate_ = this->parent_->get_baud_rate(); + if (baud_rate <= 0) { + baud_rate = this->original_baud_rate_; + } ESP_LOGD(TAG, "Baud rate: %" PRIu32, baud_rate); // Define the configuration for the HTTP client From acdcd56395fdcad51c652c10b6e289196403ef1b Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 25 Nov 2025 11:14:53 -0500 Subject: [PATCH 04/57] [esp32] Fix platformio flash size print (#12099) Co-authored-by: J. Nick Koston --- esphome/components/esp32/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 59c60293342..d372af3e6ac 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -854,6 +854,10 @@ def _configure_lwip_max_sockets(conf: dict) -> None: async def to_code(config): cg.add_platformio_option("board", config[CONF_BOARD]) cg.add_platformio_option("board_upload.flash_size", config[CONF_FLASH_SIZE]) + cg.add_platformio_option( + "board_upload.maximum_size", + int(config[CONF_FLASH_SIZE].removesuffix("MB")) * 1024 * 1024, + ) cg.set_cpp_standard("gnu++20") cg.add_build_flag("-DUSE_ESP32") cg.add_define("ESPHOME_BOARD", config[CONF_BOARD]) From 278f12fb99fd83eefaf261e2ccab396c0cb38d67 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 25 Nov 2025 10:30:01 -0600 Subject: [PATCH 05/57] [script] Fix script.wait hanging when triggered from on_boot (#12102) --- esphome/components/script/script.h | 7 +- .../fixtures/script_wait_on_boot.yaml | 54 ++++++++ tests/integration/test_script_wait_on_boot.py | 130 ++++++++++++++++++ 3 files changed, 190 insertions(+), 1 deletion(-) create mode 100644 tests/integration/fixtures/script_wait_on_boot.yaml create mode 100644 tests/integration/test_script_wait_on_boot.py diff --git a/esphome/components/script/script.h b/esphome/components/script/script.h index d60ed657f70..3a0823f3ccb 100644 --- a/esphome/components/script/script.h +++ b/esphome/components/script/script.h @@ -278,7 +278,12 @@ template class ScriptWaitAction : public Action, void setup() override { // Start with loop disabled - only enable when there's work to do - this->disable_loop(); + // IMPORTANT: Only disable if num_running_ is 0, otherwise play_complex() was already + // called before our setup() (e.g., from on_boot trigger at same priority level) + // and we must not undo its enable_loop() call + if (this->num_running_ == 0) { + this->disable_loop(); + } } void play_complex(const Ts &...x) override { diff --git a/tests/integration/fixtures/script_wait_on_boot.yaml b/tests/integration/fixtures/script_wait_on_boot.yaml new file mode 100644 index 00000000000..8736b022940 --- /dev/null +++ b/tests/integration/fixtures/script_wait_on_boot.yaml @@ -0,0 +1,54 @@ +esphome: + name: test-script-wait-on-boot + on_boot: + # Use default priority (600.0) which is same as ScriptWaitAction's setup priority + # This tests the race condition where on_boot runs before ScriptWaitAction::setup() + then: + - logger.log: "=== on_boot: Starting boot sequence ===" + - script.execute: show_start_page + - script.wait: show_start_page + - logger.log: "=== on_boot: First script completed, starting second ===" + - script.execute: flip_thru_pages + - script.wait: flip_thru_pages + - logger.log: "=== on_boot: All boot scripts completed successfully ===" + +host: + +api: + actions: + # Manual trigger for additional testing + - action: test_script_wait + then: + - logger.log: "=== Manual test: Starting ===" + - script.execute: show_start_page + - script.wait: show_start_page + - logger.log: "=== Manual test: First script completed ===" + - script.execute: flip_thru_pages + - script.wait: flip_thru_pages + - logger.log: "=== Manual test: All completed ===" + +logger: + level: DEBUG + +script: + # First script - simulates display initialization + - id: show_start_page + mode: single + then: + - logger.log: "show_start_page: Starting" + - delay: 100ms + - logger.log: "show_start_page: After delay 1" + - delay: 100ms + - logger.log: "show_start_page: Completed" + + # Second script - simulates page flip sequence + - id: flip_thru_pages + mode: single + then: + - logger.log: "flip_thru_pages: Starting" + - delay: 50ms + - logger.log: "flip_thru_pages: Page 1" + - delay: 50ms + - logger.log: "flip_thru_pages: Page 2" + - delay: 50ms + - logger.log: "flip_thru_pages: Completed" diff --git a/tests/integration/test_script_wait_on_boot.py b/tests/integration/test_script_wait_on_boot.py new file mode 100644 index 00000000000..478090f7820 --- /dev/null +++ b/tests/integration/test_script_wait_on_boot.py @@ -0,0 +1,130 @@ +"""Integration test for script.wait during on_boot (issue #12043). + +This test verifies that script.wait works correctly when triggered from on_boot. +The issue was that ScriptWaitAction::setup() unconditionally disabled the loop, +even if play_complex() had already been called (from an on_boot trigger at the +same priority level) and enabled it. + +The race condition occurs because: +1. on_boot's default priority is 600.0 (setup_priority::DATA) +2. ScriptWaitAction's default setup priority is also DATA (600.0) +3. When they have the same priority, if on_boot runs first and triggers a script, + ScriptWaitAction::play_complex() enables the loop +4. Then ScriptWaitAction::setup() runs and unconditionally disables the loop +5. The wait never completes because the loop is disabled + +The fix adds a conditional check (like WaitUntilAction has) to only disable the +loop in setup() if num_running_ is 0. +""" + +from __future__ import annotations + +import asyncio +import re + +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_script_wait_on_boot( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test that script.wait works correctly when triggered from on_boot. + + This reproduces issue #12043 where script.wait would hang forever when + triggered from on_boot due to a race condition in ScriptWaitAction::setup(). + """ + test_complete = asyncio.Event() + + # Track progress through the boot sequence + boot_started = False + first_script_started = False + first_script_completed = False + first_wait_returned = False + second_script_started = False + second_script_completed = False + all_completed = False + + # Patterns for boot sequence logs + boot_start_pattern = re.compile(r"on_boot: Starting boot sequence") + show_start_pattern = re.compile(r"show_start_page: Starting") + show_complete_pattern = re.compile(r"show_start_page: Completed") + first_wait_pattern = re.compile(r"on_boot: First script completed") + flip_start_pattern = re.compile(r"flip_thru_pages: Starting") + flip_complete_pattern = re.compile(r"flip_thru_pages: Completed") + all_complete_pattern = re.compile(r"on_boot: All boot scripts completed") + + def check_output(line: str) -> None: + """Check log output for boot sequence progress.""" + nonlocal boot_started, first_script_started, first_script_completed + nonlocal first_wait_returned, second_script_started, second_script_completed + nonlocal all_completed + + if boot_start_pattern.search(line): + boot_started = True + elif show_start_pattern.search(line): + first_script_started = True + elif show_complete_pattern.search(line): + first_script_completed = True + elif first_wait_pattern.search(line): + first_wait_returned = True + elif flip_start_pattern.search(line): + second_script_started = True + elif flip_complete_pattern.search(line): + second_script_completed = True + elif all_complete_pattern.search(line): + all_completed = True + test_complete.set() + + async with ( + run_compiled(yaml_config, line_callback=check_output), + api_client_connected() as client, + ): + # Verify device info + device_info = await client.device_info() + assert device_info is not None + assert device_info.name == "test-script-wait-on-boot" + + # Wait for on_boot sequence to complete + # The boot sequence should complete automatically + # Timeout is generous to allow for delays in the scripts + try: + await asyncio.wait_for(test_complete.wait(), timeout=5.0) + except TimeoutError: + # Build a detailed error message showing where the boot sequence got stuck + progress = [] + if boot_started: + progress.append("boot started") + if first_script_started: + progress.append("show_start_page started") + if first_script_completed: + progress.append("show_start_page completed") + if first_wait_returned: + progress.append("first script.wait returned") + if second_script_started: + progress.append("flip_thru_pages started") + if second_script_completed: + progress.append("flip_thru_pages completed") + + if not first_wait_returned and first_script_completed: + pytest.fail( + f"Test timed out - script.wait hung after show_start_page completed! " + f"This is the issue #12043 bug. Progress: {', '.join(progress)}" + ) + else: + pytest.fail( + f"Test timed out. Progress: {', '.join(progress) if progress else 'none'}" + ) + + # Verify the complete boot sequence executed in order + assert boot_started, "on_boot did not start" + assert first_script_started, "show_start_page did not start" + assert first_script_completed, "show_start_page did not complete" + assert first_wait_returned, "First script.wait did not return" + assert second_script_started, "flip_thru_pages did not start" + assert second_script_completed, "flip_thru_pages did not complete" + assert all_completed, "Boot sequence did not complete" From 46ae6d35a237fecea76eb68a4398ac2d7765a5d5 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Wed, 26 Nov 2025 05:06:42 +1000 Subject: [PATCH 06/57] [lvgl] Allow multiple widgets per grid cell (#12091) --- esphome/components/lvgl/layout.py | 9 ++++++++- tests/components/lvgl/lvgl-package.yaml | 5 +++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/esphome/components/lvgl/layout.py b/esphome/components/lvgl/layout.py index a6aa816fda6..caa503ef0d2 100644 --- a/esphome/components/lvgl/layout.py +++ b/esphome/components/lvgl/layout.py @@ -36,6 +36,8 @@ from .defines import ( ) from .lv_validation import padding, size +CONF_MULTIPLE_WIDGETS_PER_CELL = "multiple_widgets_per_cell" + cell_alignments = LV_CELL_ALIGNMENTS.one_of grid_alignments = LV_GRID_ALIGNMENTS.one_of flex_alignments = LV_FLEX_ALIGNMENTS.one_of @@ -220,6 +222,7 @@ class GridLayout(Layout): cv.Optional(CONF_GRID_ROW_ALIGN): grid_alignments, cv.Optional(CONF_PAD_ROW): padding, cv.Optional(CONF_PAD_COLUMN): padding, + cv.Optional(CONF_MULTIPLE_WIDGETS_PER_CELL, default=False): cv.boolean, }, { cv.Optional(CONF_GRID_CELL_ROW_POS): cv.positive_int, @@ -263,6 +266,7 @@ class GridLayout(Layout): # should be guaranteed to be a dict at this point assert isinstance(layout, dict) assert layout.get(CONF_TYPE).lower() == TYPE_GRID + allow_multiple = layout.get(CONF_MULTIPLE_WIDGETS_PER_CELL, False) rows = len(layout[CONF_GRID_ROWS]) columns = len(layout[CONF_GRID_COLUMNS]) used_cells = [[None] * columns for _ in range(rows)] @@ -299,7 +303,10 @@ class GridLayout(Layout): f"exceeds grid size {rows}x{columns}", [CONF_WIDGETS, index], ) - if used_cells[row + i][column + j] is not None: + if ( + not allow_multiple + and used_cells[row + i][column + j] is not None + ): raise cv.Invalid( f"Cell span {row + i}/{column + j} already occupied by widget at index {used_cells[row + i][column + j]}", [CONF_WIDGETS, index], diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml index cb5b6f59b14..70afd5b3d9d 100644 --- a/tests/components/lvgl/lvgl-package.yaml +++ b/tests/components/lvgl/lvgl-package.yaml @@ -881,6 +881,7 @@ lvgl: grid_columns: [40, fr(1), fr(1)] pad_row: 6px pad_column: 0 + multiple_widgets_per_cell: true widgets: - image: grid_cell_row_pos: 0 @@ -905,6 +906,10 @@ lvgl: grid_cell_row_pos: 1 grid_cell_column_pos: 0 text: "Grid cell 1/0" + - label: + grid_cell_row_pos: 1 + grid_cell_column_pos: 0 + text: "Duplicate for 1/0" - label: styles: bdr_style grid_cell_row_pos: 1 From ae140f52e3dbfab83c516a9b5e7d1997fe7b3149 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Wed, 26 Nov 2025 11:47:27 +1000 Subject: [PATCH 07/57] [lvgl] Fix position of errors in widget config (#12111) Co-authored-by: J. Nick Koston --- esphome/components/lvgl/schemas.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/esphome/components/lvgl/schemas.py b/esphome/components/lvgl/schemas.py index 6b77f66abb5..b2d463c5fdd 100644 --- a/esphome/components/lvgl/schemas.py +++ b/esphome/components/lvgl/schemas.py @@ -1,6 +1,7 @@ from esphome import config_validation as cv from esphome.automation import Trigger, validate_automation from esphome.components.time import RealTimeClock +from esphome.config_validation import prepend_path from esphome.const import ( CONF_ARGS, CONF_FORMAT, @@ -422,7 +423,10 @@ def any_widget_schema(extras=None): def validator(value): if isinstance(value, dict): # Convert to list + is_dict = True value = [{k: v} for k, v in value.items()] + else: + is_dict = False if not isinstance(value, list): raise cv.Invalid("Expected a list of widgets") result = [] @@ -443,7 +447,9 @@ def any_widget_schema(extras=None): ) # Apply custom validation value = widget_type.validate(value or {}) - result.append({key: container_validator(value)}) + path = [key] if is_dict else [index, key] + with prepend_path(path): + result.append({key: container_validator(value)}) return result return validator From 6645994700cd0c9b3bd25e2799bfdc4a2f606fb9 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 26 Nov 2025 20:01:35 -0500 Subject: [PATCH 08/57] [esp32] Fix hosted update when there is no wifi (#12123) --- .../components/esp32_hosted/update/esp32_hosted_update.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp b/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp index adbcc5bf115..6f91d1b3e6c 100644 --- a/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp +++ b/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp @@ -22,6 +22,11 @@ constexpr size_t CHUNK_SIZE = 1500; void Esp32HostedUpdate::setup() { this->update_info_.title = "ESP32 Hosted Coprocessor"; + // if wifi is not present, connect to the coprocessor +#ifndef USE_WIFI + esp_hosted_connect_to_slave(); // NOLINT +#endif + // get coprocessor version esp_hosted_coprocessor_fwver_t ver_info; if (esp_hosted_get_coprocessor_fwversion(&ver_info) == ESP_OK) { From b4b34aee13874eb4d3ce4062fa90ff50c1636cdd Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Fri, 28 Nov 2025 02:30:03 +1100 Subject: [PATCH 09/57] [wifi] Restore blocking setup until connected for RP2040 (#12142) --- esphome/components/wifi/wifi_component.cpp | 14 ++++++++++++++ esphome/components/wifi/wifi_component.h | 4 ++++ 2 files changed, 18 insertions(+) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index e31d7bbf32f..abf62cb0633 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -1555,6 +1555,20 @@ void WiFiComponent::retry_connect() { } } +#ifdef USE_RP2040 +// 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, ensuring mDNS.begin() is called with an active connection. + +bool WiFiComponent::can_proceed() { + if (!this->has_sta() || this->state_ == WIFI_COMPONENT_STATE_DISABLED || this->ap_setup_) { + return true; + } + return this->is_connected(); +} +#endif + void WiFiComponent::set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeout_ = reboot_timeout; } bool WiFiComponent::is_connected() { return this->state_ == WIFI_COMPONENT_STATE_STA_CONNECTED && diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 2e0a9816c65..28eef211d32 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -280,6 +280,10 @@ class WiFiComponent : public Component { void retry_connect(); +#ifdef USE_RP2040 + bool can_proceed() override; +#endif + void set_reboot_timeout(uint32_t reboot_timeout); bool is_connected(); From d5e2543751105d90b31e4e0d6dc9bacd08ca9827 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 27 Nov 2025 10:50:21 -0600 Subject: [PATCH 10/57] [scheduler] Fix use-after-move crash in heap operations (#12124) --- esphome/core/scheduler.cpp | 26 +++++++++++++------------- esphome/core/scheduler.h | 4 +++- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index 09d50ee7c81..352587bf10b 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -359,8 +359,7 @@ void HOT Scheduler::call(uint32_t now) { std::unique_ptr item; { LockGuard guard{this->lock_}; - item = std::move(this->items_[0]); - this->pop_raw_(); + item = this->pop_raw_locked_(); } const char *name = item->get_name(); @@ -401,7 +400,7 @@ void HOT Scheduler::call(uint32_t now) { // Don't run on failed components if (item->component != nullptr && item->component->is_failed()) { LockGuard guard{this->lock_}; - this->pop_raw_(); + this->recycle_item_(this->pop_raw_locked_()); continue; } @@ -414,7 +413,7 @@ void HOT Scheduler::call(uint32_t now) { { LockGuard guard{this->lock_}; if (is_item_removed_(item.get())) { - this->pop_raw_(); + this->recycle_item_(this->pop_raw_locked_()); this->to_remove_--; continue; } @@ -423,7 +422,7 @@ void HOT Scheduler::call(uint32_t now) { // Single-threaded or multi-threaded with atomics: can check without lock if (is_item_removed_(item.get())) { LockGuard guard{this->lock_}; - this->pop_raw_(); + this->recycle_item_(this->pop_raw_locked_()); this->to_remove_--; continue; } @@ -443,14 +442,14 @@ void HOT Scheduler::call(uint32_t now) { LockGuard guard{this->lock_}; - auto executed_item = std::move(this->items_[0]); // Only pop after function call, this ensures we were reachable // during the function call and know if we were cancelled. - this->pop_raw_(); + auto executed_item = this->pop_raw_locked_(); if (executed_item->remove) { - // We were removed/cancelled in the function call, stop + // We were removed/cancelled in the function call, recycle and continue this->to_remove_--; + this->recycle_item_(std::move(executed_item)); continue; } @@ -497,7 +496,7 @@ size_t HOT Scheduler::cleanup_() { return this->items_.size(); // We must hold the lock for the entire cleanup operation because: - // 1. We're modifying items_ (via pop_raw_) which requires exclusive access + // 1. We're modifying items_ (via pop_raw_locked_) which requires exclusive access // 2. We're decrementing to_remove_ which is also modified by other threads // (though all modifications are already under lock) // 3. Other threads read items_ when searching for items to cancel in cancel_item_locked_() @@ -510,17 +509,18 @@ size_t HOT Scheduler::cleanup_() { if (!item->remove) break; this->to_remove_--; - this->pop_raw_(); + this->recycle_item_(this->pop_raw_locked_()); } return this->items_.size(); } -void HOT Scheduler::pop_raw_() { +std::unique_ptr HOT Scheduler::pop_raw_locked_() { std::pop_heap(this->items_.begin(), this->items_.end(), SchedulerItem::cmp); - // Instead of destroying, recycle the item - this->recycle_item_(std::move(this->items_.back())); + // Move the item out before popping - this is the item that was at the front of the heap + auto item = std::move(this->items_.back()); this->items_.pop_back(); + return item; } // Helper to execute a scheduler item diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index bea1503df0e..08e003c9fbc 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -219,7 +219,9 @@ class Scheduler { // Returns the number of items remaining after cleanup // IMPORTANT: This method should only be called from the main thread (loop task). size_t cleanup_(); - void pop_raw_(); + // Remove and return the front item from the heap + // IMPORTANT: Caller must hold the scheduler lock before calling this function. + std::unique_ptr pop_raw_locked_(); private: // Helper to cancel items by name - must be called with lock held From 4115dd7222088bae60d3995c06c876be4a1b5e88 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 27 Nov 2025 17:23:28 -0500 Subject: [PATCH 11/57] Bump version to 2025.11.2 --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index a2b6efcfae0..d30bd842572 100644 --- a/Doxyfile +++ b/Doxyfile @@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = 2025.11.1 +PROJECT_NUMBER = 2025.11.2 # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a diff --git a/esphome/const.py b/esphome/const.py index f4ddd01c09b..45b726e5998 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2025.11.1" +__version__ = "2025.11.2" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 4c549798bc9faca98036748b220d122fe55eccd0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 27 Nov 2025 16:33:08 -0600 Subject: [PATCH 12/57] [usb_uart] Wake main loop immediately when USB data arrives (#12148) --- esphome/components/usb_uart/__init__.py | 7 ++++++- esphome/components/usb_uart/usb_uart.cpp | 6 ++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/esphome/components/usb_uart/__init__.py b/esphome/components/usb_uart/__init__.py index a852e1f78b5..d9bb58ae3ab 100644 --- a/esphome/components/usb_uart/__init__.py +++ b/esphome/components/usb_uart/__init__.py @@ -1,4 +1,5 @@ import esphome.codegen as cg +from esphome.components import socket from esphome.components.uart import ( CONF_DATA_BITS, CONF_PARITY, @@ -17,7 +18,7 @@ from esphome.const import ( ) from esphome.cpp_types import Component -AUTO_LOAD = ["uart", "usb_host", "bytebuffer"] +AUTO_LOAD = ["uart", "usb_host", "bytebuffer", "socket"] CODEOWNERS = ["@clydebarrow"] usb_uart_ns = cg.esphome_ns.namespace("usb_uart") @@ -116,6 +117,10 @@ CONFIG_SCHEMA = cv.ensure_list( async def to_code(config): + # Enable wake_loop_threadsafe for low-latency USB data processing + # The USB task queues data events that need immediate processing + socket.require_wake_loop_threadsafe() + for device in config: var = await register_usb_client(device) for index, channel in enumerate(device[CONF_CHANNELS]): diff --git a/esphome/components/usb_uart/usb_uart.cpp b/esphome/components/usb_uart/usb_uart.cpp index 6720c1e6907..fefccd36451 100644 --- a/esphome/components/usb_uart/usb_uart.cpp +++ b/esphome/components/usb_uart/usb_uart.cpp @@ -2,6 +2,7 @@ #if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32P4) #include "usb_uart.h" #include "esphome/core/log.h" +#include "esphome/core/application.h" #include "esphome/components/uart/uart_debugger.h" #include @@ -262,6 +263,11 @@ void USBUartComponent::start_input(USBUartChannel *channel) { // Push to lock-free queue for main loop processing // Push always succeeds because pool size == queue size this->usb_data_queue_.push(chunk); + + // 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(); +#endif } // On success, restart input immediately from USB task for performance From 71dc402a30190fa91af8d5d55d48f697a121ba64 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 27 Nov 2025 22:00:33 -0600 Subject: [PATCH 13/57] [logger] Replace std::function callbacks with LogListener interface (#12153) --- esphome/components/api/api_server.cpp | 29 ++++++----- esphome/components/api/api_server.h | 14 +++++- esphome/components/ble_nus/ble_nus.cpp | 18 ++++--- esphome/components/ble_nus/ble_nus.h | 13 ++++- esphome/components/logger/logger.cpp | 11 ++--- esphome/components/logger/logger.h | 51 ++++++++++++++------ esphome/components/mqtt/mqtt_client.cpp | 22 +++++---- esphome/components/mqtt/mqtt_client.h | 14 +++++- esphome/components/syslog/esphome_syslog.cpp | 9 ++-- esphome/components/syslog/esphome_syslog.h | 4 +- esphome/components/web_server/web_server.cpp | 17 ++++--- esphome/components/web_server/web_server.h | 16 +++++- 12 files changed, 153 insertions(+), 65 deletions(-) diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 64f8751c35a..de0c4b24c9a 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -101,19 +101,7 @@ void APIServer::setup() { #ifdef USE_LOGGER if (logger::global_logger != nullptr) { - logger::global_logger->add_on_log_callback( - [this](int level, const char *tag, const char *message, size_t message_len) { - if (this->shutting_down_) { - // Don't try to send logs during shutdown - // as it could result in a recursion and - // we would be filling a buffer we are trying to clear - return; - } - for (auto &c : this->clients_) { - if (!c->flags_.remove && c->get_log_subscription_level() >= level) - c->try_send_log_message(level, tag, message, message_len); - } - }); + logger::global_logger->add_log_listener(this); } #endif @@ -541,6 +529,21 @@ bool APIServer::is_connected(bool state_subscription_only) const { return false; } +#ifdef USE_LOGGER +void APIServer::on_log(uint8_t level, const char *tag, const char *message, size_t message_len) { + if (this->shutting_down_) { + // Don't try to send logs during shutdown + // as it could result in a recursion and + // we would be filling a buffer we are trying to clear + return; + } + for (auto &c : this->clients_) { + if (!c->flags_.remove && c->get_log_subscription_level() >= level) + c->try_send_log_message(level, tag, message, message_len); + } +} +#endif + void APIServer::on_shutdown() { this->shutting_down_ = true; diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index 428429418ae..57aea6ad0e6 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -15,6 +15,9 @@ #ifdef USE_API_USER_DEFINED_ACTIONS #include "user_services.h" #endif +#ifdef USE_LOGGER +#include "esphome/components/logger/logger.h" +#endif #include #include @@ -27,7 +30,13 @@ struct SavedNoisePsk { } PACKED; // NOLINT #endif -class APIServer : public Component, public Controller { +class APIServer : public Component, + public Controller +#ifdef USE_LOGGER + , + public logger::LogListener +#endif +{ public: APIServer(); void setup() override; @@ -37,6 +46,9 @@ class APIServer : public Component, public Controller { void dump_config() override; 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; +#endif #ifdef USE_API_PASSWORD bool check_password(const uint8_t *password_data, size_t password_len) const; void set_password(const std::string &password); diff --git a/esphome/components/ble_nus/ble_nus.cpp b/esphome/components/ble_nus/ble_nus.cpp index 9c4d0a39384..bd80592d895 100644 --- a/esphome/components/ble_nus/ble_nus.cpp +++ b/esphome/components/ble_nus/ble_nus.cpp @@ -87,17 +87,21 @@ void BLENUS::setup() { global_ble_nus = this; #ifdef USE_LOGGER if (logger::global_logger != nullptr && this->expose_log_) { - logger::global_logger->add_on_log_callback( - [this](int level, const char *tag, const char *message, size_t message_len) { - this->write_array(reinterpret_cast(message), message_len); - const char c = '\n'; - this->write_array(reinterpret_cast(&c), 1); - }); + logger::global_logger->add_log_listener(this); } - #endif } +#ifdef USE_LOGGER +void BLENUS::on_log(uint8_t level, const char *tag, const char *message, size_t message_len) { + (void) level; + (void) tag; + this->write_array(reinterpret_cast(message), message_len); + const char c = '\n'; + this->write_array(reinterpret_cast(&c), 1); +} +#endif + void BLENUS::dump_config() { ESP_LOGCONFIG(TAG, "ble nus:"); ESP_LOGCONFIG(TAG, " log: %s", YESNO(this->expose_log_)); diff --git a/esphome/components/ble_nus/ble_nus.h b/esphome/components/ble_nus/ble_nus.h index e8cba32b4c4..ef20fc5e5b5 100644 --- a/esphome/components/ble_nus/ble_nus.h +++ b/esphome/components/ble_nus/ble_nus.h @@ -2,12 +2,20 @@ #ifdef USE_ZEPHYR #include "esphome/core/defines.h" #include "esphome/core/component.h" +#ifdef USE_LOGGER +#include "esphome/components/logger/logger.h" +#endif #include #include namespace esphome::ble_nus { -class BLENUS : public Component { +class BLENUS : public Component +#ifdef USE_LOGGER + , + public logger::LogListener +#endif +{ enum TxStatus { TX_DISABLED, TX_ENABLED, @@ -20,6 +28,9 @@ class BLENUS : public Component { void loop() override; 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; +#endif protected: static void send_enabled_callback(bt_nus_send_status status); diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index 9803bf528c7..f925e85e116 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -140,8 +140,9 @@ void Logger::log_vprintf_(uint8_t level, const char *tag, int line, const __Flas uint16_t msg_length = this->tx_buffer_at_ - msg_start; // Don't subtract 1 - tx_buffer_at_ is already at the null terminator position - // Callbacks get message first (before console write) - this->log_callback_.call(level, tag, this->tx_buffer_ + msg_start, msg_length); + // Listeners get message first (before console write) + for (auto *listener : this->log_listeners_) + listener->on_log(level, tag, this->tx_buffer_ + msg_start, msg_length); // Write to console starting at the msg_start this->write_tx_buffer_to_console_(msg_start, &msg_length); @@ -203,7 +204,8 @@ void Logger::process_messages_() { this->write_footer_to_buffer_(this->tx_buffer_, &this->tx_buffer_at_, this->tx_buffer_size_); this->tx_buffer_[this->tx_buffer_at_] = '\0'; size_t msg_len = this->tx_buffer_at_; // We already know the length from tx_buffer_at_ - this->log_callback_.call(message->level, message->tag, this->tx_buffer_, msg_len); + for (auto *listener : this->log_listeners_) + listener->on_log(message->level, message->tag, this->tx_buffer_, msg_len); // At this point all the data we need from message has been transferred to the tx_buffer // so we can release the message to allow other tasks to use it as soon as possible. this->log_buffer_->release_message_main_loop(received_token); @@ -231,9 +233,6 @@ void Logger::set_log_level(const char *tag, uint8_t log_level) { this->log_level UARTSelection Logger::get_uart() const { return this->uart_; } #endif -void Logger::add_on_log_callback(std::function &&callback) { - this->log_callback_.add(std::move(callback)); -} float Logger::get_setup_priority() const { return setup_priority::BUS + 500.0f; } #ifdef USE_STORE_LOG_STR_IN_FLASH diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index 6a8b640331f..a0024411d78 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -36,6 +36,28 @@ struct device; namespace esphome::logger { +/** Interface for receiving log messages without std::function overhead. + * + * Components can implement this interface instead of using lambdas with std::function + * to reduce flash usage from std::function type erasure machinery. + * + * 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 + * } + * }; + */ +class LogListener { + public: + virtual void on_log(uint8_t level, const char *tag, const char *message, size_t message_len) = 0; +}; + #ifdef USE_LOGGER_RUNTIME_TAG_LEVELS // Comparison function for const char* keys in log_levels_ map struct CStrCompare { @@ -168,8 +190,8 @@ class Logger : public Component { inline uint8_t level_for(const char *tag); - /// Register a callback that will be called for every log message sent - void add_on_log_callback(std::function &&callback); + /// Register a log listener to receive log messages + void add_log_listener(LogListener *listener) { this->log_listeners_.push_back(listener); } // add a listener for log level changes void add_listener(std::function &&callback) { this->level_callback_.add(std::move(callback)); } @@ -240,7 +262,7 @@ class Logger : public Component { } } - // Helper to format and send a log message to both console and callbacks + // Helper to format and send a log message to both console and listeners inline void HOT log_message_to_buffer_and_send_(uint8_t level, const char *tag, int line, const char *format, va_list args) { // Format to tx_buffer and prepare for output @@ -248,8 +270,9 @@ class Logger : public Component { this->format_log_to_buffer_with_terminator_(level, tag, line, format, args, this->tx_buffer_, &this->tx_buffer_at_, this->tx_buffer_size_); - // Callbacks get message WITHOUT newline (for API/MQTT/syslog) - this->log_callback_.call(level, tag, this->tx_buffer_, this->tx_buffer_at_); + // Listeners get message WITHOUT newline (for API/MQTT/syslog) + for (auto *listener : this->log_listeners_) + listener->on_log(level, tag, this->tx_buffer_, this->tx_buffer_at_); // Console gets message WITH newline (if platform needs it) this->write_tx_buffer_to_console_(); @@ -301,7 +324,7 @@ class Logger : public Component { #ifdef USE_LOGGER_RUNTIME_TAG_LEVELS std::map log_levels_{}; #endif - CallbackManager log_callback_{}; + std::vector log_listeners_; // Log message listeners (API, MQTT, syslog, etc.) CallbackManager level_callback_{}; #ifdef USE_ESPHOME_TASK_LOG_BUFFER std::unique_ptr log_buffer_; // Will be initialized with init_log_buffer @@ -496,15 +519,15 @@ class Logger : public Component { }; extern Logger *global_logger; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -class LoggerMessageTrigger : public Trigger { +class LoggerMessageTrigger : public Trigger, public LogListener { public: - explicit LoggerMessageTrigger(Logger *parent, uint8_t level) { - this->level_ = level; - parent->add_on_log_callback([this](uint8_t level, const char *tag, const char *message, size_t message_len) { - if (level <= this->level_) { - this->trigger(level, tag, message); - } - }); + 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); + } } protected: diff --git a/esphome/components/mqtt/mqtt_client.cpp b/esphome/components/mqtt/mqtt_client.cpp index a810d98adf9..ba701b90a33 100644 --- a/esphome/components/mqtt/mqtt_client.cpp +++ b/esphome/components/mqtt/mqtt_client.cpp @@ -57,15 +57,7 @@ void MQTTClientComponent::setup() { }); #ifdef USE_LOGGER if (this->is_log_message_enabled() && logger::global_logger != nullptr) { - logger::global_logger->add_on_log_callback( - [this](int level, const char *tag, const char *message, size_t message_len) { - if (level <= this->log_level_ && this->is_connected()) { - this->publish({.topic = this->log_message_.topic, - .payload = std::string(message, message_len), - .qos = this->log_message_.qos, - .retain = this->log_message_.retain}); - } - }); + logger::global_logger->add_log_listener(this); } #endif @@ -148,6 +140,18 @@ void MQTTClientComponent::send_device_info_() { // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) } +#ifdef USE_LOGGER +void MQTTClientComponent::on_log(uint8_t level, const char *tag, const char *message, size_t message_len) { + (void) tag; + if (level <= this->log_level_ && this->is_connected()) { + this->publish({.topic = this->log_message_.topic, + .payload = std::string(message, message_len), + .qos = this->log_message_.qos, + .retain = this->log_message_.retain}); + } +} +#endif + void MQTTClientComponent::dump_config() { ESP_LOGCONFIG(TAG, "MQTT:\n" diff --git a/esphome/components/mqtt/mqtt_client.h b/esphome/components/mqtt/mqtt_client.h index 79383ee857b..8547fe337f0 100644 --- a/esphome/components/mqtt/mqtt_client.h +++ b/esphome/components/mqtt/mqtt_client.h @@ -10,6 +10,9 @@ #include "esphome/core/component.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" +#ifdef USE_LOGGER +#include "esphome/components/logger/logger.h" +#endif #if defined(USE_ESP32) #include "mqtt_backend_esp32.h" #elif defined(USE_ESP8266) @@ -97,7 +100,12 @@ enum MQTTClientState { class MQTTComponent; -class MQTTClientComponent : public Component { +class MQTTClientComponent : public Component +#ifdef USE_LOGGER + , + public logger::LogListener +#endif +{ public: MQTTClientComponent(); @@ -238,6 +246,10 @@ class MQTTClientComponent : public Component { /// MQTT client setup priority 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; +#endif + void on_message(const std::string &topic, const std::string &payload); bool can_proceed() override; diff --git a/esphome/components/syslog/esphome_syslog.cpp b/esphome/components/syslog/esphome_syslog.cpp index 71468fa9324..f5c20c891e7 100644 --- a/esphome/components/syslog/esphome_syslog.cpp +++ b/esphome/components/syslog/esphome_syslog.cpp @@ -19,11 +19,10 @@ constexpr int LOG_LEVEL_TO_SYSLOG_SEVERITY[] = { 7 // VERY_VERBOSE }; -void Syslog::setup() { - logger::global_logger->add_on_log_callback( - [this](int level, const char *tag, const char *message, size_t message_len) { - this->log_(level, tag, message, message_len); - }); +void Syslog::setup() { logger::global_logger->add_log_listener(this); } + +void Syslog::on_log(uint8_t level, const char *tag, const char *message, size_t message_len) { + this->log_(level, tag, message, message_len); } void Syslog::log_(const int level, const char *tag, const char *message, size_t message_len) const { diff --git a/esphome/components/syslog/esphome_syslog.h b/esphome/components/syslog/esphome_syslog.h index e3b2f7dae5b..1010993265b 100644 --- a/esphome/components/syslog/esphome_syslog.h +++ b/esphome/components/syslog/esphome_syslog.h @@ -2,16 +2,18 @@ #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 { namespace syslog { -class Syslog : public Component, public Parented { +class Syslog : public Component, public Parented, public logger::LogListener { 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 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 6bf6524fbc3..f5ca6741610 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -301,12 +301,7 @@ void WebServer::setup() { #ifdef USE_LOGGER if (logger::global_logger != nullptr && this->expose_log_) { - logger::global_logger->add_on_log_callback( - // logs are not deferred, the memory overhead would be too large - [this](int level, const char *tag, const char *message, size_t message_len) { - (void) message_len; - this->events_.try_send_nodefer(message, "log", millis()); - }); + logger::global_logger->add_log_listener(this); } #endif @@ -322,6 +317,16 @@ void WebServer::setup() { this->set_interval(10000, [this]() { this->events_.try_send_nodefer("", "ping", millis(), 30000); }); } void WebServer::loop() { this->events_.loop(); } + +#ifdef USE_LOGGER +void WebServer::on_log(uint8_t level, const char *tag, const char *message, size_t message_len) { + (void) level; + (void) tag; + (void) message_len; + this->events_.try_send_nodefer(message, "log", millis()); +} +#endif + void WebServer::dump_config() { ESP_LOGCONFIG(TAG, "Web Server:\n" diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index 7e1af886457..52cf0bedea4 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -7,6 +7,9 @@ #include "esphome/core/component.h" #include "esphome/core/controller.h" #include "esphome/core/entity_base.h" +#ifdef USE_LOGGER +#include "esphome/components/logger/logger.h" +#endif #include #include @@ -170,7 +173,14 @@ class DeferredUpdateEventSourceList : public std::list Date: Thu, 27 Nov 2025 22:09:27 -0600 Subject: [PATCH 14/57] [light] Replace sparse enum switch with linear search to save 156 bytes RAM (#12140) --- .../components/light/light_json_schema.cpp | 45 +++++++++---------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/esphome/components/light/light_json_schema.cpp b/esphome/components/light/light_json_schema.cpp index 1c9b92f5046..41cb8556305 100644 --- a/esphome/components/light/light_json_schema.cpp +++ b/esphome/components/light/light_json_schema.cpp @@ -7,30 +7,29 @@ namespace esphome::light { // See https://www.home-assistant.io/integrations/light.mqtt/#json-schema for documentation on the schema -// Lookup table for color mode strings -static constexpr const char *get_color_mode_json_str(ColorMode mode) { - switch (mode) { - case ColorMode::ON_OFF: - return "onoff"; - case ColorMode::BRIGHTNESS: - return "brightness"; - case ColorMode::WHITE: - return "white"; // not supported by HA in MQTT - case ColorMode::COLOR_TEMPERATURE: - return "color_temp"; - case ColorMode::COLD_WARM_WHITE: - return "cwww"; // not supported by HA - case ColorMode::RGB: - return "rgb"; - case ColorMode::RGB_WHITE: - return "rgbw"; - case ColorMode::RGB_COLOR_TEMPERATURE: - return "rgbct"; // not supported by HA - case ColorMode::RGB_COLD_WARM_WHITE: - return "rgbww"; - default: - return nullptr; +// Get JSON string for color mode using linear search (avoids large switch jump table) +static const char *get_color_mode_json_str(ColorMode mode) { + // Parallel arrays: mode values and their corresponding strings + // Uses less RAM than a switch jump table on sparse enum values + static constexpr ColorMode MODES[] = { + ColorMode::ON_OFF, + ColorMode::BRIGHTNESS, + ColorMode::WHITE, + ColorMode::COLOR_TEMPERATURE, + ColorMode::COLD_WARM_WHITE, + ColorMode::RGB, + ColorMode::RGB_WHITE, + ColorMode::RGB_COLOR_TEMPERATURE, + ColorMode::RGB_COLD_WARM_WHITE, + }; + static constexpr const char *STRINGS[] = { + "onoff", "brightness", "white", "color_temp", "cwww", "rgb", "rgbw", "rgbct", "rgbww", + }; + for (size_t i = 0; i < sizeof(MODES) / sizeof(MODES[0]); i++) { + if (MODES[i] == mode) + return STRINGS[i]; } + return nullptr; } void LightJSONSchema::dump_json(LightState &state, JsonObject root) { From e1ec6146c0d19d7d4dc021ed2afdd96d8d492518 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 27 Nov 2025 22:09:41 -0600 Subject: [PATCH 15/57] [wifi] Save 112 bytes BSS on ESP8266 by calling SDK directly for BSSID (#12137) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- esphome/components/wifi/wifi_component_esp8266.cpp | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index 540ad3a5859..2d1f979c83b 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -878,10 +878,9 @@ network::IPAddress WiFiComponent::wifi_soft_ap_ip() { bssid_t WiFiComponent::wifi_bssid() { bssid_t bssid{}; - uint8_t *raw_bssid = WiFi.BSSID(); - if (raw_bssid != nullptr) { - for (size_t i = 0; i < bssid.size(); i++) - bssid[i] = raw_bssid[i]; + struct station_config conf {}; + if (wifi_station_get_config(&conf)) { + std::copy_n(conf.bssid, bssid.size(), bssid.begin()); } return bssid; } From 60ffa0e52ef4a83c23052467e53c9d5d3ab20b29 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Nov 2025 11:27:08 -0600 Subject: [PATCH 16/57] [esp32_ble_tracker] Replace scanner state callback with listener interface (#12156) --- .../bluetooth_proxy/bluetooth_proxy.cpp | 12 +++++++----- .../bluetooth_proxy/bluetooth_proxy.h | 7 ++++++- .../esp32_ble_tracker/esp32_ble_tracker.cpp | 4 +++- .../esp32_ble_tracker/esp32_ble_tracker.h | 19 +++++++++++++++---- 4 files changed, 31 insertions(+), 11 deletions(-) diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp index 71f8da75a70..d45377b3f67 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp @@ -27,11 +27,13 @@ void BluetoothProxy::setup() { // Capture the configured scan mode from YAML before any API changes this->configured_scan_active_ = this->parent_->get_scan_active(); - this->parent_->add_scanner_state_callback([this](esp32_ble_tracker::ScannerState state) { - if (this->api_connection_ != nullptr) { - this->send_bluetooth_scanner_state_(state); - } - }); + this->parent_->add_scanner_state_listener(this); +} + +void BluetoothProxy::on_scanner_state(esp32_ble_tracker::ScannerState state) { + if (this->api_connection_ != nullptr) { + this->send_bluetooth_scanner_state_(state); + } } void BluetoothProxy::send_bluetooth_scanner_state_(esp32_ble_tracker::ScannerState state) { diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.h b/esphome/components/bluetooth_proxy/bluetooth_proxy.h index 4363c508ecd..ab9aee2d816 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.h +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.h @@ -52,7 +52,9 @@ enum BluetoothProxySubscriptionFlag : uint32_t { SUBSCRIPTION_RAW_ADVERTISEMENTS = 1 << 0, }; -class BluetoothProxy final : public esp32_ble_tracker::ESPBTDeviceListener, public Component { +class BluetoothProxy final : public esp32_ble_tracker::ESPBTDeviceListener, + public esp32_ble_tracker::BLEScannerStateListener, + public Component { friend class BluetoothConnection; // Allow connection to update connections_free_response_ public: BluetoothProxy(); @@ -108,6 +110,9 @@ class BluetoothProxy final : public esp32_ble_tracker::ESPBTDeviceListener, publ void set_active(bool active) { this->active_ = active; } bool has_active() { return this->active_; } + /// BLEScannerStateListener interface + void on_scanner_state(esp32_ble_tracker::ScannerState state) override; + uint32_t get_legacy_version() const { if (this->active_) { return LEGACY_ACTIVE_CONNECTIONS_VERSION; diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp index 8577f12a927..d3c5edfb946 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -373,7 +373,9 @@ void ESP32BLETracker::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_i void ESP32BLETracker::set_scanner_state_(ScannerState state) { this->scanner_state_ = state; - this->scanner_state_callbacks_.call(state); + for (auto *listener : this->scanner_state_listeners_) { + listener->on_scanner_state(state); + } } #ifdef USE_ESP32_BLE_DEVICE diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h index f80f3e26703..92d13a62ad6 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h @@ -180,6 +180,16 @@ enum class ScannerState { STOPPING, }; +/** Listener interface for BLE scanner state changes. + * + * Components can implement this interface to receive scanner state updates + * without the overhead of std::function callbacks. + */ +class BLEScannerStateListener { + public: + virtual void on_scanner_state(ScannerState state) = 0; +}; + // Helper function to convert ClientState to string const char *client_state_to_string(ClientState state); @@ -264,8 +274,9 @@ class ESP32BLETracker : public Component, void gap_scan_event_handler(const BLEScanResult &scan_result) override; void ble_before_disabled_event_handler() override; - void add_scanner_state_callback(std::function &&callback) { - this->scanner_state_callbacks_.add(std::move(callback)); + /// Add a listener for scanner state changes + void add_scanner_state_listener(BLEScannerStateListener *listener) { + this->scanner_state_listeners_.push_back(listener); } ScannerState get_scanner_state() const { return this->scanner_state_; } @@ -322,14 +333,14 @@ class ESP32BLETracker : public Component, return counts; } - // Group 1: Large objects (12+ bytes) - vectors and callback manager + // Group 1: Large objects (12+ bytes) - vectors #ifdef ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT StaticVector listeners_; #endif #ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT StaticVector clients_; #endif - CallbackManager scanner_state_callbacks_; + std::vector scanner_state_listeners_; #ifdef USE_ESP32_BLE_DEVICE /// Vector of addresses that have already been printed in print_bt_device_info std::vector already_discovered_; From 26e979d3d5a1d80c479ba4729a37875d8580ec64 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Nov 2025 11:27:17 -0600 Subject: [PATCH 17/57] [wifi] Replace std::function callbacks with listener interfaces (#12155) --- esphome/components/wifi/__init__.py | 16 ++-- esphome/components/wifi/wifi_component.h | 73 +++++++++++++------ .../wifi/wifi_component_esp8266.cpp | 28 ++++--- .../wifi/wifi_component_esp_idf.cpp | 30 +++++--- .../wifi/wifi_component_libretiny.cpp | 30 +++++--- .../components/wifi/wifi_component_pico_w.cpp | 24 ++++-- esphome/components/wifi_info/text_sensor.py | 6 +- .../wifi_info/wifi_info_text_sensor.cpp | 46 ++++-------- .../wifi_info/wifi_info_text_sensor.h | 36 +++++---- esphome/core/defines.h | 2 +- 10 files changed, 172 insertions(+), 119 deletions(-) diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index 31d9ca0f708..2c105060110 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -608,7 +608,7 @@ async def wifi_disable_to_code(config, action_id, template_arg, args): KEEP_SCAN_RESULTS_KEY = "wifi_keep_scan_results" RUNTIME_POWER_SAVE_KEY = "wifi_runtime_power_save" -WIFI_CALLBACKS_KEY = "wifi_callbacks" +WIFI_LISTENERS_KEY = "wifi_listeners" def request_wifi_scan_results(): @@ -634,15 +634,15 @@ def enable_runtime_power_save_control(): CORE.data[RUNTIME_POWER_SAVE_KEY] = True -def request_wifi_callbacks() -> None: - """Request that WiFi callbacks be compiled in. +def request_wifi_listeners() -> None: + """Request that WiFi state listeners be compiled in. Components that need to be notified about WiFi state changes (IP address changes, scan results, connection state) should call this function during their code generation. - This enables the add_on_ip_state_callback(), add_on_wifi_scan_state_callback(), - and add_on_wifi_connect_state_callback() APIs. + This enables the add_ip_state_listener(), add_scan_results_listener(), + and add_connect_state_listener() APIs. """ - CORE.data[WIFI_CALLBACKS_KEY] = True + CORE.data[WIFI_LISTENERS_KEY] = True @coroutine_with_priority(CoroPriority.FINAL) @@ -654,8 +654,8 @@ async def final_step(): ) if CORE.data.get(RUNTIME_POWER_SAVE_KEY, False): cg.add_define("USE_WIFI_RUNTIME_POWER_SAVE") - if CORE.data.get(WIFI_CALLBACKS_KEY, False): - cg.add_define("USE_WIFI_CALLBACKS") + if CORE.data.get(WIFI_LISTENERS_KEY, False): + cg.add_define("USE_WIFI_LISTENERS") @automation.register_action( diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index a9b03a8b8d7..97cc3961fe0 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -242,6 +242,37 @@ enum WifiMinAuthMode : uint8_t { struct IDFWiFiEvent; #endif +/** Listener interface for WiFi IP state changes. + * + * Components can implement this interface to receive IP address updates + * without the overhead of std::function callbacks. + */ +class WiFiIPStateListener { + public: + virtual void on_ip_state(const network::IPAddresses &ips, const network::IPAddress &dns1, + const network::IPAddress &dns2) = 0; +}; + +/** Listener interface for WiFi scan results. + * + * Components can implement this interface to receive scan results + * without the overhead of std::function callbacks. + */ +class WiFiScanResultsListener { + public: + virtual void on_wifi_scan_results(const wifi_scan_vector_t &results) = 0; +}; + +/** Listener interface for WiFi connection state changes. + * + * Components can implement this interface to receive connection updates + * without the overhead of std::function callbacks. + */ +class WiFiConnectStateListener { + public: + virtual void on_wifi_connect_state(const std::string &ssid, const bssid_t &bssid) = 0; +}; + /// This component is responsible for managing the ESP WiFi interface. class WiFiComponent : public Component { public: @@ -373,26 +404,22 @@ class WiFiComponent : public Component { int32_t get_wifi_channel(); -#ifdef USE_WIFI_CALLBACKS - /// Add a callback that will be called on configuration changes (IP change, SSID change, etc.) - /// @param callback The callback to be called; template arguments are: - /// - IP addresses - /// - DNS address 1 - /// - DNS address 2 - void add_on_ip_state_callback( - std::function &&callback) { - this->ip_state_callback_.add(std::move(callback)); +#ifdef USE_WIFI_LISTENERS + /** Add a listener for IP state changes. + * Listener receives: IP addresses, DNS address 1, DNS address 2 + */ + void add_ip_state_listener(WiFiIPStateListener *listener) { this->ip_state_listeners_.push_back(listener); } + /// Add a listener for WiFi scan results + void add_scan_results_listener(WiFiScanResultsListener *listener) { + this->scan_results_listeners_.push_back(listener); } - /// - Wi-Fi scan results - void add_on_wifi_scan_state_callback(std::function &)> &&callback) { - this->wifi_scan_state_callback_.add(std::move(callback)); + /** Add a listener for WiFi connection state changes. + * Listener receives: SSID, BSSID + */ + void add_connect_state_listener(WiFiConnectStateListener *listener) { + this->connect_state_listeners_.push_back(listener); } - /// - Wi-Fi SSID - /// - Wi-Fi BSSID - void add_on_wifi_connect_state_callback(std::function &&callback) { - this->wifi_connect_state_callback_.add(std::move(callback)); - } -#endif // USE_WIFI_CALLBACKS +#endif // USE_WIFI_LISTENERS #ifdef USE_WIFI_RUNTIME_POWER_SAVE /** Request high-performance mode (no power saving) for improved WiFi latency. @@ -550,11 +577,11 @@ class WiFiComponent : public Component { WiFiAP ap_; #endif optional output_power_; -#ifdef USE_WIFI_CALLBACKS - CallbackManager ip_state_callback_; - CallbackManager &)> wifi_scan_state_callback_; - CallbackManager wifi_connect_state_callback_; -#endif // USE_WIFI_CALLBACKS +#ifdef USE_WIFI_LISTENERS + std::vector ip_state_listeners_; + std::vector scan_results_listeners_; + std::vector connect_state_listeners_; +#endif // USE_WIFI_LISTENERS ESPPreferenceObject pref_; #ifdef USE_WIFI_FAST_CONNECT ESPPreferenceObject fast_connect_pref_; diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index 2d1f979c83b..701cae5f7cc 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -513,9 +513,10 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { ESP_LOGV(TAG, "Connected ssid='%s' bssid=%s channel=%u", buf, format_mac_address_pretty(it.bssid).c_str(), it.channel); s_sta_connected = true; -#ifdef USE_WIFI_CALLBACKS - global_wifi_component->wifi_connect_state_callback_.call(global_wifi_component->wifi_ssid(), - global_wifi_component->wifi_bssid()); +#ifdef USE_WIFI_LISTENERS + for (auto *listener : global_wifi_component->connect_state_listeners_) { + listener->on_wifi_connect_state(global_wifi_component->wifi_ssid(), global_wifi_component->wifi_bssid()); + } #endif break; } @@ -536,8 +537,10 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { } s_sta_connected = false; s_sta_connecting = false; -#ifdef USE_WIFI_CALLBACKS - global_wifi_component->wifi_connect_state_callback_.call("", bssid_t({0, 0, 0, 0, 0, 0})); +#ifdef USE_WIFI_LISTENERS + for (auto *listener : global_wifi_component->connect_state_listeners_) { + listener->on_wifi_connect_state("", bssid_t({0, 0, 0, 0, 0, 0})); + } #endif break; } @@ -561,10 +564,11 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { ESP_LOGV(TAG, "static_ip=%s gateway=%s netmask=%s", format_ip_addr(it.ip).c_str(), format_ip_addr(it.gw).c_str(), format_ip_addr(it.mask).c_str()); s_sta_got_ip = true; -#ifdef USE_WIFI_CALLBACKS - global_wifi_component->ip_state_callback_.call(global_wifi_component->wifi_sta_ip_addresses(), - global_wifi_component->get_dns_address(0), - global_wifi_component->get_dns_address(1)); +#ifdef USE_WIFI_LISTENERS + for (auto *listener : global_wifi_component->ip_state_listeners_) { + listener->on_ip_state(global_wifi_component->wifi_sta_ip_addresses(), global_wifi_component->get_dns_address(0), + global_wifi_component->get_dns_address(1)); + } #endif break; } @@ -740,8 +744,10 @@ void WiFiComponent::wifi_scan_done_callback_(void *arg, STATUS status) { it->is_hidden != 0); } this->scan_done_ = true; -#ifdef USE_WIFI_CALLBACKS - global_wifi_component->wifi_scan_state_callback_.call(global_wifi_component->scan_result_); +#ifdef USE_WIFI_LISTENERS + for (auto *listener : global_wifi_component->scan_results_listeners_) { + listener->on_wifi_scan_results(global_wifi_component->scan_result_); + } #endif } diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index c20c96ced0d..3d25d2890f8 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -727,8 +727,10 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { ESP_LOGV(TAG, "Connected ssid='%s' bssid=" LOG_SECRET("%s") " channel=%u, authmode=%s", buf, format_mac_address_pretty(it.bssid).c_str(), it.channel, get_auth_mode_str(it.authmode)); s_sta_connected = true; -#ifdef USE_WIFI_CALLBACKS - this->wifi_connect_state_callback_.call(this->wifi_ssid(), this->wifi_bssid()); +#ifdef USE_WIFI_LISTENERS + for (auto *listener : this->connect_state_listeners_) { + listener->on_wifi_connect_state(this->wifi_ssid(), this->wifi_bssid()); + } #endif } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_STA_DISCONNECTED) { @@ -753,8 +755,10 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { s_sta_connected = false; s_sta_connecting = false; error_from_callback_ = true; -#ifdef USE_WIFI_CALLBACKS - this->wifi_connect_state_callback_.call("", bssid_t({0, 0, 0, 0, 0, 0})); +#ifdef USE_WIFI_LISTENERS + for (auto *listener : this->connect_state_listeners_) { + listener->on_wifi_connect_state("", bssid_t({0, 0, 0, 0, 0, 0})); + } #endif } else if (data->event_base == IP_EVENT && data->event_id == IP_EVENT_STA_GOT_IP) { @@ -764,8 +768,10 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { #endif /* USE_NETWORK_IPV6 */ ESP_LOGV(TAG, "static_ip=" IPSTR " gateway=" IPSTR, IP2STR(&it.ip_info.ip), IP2STR(&it.ip_info.gw)); this->got_ipv4_address_ = true; -#ifdef USE_WIFI_CALLBACKS - this->ip_state_callback_.call(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1)); +#ifdef USE_WIFI_LISTENERS + for (auto *listener : this->ip_state_listeners_) { + listener->on_ip_state(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1)); + } #endif #if USE_NETWORK_IPV6 @@ -773,8 +779,10 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { const auto &it = data->data.ip_got_ip6; ESP_LOGV(TAG, "IPv6 address=" IPV6STR, IPV62STR(it.ip6_info.ip)); this->num_ipv6_addresses_++; -#ifdef USE_WIFI_CALLBACKS - this->ip_state_callback_.call(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1)); +#ifdef USE_WIFI_LISTENERS + for (auto *listener : this->ip_state_listeners_) { + listener->on_ip_state(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1)); + } #endif #endif /* USE_NETWORK_IPV6 */ @@ -815,8 +823,10 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { scan_result_.emplace_back(bssid, ssid, record.primary, record.rssi, record.authmode != WIFI_AUTH_OPEN, ssid.empty()); } -#ifdef USE_WIFI_CALLBACKS - this->wifi_scan_state_callback_.call(this->scan_result_); +#ifdef USE_WIFI_LISTENERS + for (auto *listener : this->scan_results_listeners_) { + listener->on_wifi_scan_results(this->scan_result_); + } #endif } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_AP_START) { diff --git a/esphome/components/wifi/wifi_component_libretiny.cpp b/esphome/components/wifi/wifi_component_libretiny.cpp index 04d0d4fa852..f1405d3bef5 100644 --- a/esphome/components/wifi/wifi_component_libretiny.cpp +++ b/esphome/components/wifi/wifi_component_libretiny.cpp @@ -287,8 +287,10 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ buf[it.ssid_len] = '\0'; ESP_LOGV(TAG, "Connected ssid='%s' bssid=" LOG_SECRET("%s") " channel=%u, authmode=%s", buf, format_mac_address_pretty(it.bssid).c_str(), it.channel, get_auth_mode_str(it.authmode)); -#ifdef USE_WIFI_CALLBACKS - this->wifi_connect_state_callback_.call(this->wifi_ssid(), this->wifi_bssid()); +#ifdef USE_WIFI_LISTENERS + for (auto *listener : this->connect_state_listeners_) { + listener->on_wifi_connect_state(this->wifi_ssid(), this->wifi_bssid()); + } #endif break; } @@ -315,8 +317,10 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ } s_sta_connecting = false; -#ifdef USE_WIFI_CALLBACKS - this->wifi_connect_state_callback_.call("", bssid_t({0, 0, 0, 0, 0, 0})); +#ifdef USE_WIFI_LISTENERS + for (auto *listener : this->connect_state_listeners_) { + listener->on_wifi_connect_state("", bssid_t({0, 0, 0, 0, 0, 0})); + } #endif break; } @@ -339,16 +343,20 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ ESP_LOGV(TAG, "static_ip=%s gateway=%s", format_ip4_addr(WiFi.localIP()).c_str(), format_ip4_addr(WiFi.gatewayIP()).c_str()); s_sta_connecting = false; -#ifdef USE_WIFI_CALLBACKS - this->ip_state_callback_.call(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1)); +#ifdef USE_WIFI_LISTENERS + for (auto *listener : this->ip_state_listeners_) { + listener->on_ip_state(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1)); + } #endif break; } case ESPHOME_EVENT_ID_WIFI_STA_GOT_IP6: { // auto it = info.got_ip.ip_info; ESP_LOGV(TAG, "Got IPv6"); -#ifdef USE_WIFI_CALLBACKS - this->ip_state_callback_.call(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1)); +#ifdef USE_WIFI_LISTENERS + for (auto *listener : this->ip_state_listeners_) { + listener->on_ip_state(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1)); + } #endif break; } @@ -443,8 +451,10 @@ void WiFiComponent::wifi_scan_done_callback_() { } WiFi.scanDelete(); this->scan_done_ = true; -#ifdef USE_WIFI_CALLBACKS - this->wifi_scan_state_callback_.call(this->scan_result_); +#ifdef USE_WIFI_LISTENERS + for (auto *listener : this->scan_results_listeners_) { + listener->on_wifi_scan_results(this->scan_result_); + } #endif } diff --git a/esphome/components/wifi/wifi_component_pico_w.cpp b/esphome/components/wifi/wifi_component_pico_w.cpp index 326883c0c46..1a8b75213ca 100644 --- a/esphome/components/wifi/wifi_component_pico_w.cpp +++ b/esphome/components/wifi/wifi_component_pico_w.cpp @@ -225,8 +225,10 @@ void WiFiComponent::wifi_loop_() { if (this->state_ == WIFI_COMPONENT_STATE_STA_SCANNING && !cyw43_wifi_scan_active(&cyw43_state)) { this->scan_done_ = true; ESP_LOGV(TAG, "Scan done"); -#ifdef USE_WIFI_CALLBACKS - this->wifi_scan_state_callback_.call(this->scan_result_); +#ifdef USE_WIFI_LISTENERS + for (auto *listener : this->scan_results_listeners_) { + listener->on_wifi_scan_results(this->scan_result_); + } #endif } @@ -241,16 +243,20 @@ void WiFiComponent::wifi_loop_() { // Just connected s_sta_was_connected = true; ESP_LOGV(TAG, "Connected"); -#ifdef USE_WIFI_CALLBACKS - this->wifi_connect_state_callback_.call(this->wifi_ssid(), this->wifi_bssid()); +#ifdef USE_WIFI_LISTENERS + for (auto *listener : this->connect_state_listeners_) { + listener->on_wifi_connect_state(this->wifi_ssid(), this->wifi_bssid()); + } #endif } else if (!is_connected && s_sta_was_connected) { // Just disconnected s_sta_was_connected = false; s_sta_had_ip = false; ESP_LOGV(TAG, "Disconnected"); -#ifdef USE_WIFI_CALLBACKS - this->wifi_connect_state_callback_.call("", bssid_t({0, 0, 0, 0, 0, 0})); +#ifdef USE_WIFI_LISTENERS + for (auto *listener : this->connect_state_listeners_) { + listener->on_wifi_connect_state("", bssid_t({0, 0, 0, 0, 0, 0})); + } #endif } @@ -267,8 +273,10 @@ void WiFiComponent::wifi_loop_() { // Just got IP address s_sta_had_ip = true; ESP_LOGV(TAG, "Got IP address"); -#ifdef USE_WIFI_CALLBACKS - this->ip_state_callback_.call(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1)); +#ifdef USE_WIFI_LISTENERS + for (auto *listener : this->ip_state_listeners_) { + listener->on_ip_state(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1)); + } #endif } } diff --git a/esphome/components/wifi_info/text_sensor.py b/esphome/components/wifi_info/text_sensor.py index 0feee3d4a98..bc0c038f804 100644 --- a/esphome/components/wifi_info/text_sensor.py +++ b/esphome/components/wifi_info/text_sensor.py @@ -61,7 +61,7 @@ CONFIG_SCHEMA = cv.Schema( } ) -# Keys that require WiFi callbacks +# Keys that require WiFi listeners _NETWORK_INFO_KEYS = { CONF_SSID, CONF_BSSID, @@ -79,9 +79,9 @@ async def setup_conf(config, key): async def to_code(config): - # Request WiFi callbacks for any sensor that needs them + # Request WiFi listeners for any sensor that needs them if _NETWORK_INFO_KEYS.intersection(config): - wifi.request_wifi_callbacks() + wifi.request_wifi_listeners() await setup_conf(config, CONF_SSID) await setup_conf(config, CONF_BSSID) diff --git a/esphome/components/wifi_info/wifi_info_text_sensor.cpp b/esphome/components/wifi_info/wifi_info_text_sensor.cpp index abd590b168c..92d3ea29f59 100644 --- a/esphome/components/wifi_info/wifi_info_text_sensor.cpp +++ b/esphome/components/wifi_info/wifi_info_text_sensor.cpp @@ -12,16 +12,12 @@ static constexpr size_t MAX_STATE_LENGTH = 255; * IPAddressWiFiInfo *******************/ -void IPAddressWiFiInfo::setup() { - wifi::global_wifi_component->add_on_ip_state_callback( - [this](const network::IPAddresses &ips, const network::IPAddress &dns1_ip, const network::IPAddress &dns2_ip) { - this->state_callback_(ips); - }); -} +void IPAddressWiFiInfo::setup() { wifi::global_wifi_component->add_ip_state_listener(this); } void IPAddressWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "IP Address", this); } -void IPAddressWiFiInfo::state_callback_(const network::IPAddresses &ips) { +void IPAddressWiFiInfo::on_ip_state(const network::IPAddresses &ips, const network::IPAddress &dns1, + const network::IPAddress &dns2) { this->publish_state(ips[0].str()); uint8_t sensor = 0; for (const auto &ip : ips) { @@ -38,17 +34,13 @@ void IPAddressWiFiInfo::state_callback_(const network::IPAddresses &ips) { * DNSAddressWifiInfo ********************/ -void DNSAddressWifiInfo::setup() { - wifi::global_wifi_component->add_on_ip_state_callback( - [this](const network::IPAddresses &ips, const network::IPAddress &dns1_ip, const network::IPAddress &dns2_ip) { - this->state_callback_(dns1_ip, dns2_ip); - }); -} +void DNSAddressWifiInfo::setup() { wifi::global_wifi_component->add_ip_state_listener(this); } void DNSAddressWifiInfo::dump_config() { LOG_TEXT_SENSOR("", "DNS Address", this); } -void DNSAddressWifiInfo::state_callback_(const network::IPAddress &dns1_ip, const network::IPAddress &dns2_ip) { - std::string dns_results = dns1_ip.str() + " " + dns2_ip.str(); +void DNSAddressWifiInfo::on_ip_state(const network::IPAddresses &ips, const network::IPAddress &dns1, + const network::IPAddress &dns2) { + std::string dns_results = dns1.str() + " " + dns2.str(); this->publish_state(dns_results); } @@ -56,14 +48,11 @@ void DNSAddressWifiInfo::state_callback_(const network::IPAddress &dns1_ip, cons * ScanResultsWiFiInfo *********************/ -void ScanResultsWiFiInfo::setup() { - wifi::global_wifi_component->add_on_wifi_scan_state_callback( - [this](const wifi::wifi_scan_vector_t &results) { this->state_callback_(results); }); -} +void ScanResultsWiFiInfo::setup() { wifi::global_wifi_component->add_scan_results_listener(this); } void ScanResultsWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "Scan Results", this); } -void ScanResultsWiFiInfo::state_callback_(const wifi::wifi_scan_vector_t &results) { +void ScanResultsWiFiInfo::on_wifi_scan_results(const wifi::wifi_scan_vector_t &results) { std::string scan_results; for (const auto &scan : results) { if (scan.get_is_hidden()) @@ -85,33 +74,30 @@ void ScanResultsWiFiInfo::state_callback_(const wifi::wifi_scan_vector_tadd_on_wifi_connect_state_callback( - [this](const std::string &ssid, const wifi::bssid_t &bssid) { this->state_callback_(ssid); }); -} +void SSIDWiFiInfo::setup() { wifi::global_wifi_component->add_connect_state_listener(this); } void SSIDWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "SSID", this); } -void SSIDWiFiInfo::state_callback_(const std::string &ssid) { this->publish_state(ssid); } +void SSIDWiFiInfo::on_wifi_connect_state(const std::string &ssid, const wifi::bssid_t &bssid) { + this->publish_state(ssid); +} /**************** * BSSIDWiFiInfo ***************/ -void BSSIDWiFiInfo::setup() { - wifi::global_wifi_component->add_on_wifi_connect_state_callback( - [this](const std::string &ssid, const wifi::bssid_t &bssid) { this->state_callback_(bssid); }); -} +void BSSIDWiFiInfo::setup() { wifi::global_wifi_component->add_connect_state_listener(this); } void BSSIDWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "BSSID", this); } -void BSSIDWiFiInfo::state_callback_(const wifi::bssid_t &bssid) { +void BSSIDWiFiInfo::on_wifi_connect_state(const std::string &ssid, const wifi::bssid_t &bssid) { char buf[18] = "unknown"; if (mac_address_is_valid(bssid.data())) { format_mac_addr_upper(bssid.data(), buf); } this->publish_state(buf); } + /********************* * MacAddressWifiInfo ********************/ diff --git a/esphome/components/wifi_info/wifi_info_text_sensor.h b/esphome/components/wifi_info/wifi_info_text_sensor.h index 12666b4059d..74d951f9226 100644 --- a/esphome/components/wifi_info/wifi_info_text_sensor.h +++ b/esphome/components/wifi_info/wifi_info_text_sensor.h @@ -9,55 +9,61 @@ namespace esphome::wifi_info { -class IPAddressWiFiInfo : public Component, public text_sensor::TextSensor { +class IPAddressWiFiInfo final : public Component, public text_sensor::TextSensor, public wifi::WiFiIPStateListener { public: void setup() override; void dump_config() override; void add_ip_sensors(uint8_t index, text_sensor::TextSensor *s) { this->ip_sensors_[index] = s; } + // WiFiIPStateListener interface + void on_ip_state(const network::IPAddresses &ips, const network::IPAddress &dns1, + const network::IPAddress &dns2) override; + protected: - void state_callback_(const network::IPAddresses &ips); std::array ip_sensors_; }; -class DNSAddressWifiInfo : public Component, public text_sensor::TextSensor { +class DNSAddressWifiInfo final : public Component, public text_sensor::TextSensor, public wifi::WiFiIPStateListener { public: void setup() override; void dump_config() override; - protected: - void state_callback_(const network::IPAddress &dns1_ip, const network::IPAddress &dns2_ip); + // WiFiIPStateListener interface + void on_ip_state(const network::IPAddresses &ips, const network::IPAddress &dns1, + const network::IPAddress &dns2) override; }; -class ScanResultsWiFiInfo : public Component, public text_sensor::TextSensor { +class ScanResultsWiFiInfo final : public Component, + public text_sensor::TextSensor, + public wifi::WiFiScanResultsListener { public: void setup() override; float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } void dump_config() override; - protected: - void state_callback_(const wifi::wifi_scan_vector_t &results); + // WiFiScanResultsListener interface + void on_wifi_scan_results(const wifi::wifi_scan_vector_t &results) override; }; -class SSIDWiFiInfo : public Component, public text_sensor::TextSensor { +class SSIDWiFiInfo final : public Component, public text_sensor::TextSensor, public wifi::WiFiConnectStateListener { public: void setup() override; void dump_config() override; - protected: - void state_callback_(const std::string &ssid); + // WiFiConnectStateListener interface + void on_wifi_connect_state(const std::string &ssid, const wifi::bssid_t &bssid) override; }; -class BSSIDWiFiInfo : public Component, public text_sensor::TextSensor { +class BSSIDWiFiInfo final : public Component, public text_sensor::TextSensor, public wifi::WiFiConnectStateListener { public: void setup() override; void dump_config() override; - protected: - void state_callback_(const wifi::bssid_t &bssid); + // WiFiConnectStateListener interface + void on_wifi_connect_state(const std::string &ssid, const wifi::bssid_t &bssid) override; }; -class MacAddressWifiInfo : public Component, public text_sensor::TextSensor { +class MacAddressWifiInfo final : public Component, public text_sensor::TextSensor { public: void setup() override { char mac_s[18]; diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 1373ea63669..f4026aad967 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -210,7 +210,7 @@ #define USE_WEBSERVER_SORTING #define USE_WIFI_11KV_SUPPORT #define USE_WIFI_FAST_CONNECT -#define USE_WIFI_CALLBACKS +#define USE_WIFI_LISTENERS #define USE_WIFI_RUNTIME_POWER_SAVE #define USB_HOST_MAX_REQUESTS 16 From fb82362e9cbd97e09b5eb1cee5e4d5840e038ff0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Nov 2025 12:13:29 -0600 Subject: [PATCH 18/57] [api] Eliminate rx_buf heap churn and release buffers after initial sync (#12133) --- esphome/components/api/api_connection.cpp | 6 ++++-- esphome/components/api/api_connection.h | 15 +++++++++---- esphome/components/api/api_frame_helper.h | 21 ++++++++++++++++--- .../components/api/api_frame_helper_noise.cpp | 3 +-- .../api/api_frame_helper_plaintext.cpp | 3 +-- esphome/components/api/api_pb2_service.cpp | 4 ++-- esphome/components/api/api_pb2_service.h | 4 ++-- esphome/components/api/proto.h | 2 +- script/api_protobuf/api_protobuf.py | 8 +++---- 9 files changed, 44 insertions(+), 22 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 12cbbb991da..9ad45dc6b78 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -169,8 +169,7 @@ void APIConnection::loop() { } else { this->last_traffic_ = now; // read a packet - this->read_message(buffer.data_len, buffer.type, - buffer.data_len > 0 ? &buffer.container[buffer.data_offset] : nullptr); + this->read_message(buffer.data_len, buffer.type, buffer.data); if (this->flags_.remove) return; } @@ -195,6 +194,9 @@ void APIConnection::loop() { } // Now that everything is sent, enable immediate sending for future state changes this->flags_.should_try_send_immediately = true; + // Release excess memory from buffers that grew during initial sync + this->deferred_batch_.release_buffer(); + this->helper_->release_buffers(); } } diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index af3a19909f1..05af0ccde79 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -554,10 +554,8 @@ class APIConnection final : public APIServerConnection { std::vector items; uint32_t batch_start_time{0}; - DeferredBatch() { - // Pre-allocate capacity for typical batch sizes to avoid reallocation - items.reserve(8); - } + // No pre-allocation - log connections never use batching, and for + // connections that do, buffers are released after initial sync anyway // Add item to the batch void add_item(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size); @@ -576,6 +574,15 @@ class APIConnection final : public APIServerConnection { bool empty() const { return items.empty(); } size_t size() const { return items.size(); } const BatchItem &operator[](size_t index) const { return items[index]; } + // Release excess capacity - only releases if items already empty + void release_buffer() { + // Safe to call: batch is processed before release_buffer is called, + // and if any items remain (partial processing), we must not clear them. + // Use swap trick since shrink_to_fit() is non-binding and may be ignored. + if (items.empty()) { + std::vector().swap(items); + } + } }; // DeferredBatch here (16 bytes, 4-byte aligned) diff --git a/esphome/components/api/api_frame_helper.h b/esphome/components/api/api_frame_helper.h index d931a6e3a95..b582bcea9a0 100644 --- a/esphome/components/api/api_frame_helper.h +++ b/esphome/components/api/api_frame_helper.h @@ -35,10 +35,9 @@ struct ClientInfo; class ProtoWriteBuffer; struct ReadPacketBuffer { - std::vector container; - uint16_t type; - uint16_t data_offset; + const uint8_t *data; // Points directly into frame helper's rx_buf_ (valid until next read_packet call) uint16_t data_len; + uint16_t type; }; // Packed packet info structure to minimize memory usage @@ -119,6 +118,22 @@ class APIFrameHelper { uint8_t frame_footer_size() const { return frame_footer_size_; } // Check if socket has data ready to read bool is_socket_ready() const { return socket_ != nullptr && socket_->ready(); } + // Release excess memory from internal buffers after initial sync + void release_buffers() { + // rx_buf_: Safe to clear only if no partial read in progress. + // rx_buf_len_ tracks bytes read so far; if non-zero, we're mid-frame + // and clearing would lose partially received data. + if (this->rx_buf_len_ == 0) { + // Use swap trick since shrink_to_fit() is non-binding and may be ignored + std::vector().swap(this->rx_buf_); + } + // reusable_iovs_: Safe to release unconditionally. + // Only used within write_protobuf_packets() calls - cleared at start, + // populated with pointers, used for writev(), then function returns. + // The iovecs contain stale pointers after the call (data was either sent + // or copied to tx_buf_), and are cleared on next write_protobuf_packets(). + std::vector().swap(this->reusable_iovs_); + } protected: // Buffer containing data to be sent diff --git a/esphome/components/api/api_frame_helper_noise.cpp b/esphome/components/api/api_frame_helper_noise.cpp index f1028fa2991..ae69f0b673c 100644 --- a/esphome/components/api/api_frame_helper_noise.cpp +++ b/esphome/components/api/api_frame_helper_noise.cpp @@ -407,8 +407,7 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) { return APIError::BAD_DATA_PACKET; } - buffer->container = std::move(this->rx_buf_); - buffer->data_offset = 4; + buffer->data = msg_data + 4; // Skip 4-byte header (type + length) buffer->data_len = data_len; buffer->type = type; return APIError::OK; diff --git a/esphome/components/api/api_frame_helper_plaintext.cpp b/esphome/components/api/api_frame_helper_plaintext.cpp index dcbd35aa324..b5d90b24291 100644 --- a/esphome/components/api/api_frame_helper_plaintext.cpp +++ b/esphome/components/api/api_frame_helper_plaintext.cpp @@ -210,8 +210,7 @@ APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) { return aerr; } - buffer->container = std::move(this->rx_buf_); - buffer->data_offset = 0; + buffer->data = this->rx_buf_.data(); buffer->data_len = this->rx_header_parsed_len_; buffer->type = this->rx_header_parsed_type_; return APIError::OK; diff --git a/esphome/components/api/api_pb2_service.cpp b/esphome/components/api/api_pb2_service.cpp index 3d28a137c85..45f6ecd30e7 100644 --- a/esphome/components/api/api_pb2_service.cpp +++ b/esphome/components/api/api_pb2_service.cpp @@ -13,7 +13,7 @@ void APIServerConnectionBase::log_send_message_(const char *name, const std::str } #endif -void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) { +void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) { switch (msg_type) { case HelloRequest::MESSAGE_TYPE: { HelloRequest msg; @@ -827,7 +827,7 @@ void APIServerConnection::on_z_wave_proxy_frame(const ZWaveProxyFrame &msg) { th void APIServerConnection::on_z_wave_proxy_request(const ZWaveProxyRequest &msg) { this->zwave_proxy_request(msg); } #endif -void APIServerConnection::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) { +void APIServerConnection::read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) { // Check authentication/connection requirements for messages switch (msg_type) { case HelloRequest::MESSAGE_TYPE: // No setup required diff --git a/esphome/components/api/api_pb2_service.h b/esphome/components/api/api_pb2_service.h index 827b89e23c1..6d94046a23a 100644 --- a/esphome/components/api/api_pb2_service.h +++ b/esphome/components/api/api_pb2_service.h @@ -218,7 +218,7 @@ class APIServerConnectionBase : public ProtoService { virtual void on_z_wave_proxy_request(const ZWaveProxyRequest &value){}; #endif protected: - void read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override; + void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) override; }; class APIServerConnection : public APIServerConnectionBase { @@ -480,7 +480,7 @@ class APIServerConnection : public APIServerConnectionBase { #ifdef USE_ZWAVE_PROXY void on_z_wave_proxy_request(const ZWaveProxyRequest &msg) override; #endif - void read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override; + void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) override; }; } // namespace esphome::api diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index e7585924a59..83b6922be1a 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -846,7 +846,7 @@ class ProtoService { */ virtual ProtoWriteBuffer create_buffer(uint32_t reserve_size) = 0; virtual bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) = 0; - virtual void read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) = 0; + virtual void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) = 0; // Optimized method that pre-allocates buffer based on message size bool send_message_(const ProtoMessage &msg, uint8_t message_type) { diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index b07a249c8df..3412fac5db4 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -2769,8 +2769,8 @@ static const char *const TAG = "api.service"; cases = list(RECEIVE_CASES.items()) cases.sort() hpp += " protected:\n" - hpp += " void read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override;\n" - out = f"void {class_name}::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) {{\n" + hpp += " void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) override;\n" + out = f"void {class_name}::read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) {{\n" out += " switch (msg_type) {\n" for i, (case, ifdef, message_name) in cases: if ifdef is not None: @@ -2878,9 +2878,9 @@ static const char *const TAG = "api.service"; result += "#endif\n" return result - hpp_protected += " void read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override;\n" + hpp_protected += " void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) override;\n" - cpp += f"\nvoid {class_name}::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) {{\n" + cpp += f"\nvoid {class_name}::read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) {{\n" cpp += " // Check authentication/connection requirements for messages\n" cpp += " switch (msg_type) {\n" From e15f3a08aef0255b111db331c726128e4b68db8d Mon Sep 17 00:00:00 2001 From: Javier Peletier Date: Fri, 28 Nov 2025 19:15:55 +0100 Subject: [PATCH 19/57] [tests] Remote packages with substitutions (#12145) --- .../06-remote_packages.approved.yaml | 25 +++++++++++++ .../06-remote_packages.input.yaml | 37 +++++++++++++++++++ .../substitutions/remote_package_proxy.yaml | 6 +++ .../fixtures/substitutions/remotes/README.md | 3 ++ .../remotes/repo1/main/file1.yaml | 9 +++++ .../remotes/repo2/main/file2.yaml | 10 +++++ tests/unit_tests/test_substitutions.py | 33 ++++++++++++++++- 7 files changed, 122 insertions(+), 1 deletion(-) create mode 100644 tests/unit_tests/fixtures/substitutions/06-remote_packages.approved.yaml create mode 100644 tests/unit_tests/fixtures/substitutions/06-remote_packages.input.yaml create mode 100644 tests/unit_tests/fixtures/substitutions/remote_package_proxy.yaml create mode 100644 tests/unit_tests/fixtures/substitutions/remotes/README.md create mode 100644 tests/unit_tests/fixtures/substitutions/remotes/repo1/main/file1.yaml create mode 100644 tests/unit_tests/fixtures/substitutions/remotes/repo2/main/file2.yaml diff --git a/tests/unit_tests/fixtures/substitutions/06-remote_packages.approved.yaml b/tests/unit_tests/fixtures/substitutions/06-remote_packages.approved.yaml new file mode 100644 index 00000000000..4b5315013ce --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/06-remote_packages.approved.yaml @@ -0,0 +1,25 @@ +substitutions: + x: 10 + y: 20 + z: 30 +values_from_repo1_main: + - package_name: package1 + x: 3 + y: 4 + z: 5 + volume: 60 + - package_name: package2 + x: 6 + y: 7 + z: 8 + volume: 336 + - package_name: default + x: 10 + y: 20 + z: 5 + volume: 1000 + - package_name: package4_from_repo2 + x: 9 + y: 10 + z: 11 + volume: 990 diff --git a/tests/unit_tests/fixtures/substitutions/06-remote_packages.input.yaml b/tests/unit_tests/fixtures/substitutions/06-remote_packages.input.yaml new file mode 100644 index 00000000000..a8128a7a07b --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/06-remote_packages.input.yaml @@ -0,0 +1,37 @@ +substitutions: + x: 10 + y: 20 + z: 30 +packages: + package1: + url: https://github.com/esphome/repo1 + files: + - path: file1.yaml + vars: + package_name: package1 + x: 3 + y: 4 + ref: main + package2: !include # a package that just includes the given remote package + file: remote_package_proxy.yaml + vars: + url: https://github.com/esphome/repo1 + ref: main + files: + - path: file1.yaml + vars: + package_name: package2 + x: 6 + y: 7 + z: 8 + package3: github://esphome/repo1/file1.yaml@main # a package that uses the shorthand syntax + package4: # include repo2, which itself includes repo1 + url: https://github.com/esphome/repo2 + files: + - path: file2.yaml + vars: + package_name: package4 + a: 9 + b: 10 + c: 11 + ref: main diff --git a/tests/unit_tests/fixtures/substitutions/remote_package_proxy.yaml b/tests/unit_tests/fixtures/substitutions/remote_package_proxy.yaml new file mode 100644 index 00000000000..05da30acb45 --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/remote_package_proxy.yaml @@ -0,0 +1,6 @@ +# acts as a proxy to be able to include a remote package +# in which the url/ref/files come from a substitution +packages: + - url: ${url} + ref: ${ref} + files: ${files} diff --git a/tests/unit_tests/fixtures/substitutions/remotes/README.md b/tests/unit_tests/fixtures/substitutions/remotes/README.md new file mode 100644 index 00000000000..09d9f386998 --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/remotes/README.md @@ -0,0 +1,3 @@ +This folder contains fake repos for remote packages testing +These are used by `test_substitutions.py`. +To add repos, create a folder and add its path to the `REMOTES` constant in `test_substitutions.py`. diff --git a/tests/unit_tests/fixtures/substitutions/remotes/repo1/main/file1.yaml b/tests/unit_tests/fixtures/substitutions/remotes/repo1/main/file1.yaml new file mode 100644 index 00000000000..3830b1650f3 --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/remotes/repo1/main/file1.yaml @@ -0,0 +1,9 @@ +defaults: + z: 5 + package_name: default +values_from_repo1_main: + - package_name: ${package_name} + x: ${x} + y: ${y} + z: ${z} + volume: ${x*y*z} diff --git a/tests/unit_tests/fixtures/substitutions/remotes/repo2/main/file2.yaml b/tests/unit_tests/fixtures/substitutions/remotes/repo2/main/file2.yaml new file mode 100644 index 00000000000..7f62ab8926e --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/remotes/repo2/main/file2.yaml @@ -0,0 +1,10 @@ +packages: + - url: https://github.com/esphome/repo1 + ref: main + files: + - path: file1.yaml + vars: + package_name: ${package_name}_from_repo2 + x: ${a} + y: ${b} + z: ${c} diff --git a/tests/unit_tests/test_substitutions.py b/tests/unit_tests/test_substitutions.py index 7d50b44506e..c5e6618ea6f 100644 --- a/tests/unit_tests/test_substitutions.py +++ b/tests/unit_tests/test_substitutions.py @@ -2,6 +2,7 @@ import glob import logging from pathlib import Path from typing import Any +from unittest.mock import patch from esphome import config as config_module, yaml_util from esphome.components import substitutions @@ -84,11 +85,41 @@ def verify_database(value: Any, path: str = "") -> str | None: return None -def test_substitutions_fixtures(fixture_path): +# Mapping of (url, ref) to local test repository path under fixtures/substitutions +REMOTES = { + ("https://github.com/esphome/repo1", "main"): "remotes/repo1/main", + ("https://github.com/esphome/repo2", "main"): "remotes/repo2/main", +} + + +@patch("esphome.git.clone_or_update") +def test_substitutions_fixtures(mock_clone_or_update, fixture_path): base_dir = fixture_path / "substitutions" sources = sorted(glob.glob(str(base_dir / "*.input.yaml"))) assert sources, f"No input YAML files found in {base_dir}" + def fake_clone_or_update( + *, + url: str, + ref: str | None = None, + refresh=None, + domain: str, + username: str | None = None, + password: str | None = None, + submodules: list[str] | None = None, + _recover_broken: bool = True, + ) -> tuple[Path, None]: + path = REMOTES.get((url, ref)) + if path is None: + path = REMOTES.get((url.rstrip(".git"), ref)) + if path is None: + raise RuntimeError( + f"Cannot find test repository for {url} @ {ref}. Check the REMOTES mapping in test_substitutions.py" + ) + return base_dir / path, None + + mock_clone_or_update.side_effect = fake_clone_or_update + failures = [] for source_path in sources: source_path = Path(source_path) From d6ca01775e5c19a48c58f7147c3dfc8e6cc00489 Mon Sep 17 00:00:00 2001 From: Javier Peletier Date: Fri, 28 Nov 2025 19:24:09 +0100 Subject: [PATCH 20/57] [packages] Restore remote shorthand vars and !remove in early package contents validation (#12158) Co-authored-by: J. Nick Koston --- esphome/components/packages/__init__.py | 12 +++++++++--- .../substitutions/06-remote_packages.approved.yaml | 5 +++++ .../substitutions/06-remote_packages.input.yaml | 6 ++++++ .../substitutions/remote_package_shorthand.yaml | 4 ++++ 4 files changed, 24 insertions(+), 3 deletions(-) create mode 100644 tests/unit_tests/fixtures/substitutions/remote_package_shorthand.yaml diff --git a/esphome/components/packages/__init__.py b/esphome/components/packages/__init__.py index 41cde0391bc..67fd2770e92 100644 --- a/esphome/components/packages/__init__.py +++ b/esphome/components/packages/__init__.py @@ -2,7 +2,8 @@ import logging from pathlib import Path from esphome import git, yaml_util -from esphome.config_helpers import merge_config +from esphome.components.substitutions.jinja import has_jinja +from esphome.config_helpers import Remove, merge_config import esphome.config_validation as cv from esphome.const import ( CONF_ESPHOME, @@ -39,10 +40,15 @@ def valid_package_contents(package_config: dict): for k, v in package_config.items(): if not isinstance(k, str): raise cv.Invalid("Package content keys must be strings") - if isinstance(v, (dict, list)): - continue # e.g. script: [] or logger: {level: debug} + if isinstance(v, (dict, list, Remove)): + continue # e.g. script: [], psram: !remove, logger: {level: debug} if v is None: continue # e.g. web_server: + if isinstance(v, str) and has_jinja(v): + # e.g: remote package shorthand: + # package_name: github://esphome/repo/file.yaml@${ branch } + continue + raise cv.Invalid("Invalid component content in package definition") return package_config diff --git a/tests/unit_tests/fixtures/substitutions/06-remote_packages.approved.yaml b/tests/unit_tests/fixtures/substitutions/06-remote_packages.approved.yaml index 4b5315013ce..0fffbfb7cba 100644 --- a/tests/unit_tests/fixtures/substitutions/06-remote_packages.approved.yaml +++ b/tests/unit_tests/fixtures/substitutions/06-remote_packages.approved.yaml @@ -23,3 +23,8 @@ values_from_repo1_main: y: 10 z: 11 volume: 990 + - package_name: default + x: 10 + y: 20 + z: 5 + volume: 1000 diff --git a/tests/unit_tests/fixtures/substitutions/06-remote_packages.input.yaml b/tests/unit_tests/fixtures/substitutions/06-remote_packages.input.yaml index a8128a7a07b..772860bf19f 100644 --- a/tests/unit_tests/fixtures/substitutions/06-remote_packages.input.yaml +++ b/tests/unit_tests/fixtures/substitutions/06-remote_packages.input.yaml @@ -35,3 +35,9 @@ packages: b: 10 c: 11 ref: main + package5: !include + file: remote_package_shorthand.yaml + vars: + repo: repo1 + file: file1.yaml + ref: main diff --git a/tests/unit_tests/fixtures/substitutions/remote_package_shorthand.yaml b/tests/unit_tests/fixtures/substitutions/remote_package_shorthand.yaml new file mode 100644 index 00000000000..f49e85e0383 --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/remote_package_shorthand.yaml @@ -0,0 +1,4 @@ +# acts as a proxy to be able to include a remote package +# in which the shorthand comes from a substitution +packages: + - github://esphome/${repo}/${file}@${ref} From 2e5529664006c585692ba47310229e2fe3f3e52c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Nov 2025 14:43:11 -0600 Subject: [PATCH 21/57] [sensor] Replace timeout filter scheduler with loop-based implementation (#11922) --- esphome/components/sensor/__init__.py | 11 +++++-- esphome/components/sensor/filter.cpp | 43 ++++++++++++++++++++------ esphome/components/sensor/filter.h | 44 ++++++++++++++++++++++----- 3 files changed, 78 insertions(+), 20 deletions(-) diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index e8fec222a1c..f83226d10fc 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -270,7 +270,9 @@ ThrottleFilter = sensor_ns.class_("ThrottleFilter", Filter) ThrottleWithPriorityFilter = sensor_ns.class_( "ThrottleWithPriorityFilter", ValueListFilter ) -TimeoutFilter = sensor_ns.class_("TimeoutFilter", Filter, cg.Component) +TimeoutFilterBase = sensor_ns.class_("TimeoutFilterBase", Filter, cg.Component) +TimeoutFilterLast = sensor_ns.class_("TimeoutFilterLast", TimeoutFilterBase) +TimeoutFilterConfigured = sensor_ns.class_("TimeoutFilterConfigured", TimeoutFilterBase) DebounceFilter = sensor_ns.class_("DebounceFilter", Filter, cg.Component) HeartbeatFilter = sensor_ns.class_("HeartbeatFilter", Filter, cg.Component) DeltaFilter = sensor_ns.class_("DeltaFilter", Filter) @@ -681,11 +683,16 @@ TIMEOUT_SCHEMA = cv.maybe_simple_value( ) -@FILTER_REGISTRY.register("timeout", TimeoutFilter, TIMEOUT_SCHEMA) +@FILTER_REGISTRY.register("timeout", TimeoutFilterBase, TIMEOUT_SCHEMA) async def timeout_filter_to_code(config, filter_id): + filter_id = filter_id.copy() if config[CONF_VALUE] == "last": + # Use TimeoutFilterLast for "last" mode (smaller, more common - LD2450, LD2412, etc.) + filter_id.type = TimeoutFilterLast var = cg.new_Pvariable(filter_id, config[CONF_TIMEOUT]) else: + # Use TimeoutFilterConfigured for configured value mode + filter_id.type = TimeoutFilterConfigured template_ = await cg.templatable(config[CONF_VALUE], [], float) var = cg.new_Pvariable(filter_id, config[CONF_TIMEOUT], template_) await cg.register_component(var, {}) diff --git a/esphome/components/sensor/filter.cpp b/esphome/components/sensor/filter.cpp index 65d8dea31c0..c8c65401126 100644 --- a/esphome/components/sensor/filter.cpp +++ b/esphome/components/sensor/filter.cpp @@ -339,20 +339,43 @@ void OrFilter::initialize(Sensor *parent, Filter *next) { this->phi_.initialize(parent, nullptr); } -// TimeoutFilter -optional TimeoutFilter::new_value(float value) { - if (this->value_.has_value()) { - this->set_timeout("timeout", this->time_period_, [this]() { this->output(this->value_.value().value()); }); - } else { - this->set_timeout("timeout", this->time_period_, [this, value]() { this->output(value); }); +// TimeoutFilterBase - shared loop logic +void TimeoutFilterBase::loop() { + // Check if timeout period has elapsed + // Use cached loop start time to avoid repeated millis() calls + const uint32_t now = App.get_loop_component_start_time(); + if (now - this->timeout_start_time_ >= this->time_period_) { + // Timeout fired - get output value from derived class and output it + this->output(this->get_output_value()); + + // Disable loop until next value arrives + this->disable_loop(); } +} + +float TimeoutFilterBase::get_setup_priority() const { return setup_priority::HARDWARE; } + +// TimeoutFilterLast - "last" mode implementation +optional TimeoutFilterLast::new_value(float value) { + // Store the value to output when timeout fires + this->pending_value_ = value; + + // Record when timeout started and enable loop + this->timeout_start_time_ = millis(); + this->enable_loop(); + return value; } -TimeoutFilter::TimeoutFilter(uint32_t time_period) : time_period_(time_period) {} -TimeoutFilter::TimeoutFilter(uint32_t time_period, const TemplatableValue &new_value) - : time_period_(time_period), value_(new_value) {} -float TimeoutFilter::get_setup_priority() const { return setup_priority::HARDWARE; } +// TimeoutFilterConfigured - configured value mode implementation +optional TimeoutFilterConfigured::new_value(float value) { + // Record when timeout started and enable loop + // Note: we don't store the incoming value since we have a configured value + this->timeout_start_time_ = millis(); + this->enable_loop(); + + return value; +} // DebounceFilter optional DebounceFilter::new_value(float value) { diff --git a/esphome/components/sensor/filter.h b/esphome/components/sensor/filter.h index 75e28a1efef..92a9184c18c 100644 --- a/esphome/components/sensor/filter.h +++ b/esphome/components/sensor/filter.h @@ -380,18 +380,46 @@ class ThrottleWithPriorityFilter : public ValueListFilter { uint32_t min_time_between_inputs_; }; -class TimeoutFilter : public Filter, public Component { +// Base class for timeout filters - contains common loop logic +class TimeoutFilterBase : public Filter, public Component { public: - explicit TimeoutFilter(uint32_t time_period); - explicit TimeoutFilter(uint32_t time_period, const TemplatableValue &new_value); - - optional new_value(float value) override; - + void loop() override; float get_setup_priority() const override; protected: - uint32_t time_period_; - optional> value_; + explicit TimeoutFilterBase(uint32_t time_period) : time_period_(time_period) { this->disable_loop(); } + virtual float get_output_value() = 0; + + uint32_t time_period_; // 4 bytes (timeout duration in ms) + uint32_t timeout_start_time_{0}; // 4 bytes (when the timeout was started) + // Total base: 8 bytes +}; + +// Timeout filter for "last" mode - outputs the last received value after timeout +class TimeoutFilterLast : public TimeoutFilterBase { + public: + explicit TimeoutFilterLast(uint32_t time_period) : TimeoutFilterBase(time_period) {} + + optional new_value(float value) override; + + protected: + float get_output_value() override { return this->pending_value_; } + float pending_value_{0}; // 4 bytes (value to output when timeout fires) + // Total: 8 (base) + 4 = 12 bytes + vtable ptr + Component overhead +}; + +// Timeout filter with configured value - evaluates TemplatableValue after timeout +class TimeoutFilterConfigured : public TimeoutFilterBase { + public: + explicit TimeoutFilterConfigured(uint32_t time_period, const TemplatableValue &new_value) + : TimeoutFilterBase(time_period), value_(new_value) {} + + optional new_value(float value) override; + + protected: + float get_output_value() override { return this->value_.value(); } + TemplatableValue value_; // 16 bytes (configured output value, can be lambda) + // Total: 8 (base) + 16 = 24 bytes + vtable ptr + Component overhead }; class DebounceFilter : public Filter, public Component { From ca599b25c2a817a963eb5f76de2a0fcd8cc73738 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Nov 2025 15:33:28 -0600 Subject: [PATCH 22/57] [espnow] Initialize LwIP stack when running without WiFi component (#12169) --- esphome/components/espnow/espnow_component.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/esphome/components/espnow/espnow_component.cpp b/esphome/components/espnow/espnow_component.cpp index d2f136d1c7a..bc058337092 100644 --- a/esphome/components/espnow/espnow_component.cpp +++ b/esphome/components/espnow/espnow_component.cpp @@ -10,6 +10,7 @@ #include #include +#include #include #include #include @@ -157,6 +158,12 @@ bool ESPNowComponent::is_wifi_enabled() { } void ESPNowComponent::setup() { +#ifndef USE_WIFI + // Initialize LwIP stack for wake_loop_threadsafe() socket support + // When WiFi component is present, it handles esp_netif_init() + ESP_ERROR_CHECK(esp_netif_init()); +#endif + if (this->enable_on_boot_) { this->enable_(); } else { From bc50be6053493aa453ab68602be89c2c4c55ecc6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Nov 2025 16:14:00 -0600 Subject: [PATCH 23/57] [logger] Conditionally compile log level change listener (#12168) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/logger/__init__.py | 23 +++++++++++++ esphome/components/logger/logger.cpp | 5 ++- esphome/components/logger/logger.h | 34 +++++++++++++++++-- esphome/components/logger/select/__init__.py | 9 ++++- .../logger/select/logger_level_select.cpp | 6 ++-- .../logger/select/logger_level_select.h | 9 +++-- esphome/core/defines.h | 1 + 7 files changed, 77 insertions(+), 10 deletions(-) diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index 39877030e9e..d9ca44d3c9e 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -406,6 +406,8 @@ async def to_code(config): conf, ) + CORE.add_job(final_step) + def validate_printf(value): # https://stackoverflow.com/questions/30011379/how-can-i-parse-a-c-format-string-in-python @@ -506,3 +508,24 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform( }, } ) + +# Keys for CORE.data storage +DOMAIN = "logger" +KEY_LEVEL_LISTENERS = "level_listeners" + + +def request_logger_level_listeners() -> None: + """Request that logger level listeners be compiled in. + + Components that need to be notified about log level changes should call this + function during their code generation. This enables the add_level_listener() + method and compiles in the listener vector. + """ + CORE.data.setdefault(DOMAIN, {})[KEY_LEVEL_LISTENERS] = True + + +@coroutine_with_priority(CoroPriority.FINAL) +async def final_step(): + """Final code generation step to configure optional logger features.""" + if CORE.data.get(DOMAIN, {}).get(KEY_LEVEL_LISTENERS, False): + cg.add_define("USE_LOGGER_LEVEL_LISTENERS") diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index f925e85e116..21e2b448082 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -288,7 +288,10 @@ void Logger::set_log_level(uint8_t level) { ESP_LOGW(TAG, "Cannot set log level higher than pre-compiled %s", LOG_STR_ARG(LOG_LEVELS[ESPHOME_LOG_LEVEL])); } this->current_level_ = level; - this->level_callback_.call(level); +#ifdef USE_LOGGER_LEVEL_LISTENERS + for (auto *listener : this->level_listeners_) + listener->on_log_level_change(level); +#endif } Logger *global_logger = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index a0024411d78..8abc1196e18 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -58,6 +58,30 @@ class LogListener { virtual void on_log(uint8_t level, const char *tag, const char *message, size_t message_len) = 0; }; +#ifdef USE_LOGGER_LEVEL_LISTENERS +/** Interface for receiving log level changes without std::function overhead. + * + * Components can implement this interface instead of using lambdas with std::function + * to reduce flash usage from std::function type erasure machinery. + * + * Usage: + * class MyComponent : public Component, public LoggerLevelListener { + * public: + * void setup() override { + * if (logger::global_logger != nullptr) + * logger::global_logger->add_logger_level_listener(this); + * } + * void on_log_level_change(uint8_t level) override { + * // Handle log level change + * } + * }; + */ +class LoggerLevelListener { + public: + virtual void on_log_level_change(uint8_t level) = 0; +}; +#endif + #ifdef USE_LOGGER_RUNTIME_TAG_LEVELS // Comparison function for const char* keys in log_levels_ map struct CStrCompare { @@ -193,8 +217,10 @@ class Logger : public Component { /// Register a log listener to receive log messages void add_log_listener(LogListener *listener) { this->log_listeners_.push_back(listener); } - // add a listener for log level changes - void add_listener(std::function &&callback) { this->level_callback_.add(std::move(callback)); } +#ifdef USE_LOGGER_LEVEL_LISTENERS + /// Register a listener for log level changes + void add_level_listener(LoggerLevelListener *listener) { this->level_listeners_.push_back(listener); } +#endif float get_setup_priority() const override; @@ -325,7 +351,9 @@ class Logger : public Component { std::map log_levels_{}; #endif std::vector log_listeners_; // Log message listeners (API, MQTT, syslog, etc.) - CallbackManager level_callback_{}; +#ifdef USE_LOGGER_LEVEL_LISTENERS + std::vector level_listeners_; // Log level change listeners +#endif #ifdef USE_ESPHOME_TASK_LOG_BUFFER std::unique_ptr log_buffer_; // Will be initialized with init_log_buffer #endif diff --git a/esphome/components/logger/select/__init__.py b/esphome/components/logger/select/__init__.py index 2e83599eb49..6ce663978e4 100644 --- a/esphome/components/logger/select/__init__.py +++ b/esphome/components/logger/select/__init__.py @@ -5,7 +5,13 @@ from esphome.const import CONF_LEVEL, CONF_LOGGER, ENTITY_CATEGORY_CONFIG, ICON_ from esphome.core import CORE from esphome.cpp_helpers import register_component, register_parented -from .. import CONF_LOGGER_ID, LOG_LEVELS, Logger, logger_ns +from .. import ( + CONF_LOGGER_ID, + LOG_LEVELS, + Logger, + logger_ns, + request_logger_level_listeners, +) CODEOWNERS = ["@clydebarrow"] @@ -21,6 +27,7 @@ CONFIG_SCHEMA = select.select_schema( async def to_code(config): + request_logger_level_listeners() parent = await cg.get_variable(config[CONF_LOGGER_ID]) levels = list(LOG_LEVELS) index = levels.index(CORE.data[CONF_LOGGER][CONF_LEVEL]) diff --git a/esphome/components/logger/select/logger_level_select.cpp b/esphome/components/logger/select/logger_level_select.cpp index e2ec28a3908..3091ca18515 100644 --- a/esphome/components/logger/select/logger_level_select.cpp +++ b/esphome/components/logger/select/logger_level_select.cpp @@ -2,7 +2,7 @@ namespace esphome::logger { -void LoggerLevelSelect::publish_state(int level) { +void LoggerLevelSelect::on_log_level_change(uint8_t level) { auto index = level_to_index(level); if (!this->has_index(index)) return; @@ -10,8 +10,8 @@ void LoggerLevelSelect::publish_state(int level) { } void LoggerLevelSelect::setup() { - this->parent_->add_listener([this](int level) { this->publish_state(level); }); - this->publish_state(this->parent_->get_log_level()); + this->parent_->add_level_listener(this); + this->on_log_level_change(this->parent_->get_log_level()); } void LoggerLevelSelect::control(size_t index) { this->parent_->set_log_level(index_to_level(index)); } diff --git a/esphome/components/logger/select/logger_level_select.h b/esphome/components/logger/select/logger_level_select.h index 950edd29ac4..6482114943e 100644 --- a/esphome/components/logger/select/logger_level_select.h +++ b/esphome/components/logger/select/logger_level_select.h @@ -5,12 +5,17 @@ #include "esphome/components/logger/logger.h" namespace esphome::logger { -class LoggerLevelSelect : public Component, public select::Select, public Parented { +class LoggerLevelSelect final : public Component, + public select::Select, + public Parented, + public LoggerLevelListener { public: - void publish_state(int level); void setup() override; void control(size_t index) override; + // LoggerLevelListener interface + void on_log_level_change(uint8_t level) override; + protected: // Convert log level to option index (skip CONFIG at level 4) static uint8_t level_to_index(uint8_t level) { return (level > ESPHOME_LOG_LEVEL_CONFIG) ? level - 1 : level; } diff --git a/esphome/core/defines.h b/esphome/core/defines.h index f4026aad967..538d4e3d6e5 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -51,6 +51,7 @@ #define USE_LIGHT #define USE_LOCK #define USE_LOGGER +#define USE_LOGGER_LEVEL_LISTENERS #define USE_LOGGER_RUNTIME_TAG_LEVELS #define USE_LVGL #define USE_LVGL_ANIMIMG From 5fa4ff754c4306083ddd320edc68c706d568e862 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Nov 2025 21:57:01 -0600 Subject: [PATCH 24/57] [ble_client] Convert to C++17 namespace style (#12176) --- esphome/components/ble_client/automation.cpp | 6 ++---- esphome/components/ble_client/automation.h | 6 ++---- esphome/components/ble_client/ble_client.cpp | 6 ++---- esphome/components/ble_client/ble_client.h | 6 ++---- esphome/components/ble_client/output/ble_binary_output.cpp | 6 ++---- esphome/components/ble_client/output/ble_binary_output.h | 6 ++---- esphome/components/ble_client/sensor/automation.h | 6 ++---- esphome/components/ble_client/sensor/ble_rssi_sensor.cpp | 6 ++---- esphome/components/ble_client/sensor/ble_rssi_sensor.h | 6 ++---- esphome/components/ble_client/sensor/ble_sensor.cpp | 6 ++---- esphome/components/ble_client/sensor/ble_sensor.h | 6 ++---- esphome/components/ble_client/switch/ble_switch.cpp | 6 ++---- esphome/components/ble_client/switch/ble_switch.h | 6 ++---- esphome/components/ble_client/text_sensor/automation.h | 6 ++---- .../components/ble_client/text_sensor/ble_text_sensor.cpp | 6 ++---- esphome/components/ble_client/text_sensor/ble_text_sensor.h | 6 ++---- 16 files changed, 32 insertions(+), 64 deletions(-) diff --git a/esphome/components/ble_client/automation.cpp b/esphome/components/ble_client/automation.cpp index 9a0233eb704..cd2802f6173 100644 --- a/esphome/components/ble_client/automation.cpp +++ b/esphome/components/ble_client/automation.cpp @@ -2,12 +2,10 @@ #include "automation.h" -namespace esphome { -namespace ble_client { +namespace esphome::ble_client { const char *const Automation::TAG = "ble_client.automation"; -} // namespace ble_client -} // namespace esphome +} // namespace esphome::ble_client #endif diff --git a/esphome/components/ble_client/automation.h b/esphome/components/ble_client/automation.h index 788eac4a572..ccda8945093 100644 --- a/esphome/components/ble_client/automation.h +++ b/esphome/components/ble_client/automation.h @@ -9,8 +9,7 @@ #include "esphome/components/ble_client/ble_client.h" #include "esphome/core/log.h" -namespace esphome { -namespace ble_client { +namespace esphome::ble_client { // placeholder class for static TAG . class Automation { @@ -391,7 +390,6 @@ template class BLEClientDisconnectAction : public Action, BLEClient *ble_client_; std::tuple var_{}; }; -} // namespace ble_client -} // namespace esphome +} // namespace esphome::ble_client #endif diff --git a/esphome/components/ble_client/ble_client.cpp b/esphome/components/ble_client/ble_client.cpp index b8968fe4ba0..d41fb17961b 100644 --- a/esphome/components/ble_client/ble_client.cpp +++ b/esphome/components/ble_client/ble_client.cpp @@ -7,8 +7,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace ble_client { +namespace esphome::ble_client { static const char *const TAG = "ble_client"; @@ -82,7 +81,6 @@ bool BLEClient::all_nodes_established_() { return true; } -} // namespace ble_client -} // namespace esphome +} // namespace esphome::ble_client #endif diff --git a/esphome/components/ble_client/ble_client.h b/esphome/components/ble_client/ble_client.h index e04f4a8042e..ca523251ef7 100644 --- a/esphome/components/ble_client/ble_client.h +++ b/esphome/components/ble_client/ble_client.h @@ -15,8 +15,7 @@ #include #include -namespace esphome { -namespace ble_client { +namespace esphome::ble_client { namespace espbt = esphome::esp32_ble_tracker; @@ -75,7 +74,6 @@ class BLEClient : public BLEClientBase { std::vector nodes_; }; -} // namespace ble_client -} // namespace esphome +} // namespace esphome::ble_client #endif diff --git a/esphome/components/ble_client/output/ble_binary_output.cpp b/esphome/components/ble_client/output/ble_binary_output.cpp index 84558717f8f..1d874a65e4d 100644 --- a/esphome/components/ble_client/output/ble_binary_output.cpp +++ b/esphome/components/ble_client/output/ble_binary_output.cpp @@ -3,8 +3,7 @@ #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" #ifdef USE_ESP32 -namespace esphome { -namespace ble_client { +namespace esphome::ble_client { static const char *const TAG = "ble_binary_output"; @@ -75,6 +74,5 @@ void BLEBinaryOutput::write_state(bool state) { ESP_LOGW(TAG, "[%s] Write error, err=%d", this->char_uuid_.to_string().c_str(), err); } -} // namespace ble_client -} // namespace esphome +} // namespace esphome::ble_client #endif diff --git a/esphome/components/ble_client/output/ble_binary_output.h b/esphome/components/ble_client/output/ble_binary_output.h index 5e8bd6da62f..299de9b8605 100644 --- a/esphome/components/ble_client/output/ble_binary_output.h +++ b/esphome/components/ble_client/output/ble_binary_output.h @@ -7,8 +7,7 @@ #ifdef USE_ESP32 #include -namespace esphome { -namespace ble_client { +namespace esphome::ble_client { namespace espbt = esphome::esp32_ble_tracker; @@ -36,7 +35,6 @@ class BLEBinaryOutput : public output::BinaryOutput, public BLEClientNode, publi esp_gatt_write_type_t write_type_{}; }; -} // namespace ble_client -} // namespace esphome +} // namespace esphome::ble_client #endif diff --git a/esphome/components/ble_client/sensor/automation.h b/esphome/components/ble_client/sensor/automation.h index 56ab7ba4c93..84430cb7d97 100644 --- a/esphome/components/ble_client/sensor/automation.h +++ b/esphome/components/ble_client/sensor/automation.h @@ -5,8 +5,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace ble_client { +namespace esphome::ble_client { class BLESensorNotifyTrigger : public Trigger, public BLESensor { public: @@ -35,7 +34,6 @@ class BLESensorNotifyTrigger : public Trigger, public BLESensor { BLESensor *sensor_; }; -} // namespace ble_client -} // namespace esphome +} // namespace esphome::ble_client #endif diff --git a/esphome/components/ble_client/sensor/ble_rssi_sensor.cpp b/esphome/components/ble_client/sensor/ble_rssi_sensor.cpp index 4edcbd3877b..dc032a7a98b 100644 --- a/esphome/components/ble_client/sensor/ble_rssi_sensor.cpp +++ b/esphome/components/ble_client/sensor/ble_rssi_sensor.cpp @@ -6,8 +6,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace ble_client { +namespace esphome::ble_client { static const char *const TAG = "ble_rssi_sensor"; @@ -78,6 +77,5 @@ void BLEClientRSSISensor::get_rssi_() { } } -} // namespace ble_client -} // namespace esphome +} // namespace esphome::ble_client #endif diff --git a/esphome/components/ble_client/sensor/ble_rssi_sensor.h b/esphome/components/ble_client/sensor/ble_rssi_sensor.h index 76cd8345a65..570a5b423c9 100644 --- a/esphome/components/ble_client/sensor/ble_rssi_sensor.h +++ b/esphome/components/ble_client/sensor/ble_rssi_sensor.h @@ -8,8 +8,7 @@ #ifdef USE_ESP32 #include -namespace esphome { -namespace ble_client { +namespace esphome::ble_client { namespace espbt = esphome::esp32_ble_tracker; @@ -29,6 +28,5 @@ class BLEClientRSSISensor : public sensor::Sensor, public PollingComponent, publ bool should_update_{false}; }; -} // namespace ble_client -} // namespace esphome +} // namespace esphome::ble_client #endif diff --git a/esphome/components/ble_client/sensor/ble_sensor.cpp b/esphome/components/ble_client/sensor/ble_sensor.cpp index 8e3e4830035..38d90faff08 100644 --- a/esphome/components/ble_client/sensor/ble_sensor.cpp +++ b/esphome/components/ble_client/sensor/ble_sensor.cpp @@ -6,8 +6,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace ble_client { +namespace esphome::ble_client { static const char *const TAG = "ble_sensor"; @@ -147,6 +146,5 @@ void BLESensor::update() { } } -} // namespace ble_client -} // namespace esphome +} // namespace esphome::ble_client #endif diff --git a/esphome/components/ble_client/sensor/ble_sensor.h b/esphome/components/ble_client/sensor/ble_sensor.h index c6335d58368..fe5b5ecd530 100644 --- a/esphome/components/ble_client/sensor/ble_sensor.h +++ b/esphome/components/ble_client/sensor/ble_sensor.h @@ -10,8 +10,7 @@ #ifdef USE_ESP32 #include -namespace esphome { -namespace ble_client { +namespace esphome::ble_client { namespace espbt = esphome::esp32_ble_tracker; @@ -48,6 +47,5 @@ class BLESensor : public sensor::Sensor, public PollingComponent, public BLEClie espbt::ESPBTUUID descr_uuid_; }; -} // namespace ble_client -} // namespace esphome +} // namespace esphome::ble_client #endif diff --git a/esphome/components/ble_client/switch/ble_switch.cpp b/esphome/components/ble_client/switch/ble_switch.cpp index 9d92b1b2b58..5baca2adcf1 100644 --- a/esphome/components/ble_client/switch/ble_switch.cpp +++ b/esphome/components/ble_client/switch/ble_switch.cpp @@ -4,8 +4,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace ble_client { +namespace esphome::ble_client { static const char *const TAG = "ble_switch"; @@ -31,6 +30,5 @@ void BLEClientSwitch::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_i void BLEClientSwitch::dump_config() { LOG_SWITCH("", "BLE Client Switch", this); } -} // namespace ble_client -} // namespace esphome +} // namespace esphome::ble_client #endif diff --git a/esphome/components/ble_client/switch/ble_switch.h b/esphome/components/ble_client/switch/ble_switch.h index 9809f904e75..9be6d06b1c6 100644 --- a/esphome/components/ble_client/switch/ble_switch.h +++ b/esphome/components/ble_client/switch/ble_switch.h @@ -8,8 +8,7 @@ #ifdef USE_ESP32 #include -namespace esphome { -namespace ble_client { +namespace esphome::ble_client { namespace espbt = esphome::esp32_ble_tracker; @@ -24,6 +23,5 @@ class BLEClientSwitch : public switch_::Switch, public Component, public BLEClie void write_state(bool state) override; }; -} // namespace ble_client -} // namespace esphome +} // namespace esphome::ble_client #endif diff --git a/esphome/components/ble_client/text_sensor/automation.h b/esphome/components/ble_client/text_sensor/automation.h index c504c35a58e..f7b077926b8 100644 --- a/esphome/components/ble_client/text_sensor/automation.h +++ b/esphome/components/ble_client/text_sensor/automation.h @@ -5,8 +5,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace ble_client { +namespace esphome::ble_client { class BLETextSensorNotifyTrigger : public Trigger, public BLETextSensor { public: @@ -33,7 +32,6 @@ class BLETextSensorNotifyTrigger : public Trigger, public BLETextSe BLETextSensor *sensor_; }; -} // namespace ble_client -} // namespace esphome +} // namespace esphome::ble_client #endif diff --git a/esphome/components/ble_client/text_sensor/ble_text_sensor.cpp b/esphome/components/ble_client/text_sensor/ble_text_sensor.cpp index bb771aed99e..415981a1ba2 100644 --- a/esphome/components/ble_client/text_sensor/ble_text_sensor.cpp +++ b/esphome/components/ble_client/text_sensor/ble_text_sensor.cpp @@ -7,8 +7,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace ble_client { +namespace esphome::ble_client { static const char *const TAG = "ble_text_sensor"; @@ -138,6 +137,5 @@ void BLETextSensor::update() { } } -} // namespace ble_client -} // namespace esphome +} // namespace esphome::ble_client #endif diff --git a/esphome/components/ble_client/text_sensor/ble_text_sensor.h b/esphome/components/ble_client/text_sensor/ble_text_sensor.h index c75a4df9523..3fbd64389c9 100644 --- a/esphome/components/ble_client/text_sensor/ble_text_sensor.h +++ b/esphome/components/ble_client/text_sensor/ble_text_sensor.h @@ -8,8 +8,7 @@ #ifdef USE_ESP32 #include -namespace esphome { -namespace ble_client { +namespace esphome::ble_client { namespace espbt = esphome::esp32_ble_tracker; @@ -40,6 +39,5 @@ class BLETextSensor : public text_sensor::TextSensor, public PollingComponent, p espbt::ESPBTUUID descr_uuid_; }; -} // namespace ble_client -} // namespace esphome +} // namespace esphome::ble_client #endif From 2174795b273ef3ad26f8606e1e7d2d9a28c41646 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Nov 2025 21:57:36 -0600 Subject: [PATCH 25/57] [number] Reduce NumberCall size by 4 bytes on 32-bit platforms (#12178) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/number/number_call.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/number/number_call.h b/esphome/components/number/number_call.h index 0f6889dcb64..584c13f4131 100644 --- a/esphome/components/number/number_call.h +++ b/esphome/components/number/number_call.h @@ -8,7 +8,7 @@ namespace esphome::number { class Number; -enum NumberOperation { +enum NumberOperation : uint8_t { NUMBER_OP_NONE, NUMBER_OP_SET, NUMBER_OP_INCREMENT, @@ -38,9 +38,9 @@ class NumberCall { float limit); Number *const parent_; - NumberOperation operation_{NUMBER_OP_NONE}; optional value_; - bool cycle_; + NumberOperation operation_{NUMBER_OP_NONE}; + bool cycle_{false}; }; } // namespace esphome::number From b71d8010d25e0252262aff70b14a49857e4a4025 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Nov 2025 21:59:31 -0600 Subject: [PATCH 26/57] [light] Store log_percent parameter strings in flash on ESP8266 (#12174) --- esphome/components/light/light_call.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/esphome/components/light/light_call.cpp b/esphome/components/light/light_call.cpp index b3bdb16c73f..f523b4451ba 100644 --- a/esphome/components/light/light_call.cpp +++ b/esphome/components/light/light_call.cpp @@ -74,11 +74,11 @@ static const LogString *color_mode_to_human(ColorMode color_mode) { // Helper to log percentage values #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG -static void log_percent(const char *name, const char *param, float value) { - ESP_LOGD(TAG, " %s: %.0f%%", param, value * 100.0f); +static void log_percent(const LogString *param, float value) { + ESP_LOGD(TAG, " %s: %.0f%%", LOG_STR_ARG(param), value * 100.0f); } #else -#define log_percent(name, param, value) +#define log_percent(param, value) #endif void LightCall::perform() { @@ -104,11 +104,11 @@ void LightCall::perform() { } if (this->has_brightness()) { - log_percent(name, "Brightness", v.get_brightness()); + log_percent(LOG_STR("Brightness"), v.get_brightness()); } if (this->has_color_brightness()) { - log_percent(name, "Color brightness", v.get_color_brightness()); + log_percent(LOG_STR("Color brightness"), v.get_color_brightness()); } if (this->has_red() || this->has_green() || this->has_blue()) { ESP_LOGD(TAG, " Red: %.0f%%, Green: %.0f%%, Blue: %.0f%%", v.get_red() * 100.0f, v.get_green() * 100.0f, @@ -116,7 +116,7 @@ void LightCall::perform() { } if (this->has_white()) { - log_percent(name, "White", v.get_white()); + log_percent(LOG_STR("White"), v.get_white()); } if (this->has_color_temperature()) { ESP_LOGD(TAG, " Color temperature: %.1f mireds", v.get_color_temperature()); From c40e8e7f5c722cb70e44bf5a3c61eb7a18a55bb9 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Sat, 29 Nov 2025 19:38:29 +1100 Subject: [PATCH 27/57] [helpers] Add conversion from FixedVector to std::vector (#12179) --- esphome/core/helpers.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index a43c55e06b5..83a12b9bf00 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -242,6 +242,9 @@ template class FixedVector { other.reset_(); } + // Allow conversion to std::vector + operator std::vector() const { return {data_, data_ + size_}; } + FixedVector &operator=(FixedVector &&other) noexcept { if (this != &other) { // Delete our current data From cf444fc3b82e68e33715434f39f6b4762f2cf809 Mon Sep 17 00:00:00 2001 From: Javier Peletier Date: Sat, 29 Nov 2025 09:40:13 +0100 Subject: [PATCH 28/57] [mipi_spi] add guition JC4827W543 C/R (#12034) --- esphome/components/mipi_spi/models/jc.py | 105 +++++++++++++++++++++++ 1 file changed, 105 insertions(+) diff --git a/esphome/components/mipi_spi/models/jc.py b/esphome/components/mipi_spi/models/jc.py index 5dbf049dedf..5b936fd956f 100644 --- a/esphome/components/mipi_spi/models/jc.py +++ b/esphome/components/mipi_spi/models/jc.py @@ -484,4 +484,109 @@ DriverChip( ), ) +DriverChip( + "JC4827W543", + height=272, + width=480, + offset_height=0, + offset_width=0, + cs_pin={CONF_NUMBER: 45, CONF_IGNORE_STRAPPING_WARNING: True}, + invert_colors=True, + color_order=MODE_RGB, + bus_mode=TYPE_QUAD, + data_rate="20MHz", + initsequence=( + (0xFF, 0xA5), + (0x41, 0x03), + (0x44, 0x15), + (0x45, 0x15), + (0x7D, 0x03), + (0xC1, 0xBB), + (0xC2, 0x05), + (0xC3, 0x10), + (0xC6, 0x3E), + (0xC7, 0x25), + (0xC8, 0x11), + (0x7A, 0x5F), + (0x6F, 0x44), + (0x78, 0x70), + (0xC9, 0x00), + (0x67, 0x21), + (0x51, 0x0A), + (0x52, 0x76), + (0x53, 0x0A), + (0x54, 0x76), + (0x46, 0x0A), + (0x47, 0x2A), + (0x48, 0x0A), + (0x49, 0x1A), + (0x56, 0x43), + (0x57, 0x42), + (0x58, 0x3C), + (0x59, 0x64), + (0x5A, 0x41), + (0x5B, 0x3C), + (0x5C, 0x02), + (0x5D, 0x3C), + (0x5E, 0x1F), + (0x60, 0x80), + (0x61, 0x3F), + (0x62, 0x21), + (0x63, 0x07), + (0x64, 0xE0), + (0x65, 0x02), + (0xCA, 0x20), + (0xCB, 0x52), + (0xCC, 0x10), + (0xCD, 0x42), + (0xD0, 0x20), + (0xD1, 0x52), + (0xD2, 0x10), + (0xD3, 0x42), + (0xD4, 0x0A), + (0xD5, 0x32), + (0x80, 0x00), + (0xA0, 0x00), + (0x81, 0x07), + (0xA1, 0x06), + (0x82, 0x02), + (0xA2, 0x01), + (0x86, 0x11), + (0xA6, 0x10), + (0x87, 0x27), + (0xA7, 0x27), + (0x83, 0x37), + (0xA3, 0x37), + (0x84, 0x35), + (0xA4, 0x35), + (0x85, 0x3F), + (0xA5, 0x3F), + (0x88, 0x0B), + (0xA8, 0x0B), + (0x89, 0x14), + (0xA9, 0x14), + (0x8A, 0x1A), + (0xAA, 0x1A), + (0x8B, 0x0A), + (0xAB, 0x0A), + (0x8C, 0x14), + (0xAC, 0x08), + (0x8D, 0x17), + (0xAD, 0x07), + (0x8E, 0x16), + (0xAE, 0x06), + (0x8F, 0x1B), + (0xAF, 0x07), + (0x90, 0x04), + (0xB0, 0x04), + (0x91, 0x0A), + (0xB1, 0x0A), + (0x92, 0x16), + (0xB2, 0x15), + (0xFF, 0x00), + (0x11, 0x00), + (0x29, 0x00), + ), +) + models = {} From 1f47797007419e10046513b7eb49d18fa200d1e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20=C4=8Cerm=C3=A1k?= Date: Sat, 29 Nov 2025 23:26:25 +0100 Subject: [PATCH 29/57] Add MEASUREMENT_ANGLE to SensorStateClass (#12085) --- esphome/components/api/api.proto | 1 + esphome/components/api/api_pb2.h | 1 + esphome/components/api/api_pb2_dump.cpp | 2 ++ esphome/components/sensor/__init__.py | 1 + esphome/components/sensor/sensor.cpp | 2 ++ esphome/components/sensor/sensor.h | 1 + esphome/const.py | 3 +++ tests/components/sensor/common.yaml | 7 +++++++ 8 files changed, 18 insertions(+) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 74a8e8ff7f8..5450c2536c5 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -589,6 +589,7 @@ enum SensorStateClass { STATE_CLASS_MEASUREMENT = 1; STATE_CLASS_TOTAL_INCREASING = 2; STATE_CLASS_TOTAL = 3; + STATE_CLASS_MEASUREMENT_ANGLE = 4; } // Deprecated in API version 1.5 diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 93ece74d852..74d3834bf5f 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -51,6 +51,7 @@ enum SensorStateClass : uint32_t { STATE_CLASS_MEASUREMENT = 1, STATE_CLASS_TOTAL_INCREASING = 2, STATE_CLASS_TOTAL = 3, + STATE_CLASS_MEASUREMENT_ANGLE = 4, }; #endif enum LogLevel : uint32_t { diff --git a/esphome/components/api/api_pb2_dump.cpp b/esphome/components/api/api_pb2_dump.cpp index a985e052ac2..bea7fc53c4b 100644 --- a/esphome/components/api/api_pb2_dump.cpp +++ b/esphome/components/api/api_pb2_dump.cpp @@ -179,6 +179,8 @@ template<> const char *proto_enum_to_string(enums::Sens return "STATE_CLASS_TOTAL_INCREASING"; case enums::STATE_CLASS_TOTAL: return "STATE_CLASS_TOTAL"; + case enums::STATE_CLASS_MEASUREMENT_ANGLE: + return "STATE_CLASS_MEASUREMENT_ANGLE"; default: return "UNKNOWN"; } diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index f83226d10fc..027d9a69b83 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -182,6 +182,7 @@ STATE_CLASSES = { "measurement": StateClasses.STATE_CLASS_MEASUREMENT, "total_increasing": StateClasses.STATE_CLASS_TOTAL_INCREASING, "total": StateClasses.STATE_CLASS_TOTAL, + "measurement_angle": StateClasses.STATE_CLASS_MEASUREMENT_ANGLE, } validate_state_class = cv.enum(STATE_CLASSES, lower=True, space="_") diff --git a/esphome/components/sensor/sensor.cpp b/esphome/components/sensor/sensor.cpp index df6bd644e84..49dc56edaa1 100644 --- a/esphome/components/sensor/sensor.cpp +++ b/esphome/components/sensor/sensor.cpp @@ -44,6 +44,8 @@ const LogString *state_class_to_string(StateClass state_class) { return LOG_STR("total_increasing"); case STATE_CLASS_TOTAL: return LOG_STR("total"); + case STATE_CLASS_MEASUREMENT_ANGLE: + return LOG_STR("measurement_angle"); case STATE_CLASS_NONE: default: return LOG_STR(""); diff --git a/esphome/components/sensor/sensor.h b/esphome/components/sensor/sensor.h index a4210e5e6c3..5d387a1ad77 100644 --- a/esphome/components/sensor/sensor.h +++ b/esphome/components/sensor/sensor.h @@ -31,6 +31,7 @@ enum StateClass : uint8_t { STATE_CLASS_MEASUREMENT = 1, STATE_CLASS_TOTAL_INCREASING = 2, STATE_CLASS_TOTAL = 3, + STATE_CLASS_MEASUREMENT_ANGLE = 4 }; const LogString *state_class_to_string(StateClass state_class); diff --git a/esphome/const.py b/esphome/const.py index 2b6b60d395f..59bf0e8b8ad 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1346,6 +1346,9 @@ STATE_CLASS_NONE = "" # The state represents a measurement in present time STATE_CLASS_MEASUREMENT = "measurement" +# The state represents a measurement in present time for angles measured in degrees (°) +STATE_CLASS_MEASUREMENT_ANGLE = "measurement_angle" + # The state represents a total that only increases, a decrease is considered a reset. STATE_CLASS_TOTAL_INCREASING = "total_increasing" diff --git a/tests/components/sensor/common.yaml b/tests/components/sensor/common.yaml index 2180f66da8d..1961c986853 100644 --- a/tests/components/sensor/common.yaml +++ b/tests/components/sensor/common.yaml @@ -236,3 +236,10 @@ sensor: - multiply: 2.0 - offset: 10.0 - lambda: return x * 3.0; + + # Testing measurement_angle state class + - platform: template + name: "Angle Sensor" + lambda: return 42.0; + update_interval: 1s + state_class: "measurement_angle" From 46567c47161a53ea48262c0ffde665ba3224885b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 29 Nov 2025 22:55:27 +0000 Subject: [PATCH 30/57] Bump aioesphomeapi from 42.8.0 to 42.9.0 (#12189) 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 a5c919e95fe..45ae3d5925c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile esptool==5.1.0 click==8.1.7 esphome-dashboard==20251013.0 -aioesphomeapi==42.8.0 +aioesphomeapi==42.9.0 zeroconf==0.148.0 puremagic==1.30 ruamel.yaml==0.18.16 # dashboard_import From ec88bf0cb12be0159a1aa1a8519fd8a770fc1c6d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 29 Nov 2025 22:56:26 +0000 Subject: [PATCH 31/57] Bump ruff from 0.14.5 to 0.14.7 (#12190) 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 b86d00f2aae..412a678d02c 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.14.5 + rev: v0.14.7 hooks: # Run the linter. - id: ruff diff --git a/requirements_test.txt b/requirements_test.txt index 7f6d3f8e268..3aec877126e 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,6 +1,6 @@ pylint==4.0.3 flake8==7.3.0 # also change in .pre-commit-config.yaml when updating -ruff==0.14.5 # also change in .pre-commit-config.yaml when updating +ruff==0.14.7 # 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 d82a92b406238162bbd01756cb301776f901a6d9 Mon Sep 17 00:00:00 2001 From: Darsey Litzenberger Date: Sat, 29 Nov 2025 16:41:47 -0700 Subject: [PATCH 32/57] [ade7953_base] Add missing CODEOWNERS (#12181) --- CODEOWNERS | 1 + esphome/components/ade7953_base/__init__.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/CODEOWNERS b/CODEOWNERS index c6332e39335..78618713235 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -21,6 +21,7 @@ esphome/components/adc128s102/* @DeerMaximum esphome/components/addressable_light/* @justfalter esphome/components/ade7880/* @kpfleming esphome/components/ade7953/* @angelnu +esphome/components/ade7953_base/* @angelnu esphome/components/ade7953_i2c/* @angelnu esphome/components/ade7953_spi/* @angelnu esphome/components/ads1118/* @solomondg1 diff --git a/esphome/components/ade7953_base/__init__.py b/esphome/components/ade7953_base/__init__.py index 42b6c8ba24c..4fc35352f92 100644 --- a/esphome/components/ade7953_base/__init__.py +++ b/esphome/components/ade7953_base/__init__.py @@ -24,6 +24,8 @@ from esphome.const import ( UNIT_WATT, ) +CODEOWNERS = ["@angelnu"] + CONF_CURRENT_A = "current_a" CONF_CURRENT_B = "current_b" CONF_ACTIVE_POWER_A = "active_power_a" From 77f5f2326f793c488dca9a190590d8d7e57e9484 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 29 Nov 2025 18:36:12 -0600 Subject: [PATCH 33/57] [hlk_fm22x] Fix Action::play method signatures (#12192) --- esphome/components/hlk_fm22x/hlk_fm22x.h | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/esphome/components/hlk_fm22x/hlk_fm22x.h b/esphome/components/hlk_fm22x/hlk_fm22x.h index 5ecc715ea17..9c981d3c445 100644 --- a/esphome/components/hlk_fm22x/hlk_fm22x.h +++ b/esphome/components/hlk_fm22x/hlk_fm22x.h @@ -189,7 +189,7 @@ template class EnrollmentAction : public Action, public P TEMPLATABLE_VALUE(std::string, name) TEMPLATABLE_VALUE(uint8_t, direction) - void play(Ts... x) override { + void play(const Ts &...x) override { auto name = this->name_.value(x...); auto direction = (HlkFm22xFaceDirection) this->direction_.value(x...); this->parent_->enroll_face(name, direction); @@ -200,7 +200,7 @@ template class DeleteAction : public Action, public Paren public: TEMPLATABLE_VALUE(int16_t, face_id) - void play(Ts... x) override { + void play(const Ts &...x) override { auto face_id = this->face_id_.value(x...); this->parent_->delete_face(face_id); } @@ -208,17 +208,17 @@ template class DeleteAction : public Action, public Paren template class DeleteAllAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->delete_all_faces(); } + void play(const Ts &...x) override { this->parent_->delete_all_faces(); } }; template class ScanAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->scan_face(); } + void play(const Ts &...x) override { this->parent_->scan_face(); } }; template class ResetAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->reset(); } + void play(const Ts &...x) override { this->parent_->reset(); } }; } // namespace esphome::hlk_fm22x From 042a08887f848e348c47b92055cd5957acf19807 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 29 Nov 2025 18:54:49 -0600 Subject: [PATCH 34/57] [climate] Use C++17 nested namespace syntax (#12194) --- esphome/components/climate/automation.h | 6 ++---- esphome/components/climate/climate.cpp | 6 ++---- esphome/components/climate/climate.h | 6 ++---- esphome/components/climate/climate_mode.cpp | 6 ++---- esphome/components/climate/climate_mode.h | 6 ++---- esphome/components/climate/climate_traits.cpp | 6 ++---- esphome/components/climate/climate_traits.h | 6 ++---- 7 files changed, 14 insertions(+), 28 deletions(-) diff --git a/esphome/components/climate/automation.h b/esphome/components/climate/automation.h index 36cc8f4f21d..fac56d9d9e9 100644 --- a/esphome/components/climate/automation.h +++ b/esphome/components/climate/automation.h @@ -3,8 +3,7 @@ #include "esphome/core/automation.h" #include "climate.h" -namespace esphome { -namespace climate { +namespace esphome::climate { template class ControlAction : public Action { public: @@ -58,5 +57,4 @@ class StateTrigger : public Trigger { } }; -} // namespace climate -} // namespace esphome +} // namespace esphome::climate diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index 82b75660bae..b0fba6aa62f 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -3,8 +3,7 @@ #include "esphome/core/controller_registry.h" #include "esphome/core/macros.h" -namespace esphome { -namespace climate { +namespace esphome::climate { static const char *const TAG = "climate"; @@ -762,5 +761,4 @@ void Climate::dump_traits_(const char *tag) { } } -} // namespace climate -} // namespace esphome +} // namespace esphome::climate diff --git a/esphome/components/climate/climate.h b/esphome/components/climate/climate.h index b277877c3e1..28a73d8c053 100644 --- a/esphome/components/climate/climate.h +++ b/esphome/components/climate/climate.h @@ -8,8 +8,7 @@ #include "climate_mode.h" #include "climate_traits.h" -namespace esphome { -namespace climate { +namespace esphome::climate { #define LOG_CLIMATE(prefix, type, obj) \ if ((obj) != nullptr) { \ @@ -345,5 +344,4 @@ class Climate : public EntityBase { const char *custom_preset_{nullptr}; }; -} // namespace climate -} // namespace esphome +} // namespace esphome::climate diff --git a/esphome/components/climate/climate_mode.cpp b/esphome/components/climate/climate_mode.cpp index 794f45ccd63..b153ee04248 100644 --- a/esphome/components/climate/climate_mode.cpp +++ b/esphome/components/climate/climate_mode.cpp @@ -1,7 +1,6 @@ #include "climate_mode.h" -namespace esphome { -namespace climate { +namespace esphome::climate { const LogString *climate_mode_to_string(ClimateMode mode) { switch (mode) { @@ -107,5 +106,4 @@ const LogString *climate_preset_to_string(ClimatePreset preset) { } } -} // namespace climate -} // namespace esphome +} // namespace esphome::climate diff --git a/esphome/components/climate/climate_mode.h b/esphome/components/climate/climate_mode.h index 44423d2f22b..c961c44248d 100644 --- a/esphome/components/climate/climate_mode.h +++ b/esphome/components/climate/climate_mode.h @@ -3,8 +3,7 @@ #include #include "esphome/core/log.h" -namespace esphome { -namespace climate { +namespace esphome::climate { /// Enum for all modes a climate device can be in. /// NOTE: If adding values, update ClimateModeMask in climate_traits.h to use the new last value @@ -132,5 +131,4 @@ const LogString *climate_swing_mode_to_string(ClimateSwingMode mode); /// Convert the given PresetMode to a human-readable string. const LogString *climate_preset_to_string(ClimatePreset preset); -} // namespace climate -} // namespace esphome +} // namespace esphome::climate diff --git a/esphome/components/climate/climate_traits.cpp b/esphome/components/climate/climate_traits.cpp index 342dffaad6d..9bf2d9acd3a 100644 --- a/esphome/components/climate/climate_traits.cpp +++ b/esphome/components/climate/climate_traits.cpp @@ -1,7 +1,6 @@ #include "climate_traits.h" -namespace esphome { -namespace climate { +namespace esphome::climate { int8_t ClimateTraits::get_target_temperature_accuracy_decimals() const { return step_to_accuracy_decimals(this->visual_target_temperature_step_); @@ -11,5 +10,4 @@ int8_t ClimateTraits::get_current_temperature_accuracy_decimals() const { return step_to_accuracy_decimals(this->visual_current_temperature_step_); } -} // namespace climate -} // namespace esphome +} // namespace esphome::climate diff --git a/esphome/components/climate/climate_traits.h b/esphome/components/climate/climate_traits.h index 0eecf9789fb..d3582934752 100644 --- a/esphome/components/climate/climate_traits.h +++ b/esphome/components/climate/climate_traits.h @@ -6,8 +6,7 @@ #include "esphome/core/finite_set_mask.h" #include "esphome/core/helpers.h" -namespace esphome { -namespace climate { +namespace esphome::climate { // Type aliases for climate enum bitmasks // These replace std::set to eliminate red-black tree overhead @@ -292,5 +291,4 @@ class ClimateTraits { std::vector supported_custom_presets_; }; -} // namespace climate -} // namespace esphome +} // namespace esphome::climate From 7317bf4a5d0adbda61fd7078b00e35348a276ed1 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Sun, 30 Nov 2025 08:04:19 -0500 Subject: [PATCH 35/57] [esp32_can] Add P4 support (#12201) --- esphome/components/esp32_can/canbus.py | 3 +++ esphome/components/esp32_can/esp32_can.cpp | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/esphome/components/esp32_can/canbus.py b/esphome/components/esp32_can/canbus.py index dfa98b2effa..acc3785f22b 100644 --- a/esphome/components/esp32_can/canbus.py +++ b/esphome/components/esp32_can/canbus.py @@ -10,6 +10,7 @@ from esphome.components.esp32.const import ( VARIANT_ESP32C3, VARIANT_ESP32C6, VARIANT_ESP32H2, + VARIANT_ESP32P4, VARIANT_ESP32S2, VARIANT_ESP32S3, ) @@ -59,6 +60,7 @@ CAN_SPEEDS_ESP32_S3 = {**CAN_SPEEDS_ESP32_S2} CAN_SPEEDS_ESP32_C3 = {**CAN_SPEEDS_ESP32_S2} CAN_SPEEDS_ESP32_C6 = {**CAN_SPEEDS_ESP32_S2} CAN_SPEEDS_ESP32_H2 = {**CAN_SPEEDS_ESP32_S2} +CAN_SPEEDS_ESP32_P4 = {**CAN_SPEEDS_ESP32_S2} CAN_SPEEDS = { VARIANT_ESP32: CAN_SPEEDS_ESP32, @@ -67,6 +69,7 @@ CAN_SPEEDS = { VARIANT_ESP32C3: CAN_SPEEDS_ESP32_C3, VARIANT_ESP32C6: CAN_SPEEDS_ESP32_C6, VARIANT_ESP32H2: CAN_SPEEDS_ESP32_H2, + VARIANT_ESP32P4: CAN_SPEEDS_ESP32_P4, } diff --git a/esphome/components/esp32_can/esp32_can.cpp b/esphome/components/esp32_can/esp32_can.cpp index cdef7b19308..f9b63b8ebc2 100644 --- a/esphome/components/esp32_can/esp32_can.cpp +++ b/esphome/components/esp32_can/esp32_can.cpp @@ -17,7 +17,7 @@ static const char *const TAG = "esp32_can"; static bool get_bitrate(canbus::CanSpeed bitrate, twai_timing_config_t *t_config) { switch (bitrate) { #if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32C3) || \ - defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32H2) + defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32H2) || defined(USE_ESP32_VARIANT_ESP32P4) case canbus::CAN_1KBPS: *t_config = (twai_timing_config_t) TWAI_TIMING_CONFIG_1KBITS(); return true; From e95ceafc175bc60fde8edc672cd2bb686934ec7b Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Sun, 30 Nov 2025 08:04:33 -0500 Subject: [PATCH 36/57] [mopeka_pro_check] Fix negative temperatures (#12198) Co-authored-by: Claude --- esphome/components/mopeka_pro_check/mopeka_pro_check.cpp | 4 ++-- esphome/components/mopeka_pro_check/mopeka_pro_check.h | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/mopeka_pro_check/mopeka_pro_check.cpp b/esphome/components/mopeka_pro_check/mopeka_pro_check.cpp index 9527f09f599..42d61f81a38 100644 --- a/esphome/components/mopeka_pro_check/mopeka_pro_check.cpp +++ b/esphome/components/mopeka_pro_check/mopeka_pro_check.cpp @@ -116,7 +116,7 @@ bool MopekaProCheck::parse_device(const esp32_ble_tracker::ESPBTDevice &device) // Get temperature of sensor if (this->temperature_ != nullptr) { - uint8_t temp_in_c = this->parse_temperature_(manu_data.data); + int8_t temp_in_c = this->parse_temperature_(manu_data.data); this->temperature_->publish_state(temp_in_c); } @@ -145,7 +145,7 @@ uint32_t MopekaProCheck::parse_distance_(const std::vector &message) { (MOPEKA_LPG_COEF[0] + MOPEKA_LPG_COEF[1] * raw_t + MOPEKA_LPG_COEF[2] * raw_t * raw_t)); } -uint8_t MopekaProCheck::parse_temperature_(const std::vector &message) { return (message[2] & 0x7F) - 40; } +int8_t MopekaProCheck::parse_temperature_(const std::vector &message) { return (message[2] & 0x7F) - 40; } SensorReadQuality MopekaProCheck::parse_read_quality_(const std::vector &message) { // Since a 8 bit value is being shifted and truncated to 2 bits all possible values are defined as enumeration diff --git a/esphome/components/mopeka_pro_check/mopeka_pro_check.h b/esphome/components/mopeka_pro_check/mopeka_pro_check.h index 4cbe8f2afef..41fb3121529 100644 --- a/esphome/components/mopeka_pro_check/mopeka_pro_check.h +++ b/esphome/components/mopeka_pro_check/mopeka_pro_check.h @@ -61,7 +61,7 @@ class MopekaProCheck : public Component, public esp32_ble_tracker::ESPBTDeviceLi uint8_t parse_battery_level_(const std::vector &message); uint32_t parse_distance_(const std::vector &message); - uint8_t parse_temperature_(const std::vector &message); + int8_t parse_temperature_(const std::vector &message); SensorReadQuality parse_read_quality_(const std::vector &message); }; From 47c767fa5e0a139e9b7634c6dd36102b9afa0331 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Sun, 30 Nov 2025 08:04:45 -0500 Subject: [PATCH 37/57] [openthread] Add C5 support (#12200) --- esphome/components/openthread/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/components/openthread/__init__.py b/esphome/components/openthread/__init__.py index e3ad3ed76ce..5b1abe4fb5e 100644 --- a/esphome/components/openthread/__init__.py +++ b/esphome/components/openthread/__init__.py @@ -1,5 +1,6 @@ import esphome.codegen as cg from esphome.components.esp32 import ( + VARIANT_ESP32C5, VARIANT_ESP32C6, VARIANT_ESP32H2, add_idf_sdkconfig_option, @@ -152,7 +153,7 @@ CONFIG_SCHEMA = cv.All( ).extend(_CONNECTION_SCHEMA), cv.has_exactly_one_key(CONF_NETWORK_KEY, CONF_TLV), cv.only_with_esp_idf, - only_on_variant(supported=[VARIANT_ESP32C6, VARIANT_ESP32H2]), + only_on_variant(supported=[VARIANT_ESP32C5, VARIANT_ESP32C6, VARIANT_ESP32H2]), _validate, _require_vfs_select, ) From 8308bc29111c47e83117e405887b9e5e6f51da99 Mon Sep 17 00:00:00 2001 From: Jimmy Hedman Date: Sun, 30 Nov 2025 14:06:06 +0100 Subject: [PATCH 38/57] [mdns] Bump mDNS component to 1.9.1 (#12207) --- esphome/components/mdns/__init__.py | 2 +- esphome/idf_component.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/mdns/__init__.py b/esphome/components/mdns/__init__.py index 4776bef22f2..1daac93a2ed 100644 --- a/esphome/components/mdns/__init__.py +++ b/esphome/components/mdns/__init__.py @@ -165,7 +165,7 @@ async def to_code(config): cg.add_library("LEAmDNS", None) if CORE.using_esp_idf: - add_idf_component(name="espressif/mdns", ref="1.8.2") + add_idf_component(name="espressif/mdns", ref="1.9.1") cg.add_define("USE_MDNS") diff --git a/esphome/idf_component.yml b/esphome/idf_component.yml index fcb3a4f4382..b27b6b8ed18 100644 --- a/esphome/idf_component.yml +++ b/esphome/idf_component.yml @@ -4,7 +4,7 @@ dependencies: espressif/esp32-camera: version: 2.1.1 espressif/mdns: - version: 1.8.2 + version: 1.9.1 espressif/esp_wifi_remote: version: 1.1.5 rules: From 82e12383302f28af748a499ee2e79664e43254c3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 30 Nov 2025 17:09:02 -0600 Subject: [PATCH 39/57] [lock] Refactor trigger classes to template and add integration tests (#12193) --- esphome/components/lock/automation.h | 18 ++---- .../fixtures/lock_automations.yaml | 17 ++++++ tests/integration/test_lock_automations.py | 58 +++++++++++++++++++ 3 files changed, 80 insertions(+), 13 deletions(-) create mode 100644 tests/integration/fixtures/lock_automations.yaml create mode 100644 tests/integration/test_lock_automations.py diff --git a/esphome/components/lock/automation.h b/esphome/components/lock/automation.h index cba2c3fdda6..011c6cc6afc 100644 --- a/esphome/components/lock/automation.h +++ b/esphome/components/lock/automation.h @@ -49,26 +49,18 @@ template class LockCondition : public Condition { bool state_; }; -class LockLockTrigger : public Trigger<> { +template class LockStateTrigger : public Trigger<> { public: - LockLockTrigger(Lock *a_lock) { + explicit LockStateTrigger(Lock *a_lock) { a_lock->add_on_state_callback([this, a_lock]() { - if (a_lock->state == LockState::LOCK_STATE_LOCKED) { + if (a_lock->state == State) { this->trigger(); } }); } }; -class LockUnlockTrigger : public Trigger<> { - public: - LockUnlockTrigger(Lock *a_lock) { - a_lock->add_on_state_callback([this, a_lock]() { - if (a_lock->state == LockState::LOCK_STATE_UNLOCKED) { - this->trigger(); - } - }); - } -}; +using LockLockTrigger = LockStateTrigger; +using LockUnlockTrigger = LockStateTrigger; } // namespace esphome::lock diff --git a/tests/integration/fixtures/lock_automations.yaml b/tests/integration/fixtures/lock_automations.yaml new file mode 100644 index 00000000000..fe11e656faf --- /dev/null +++ b/tests/integration/fixtures/lock_automations.yaml @@ -0,0 +1,17 @@ +esphome: + name: lock-automations-test + +host: +api: # Port will be automatically injected +logger: + level: DEBUG + +lock: + - platform: template + id: test_lock + name: "Test Lock" + optimistic: true + on_lock: + - logger.log: "TRIGGER: on_lock fired" + on_unlock: + - logger.log: "TRIGGER: on_unlock fired" diff --git a/tests/integration/test_lock_automations.py b/tests/integration/test_lock_automations.py new file mode 100644 index 00000000000..e200a2eacdf --- /dev/null +++ b/tests/integration/test_lock_automations.py @@ -0,0 +1,58 @@ +"""Integration test for lock automation triggers. + +Tests that on_lock and on_unlock triggers work correctly. +""" + +import asyncio + +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_lock_automations( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test lock on_lock and on_unlock triggers.""" + loop = asyncio.get_running_loop() + + # Futures for log line detection + on_lock_future: asyncio.Future[bool] = loop.create_future() + on_unlock_future: asyncio.Future[bool] = loop.create_future() + + def check_output(line: str) -> None: + """Check log output for trigger messages.""" + if "TRIGGER: on_lock fired" in line and not on_lock_future.done(): + on_lock_future.set_result(True) + elif "TRIGGER: on_unlock fired" in line and not on_unlock_future.done(): + on_unlock_future.set_result(True) + + async with ( + run_compiled(yaml_config, line_callback=check_output), + api_client_connected() as client, + ): + # Import here to avoid import errors when aioesphomeapi is not installed + from aioesphomeapi import LockCommand + + # Get entities + entities = await client.list_entities_services() + lock = next(e for e in entities[0] if e.object_id == "test_lock") + + # Test 1: Lock - should trigger on_lock + client.lock_command(key=lock.key, command=LockCommand.LOCK) + + try: + await asyncio.wait_for(on_lock_future, timeout=5.0) + except TimeoutError: + pytest.fail("on_lock trigger did not fire") + + # Test 2: Unlock - should trigger on_unlock + client.lock_command(key=lock.key, command=LockCommand.UNLOCK) + + try: + await asyncio.wait_for(on_unlock_future, timeout=5.0) + except TimeoutError: + pytest.fail("on_unlock trigger did not fire") From 2ca118f3718f18d76f07d43c7c7437953b5bb819 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 30 Nov 2025 17:25:46 -0600 Subject: [PATCH 40/57] [web_server] Replace routing table with if-else chain to save 116 bytes RAM (#12139) --- esphome/components/web_server/web_server.cpp | 105 ++++++++++++------- 1 file changed, 67 insertions(+), 38 deletions(-) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index f5ca6741610..bc48793ba2f 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -1686,6 +1686,7 @@ std::string WebServer::event_state_json_generator(WebServer *web_server, void *s 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) { auto *event = static_cast(source); return web_server->event_json(event, get_event_type(event), DETAIL_ALL); @@ -1709,6 +1710,7 @@ std::string WebServer::event_json(event::Event *obj, const std::string &event_ty return builder.serialize(); } +// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) #endif #ifdef USE_UPDATE @@ -1945,83 +1947,110 @@ void WebServer::handleRequest(AsyncWebServerRequest *request) { // Parse URL for component routing UrlMatch match = match_url(url.c_str(), url.length(), false); - // Component routing using minimal code repetition - struct ComponentRoute { - const char *domain; - void (WebServer::*handler)(AsyncWebServerRequest *, const UrlMatch &); - }; - - static const ComponentRoute ROUTES[] = { + // Route to appropriate handler based on domain + // NOLINTNEXTLINE(readability-simplify-boolean-expr) + if (false) { // Start chain for else-if macro pattern + } #ifdef USE_SENSOR - {"sensor", &WebServer::handle_sensor_request}, + else if (match.domain_equals("sensor")) { + this->handle_sensor_request(request, match); + } #endif #ifdef USE_SWITCH - {"switch", &WebServer::handle_switch_request}, + else if (match.domain_equals("switch")) { + this->handle_switch_request(request, match); + } #endif #ifdef USE_BUTTON - {"button", &WebServer::handle_button_request}, + else if (match.domain_equals("button")) { + this->handle_button_request(request, match); + } #endif #ifdef USE_BINARY_SENSOR - {"binary_sensor", &WebServer::handle_binary_sensor_request}, + else if (match.domain_equals("binary_sensor")) { + this->handle_binary_sensor_request(request, match); + } #endif #ifdef USE_FAN - {"fan", &WebServer::handle_fan_request}, + else if (match.domain_equals("fan")) { + this->handle_fan_request(request, match); + } #endif #ifdef USE_LIGHT - {"light", &WebServer::handle_light_request}, + else if (match.domain_equals("light")) { + this->handle_light_request(request, match); + } #endif #ifdef USE_TEXT_SENSOR - {"text_sensor", &WebServer::handle_text_sensor_request}, + else if (match.domain_equals("text_sensor")) { + this->handle_text_sensor_request(request, match); + } #endif #ifdef USE_COVER - {"cover", &WebServer::handle_cover_request}, + else if (match.domain_equals("cover")) { + this->handle_cover_request(request, match); + } #endif #ifdef USE_NUMBER - {"number", &WebServer::handle_number_request}, + else if (match.domain_equals("number")) { + this->handle_number_request(request, match); + } #endif #ifdef USE_DATETIME_DATE - {"date", &WebServer::handle_date_request}, + else if (match.domain_equals("date")) { + this->handle_date_request(request, match); + } #endif #ifdef USE_DATETIME_TIME - {"time", &WebServer::handle_time_request}, + else if (match.domain_equals("time")) { + this->handle_time_request(request, match); + } #endif #ifdef USE_DATETIME_DATETIME - {"datetime", &WebServer::handle_datetime_request}, + else if (match.domain_equals("datetime")) { + this->handle_datetime_request(request, match); + } #endif #ifdef USE_TEXT - {"text", &WebServer::handle_text_request}, + else if (match.domain_equals("text")) { + this->handle_text_request(request, match); + } #endif #ifdef USE_SELECT - {"select", &WebServer::handle_select_request}, + else if (match.domain_equals("select")) { + this->handle_select_request(request, match); + } #endif #ifdef USE_CLIMATE - {"climate", &WebServer::handle_climate_request}, + else if (match.domain_equals("climate")) { + this->handle_climate_request(request, match); + } #endif #ifdef USE_LOCK - {"lock", &WebServer::handle_lock_request}, + else if (match.domain_equals("lock")) { + this->handle_lock_request(request, match); + } #endif #ifdef USE_VALVE - {"valve", &WebServer::handle_valve_request}, + else if (match.domain_equals("valve")) { + this->handle_valve_request(request, match); + } #endif #ifdef USE_ALARM_CONTROL_PANEL - {"alarm_control_panel", &WebServer::handle_alarm_control_panel_request}, + else if (match.domain_equals("alarm_control_panel")) { + this->handle_alarm_control_panel_request(request, match); + } #endif #ifdef USE_UPDATE - {"update", &WebServer::handle_update_request}, + else if (match.domain_equals("update")) { + this->handle_update_request(request, match); + } #endif - }; - - // Check each route - for (const auto &route : ROUTES) { - if (match.domain_equals(route.domain)) { - (this->*route.handler)(request, match); - return; - } + else { + // No matching handler found - send 404 + ESP_LOGV(TAG, "Request for unknown URL: %s", url.c_str()); + request->send(404, "text/plain", "Not Found"); } - - // No matching handler found - send 404 - ESP_LOGV(TAG, "Request for unknown URL: %s", url.c_str()); - request->send(404, "text/plain", "Not Found"); } bool WebServer::isRequestHandlerTrivial() const { return false; } From bf4ef36c3a7875716ff6a4301e2e6eeb415ce62c Mon Sep 17 00:00:00 2001 From: Darsey Litzenberger Date: Sun, 30 Nov 2025 17:17:50 -0700 Subject: [PATCH 41/57] [ade7953] Apply voltage_gain setting to both channels (#12180) --- esphome/components/ade7953_base/ade7953_base.cpp | 13 ++++++++----- esphome/components/ade7953_base/ade7953_base.h | 10 ++++++++-- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/esphome/components/ade7953_base/ade7953_base.cpp b/esphome/components/ade7953_base/ade7953_base.cpp index 5f5fdd27ee9..821e4a3105e 100644 --- a/esphome/components/ade7953_base/ade7953_base.cpp +++ b/esphome/components/ade7953_base/ade7953_base.cpp @@ -25,7 +25,8 @@ void ADE7953::setup() { this->ade_write_8(PGA_V_8, pga_v_); this->ade_write_8(PGA_IA_8, pga_ia_); this->ade_write_8(PGA_IB_8, pga_ib_); - this->ade_write_32(AVGAIN_32, vgain_); + this->ade_write_32(AVGAIN_32, avgain_); + this->ade_write_32(BVGAIN_32, bvgain_); this->ade_write_32(AIGAIN_32, aigain_); this->ade_write_32(BIGAIN_32, bigain_); this->ade_write_32(AWGAIN_32, awgain_); @@ -34,7 +35,8 @@ void ADE7953::setup() { this->ade_read_8(PGA_V_8, &pga_v_); this->ade_read_8(PGA_IA_8, &pga_ia_); this->ade_read_8(PGA_IB_8, &pga_ib_); - this->ade_read_32(AVGAIN_32, &vgain_); + this->ade_read_32(AVGAIN_32, &avgain_); + this->ade_read_32(BVGAIN_32, &bvgain_); this->ade_read_32(AIGAIN_32, &aigain_); this->ade_read_32(BIGAIN_32, &bigain_); this->ade_read_32(AWGAIN_32, &awgain_); @@ -63,13 +65,14 @@ void ADE7953::dump_config() { " PGA_V_8: 0x%X\n" " PGA_IA_8: 0x%X\n" " PGA_IB_8: 0x%X\n" - " VGAIN_32: 0x%08jX\n" + " AVGAIN_32: 0x%08jX\n" + " BVGAIN_32: 0x%08jX\n" " AIGAIN_32: 0x%08jX\n" " BIGAIN_32: 0x%08jX\n" " AWGAIN_32: 0x%08jX\n" " BWGAIN_32: 0x%08jX", - this->use_acc_energy_regs_, pga_v_, pga_ia_, pga_ib_, (uintmax_t) vgain_, (uintmax_t) aigain_, - (uintmax_t) bigain_, (uintmax_t) awgain_, (uintmax_t) bwgain_); + this->use_acc_energy_regs_, pga_v_, pga_ia_, pga_ib_, (uintmax_t) avgain_, (uintmax_t) bvgain_, + (uintmax_t) aigain_, (uintmax_t) bigain_, (uintmax_t) awgain_, (uintmax_t) bwgain_); } #define ADE_PUBLISH_(name, val, factor) \ diff --git a/esphome/components/ade7953_base/ade7953_base.h b/esphome/components/ade7953_base/ade7953_base.h index d711a5c6bea..bcafddca4eb 100644 --- a/esphome/components/ade7953_base/ade7953_base.h +++ b/esphome/components/ade7953_base/ade7953_base.h @@ -46,7 +46,12 @@ class ADE7953 : public PollingComponent, public sensor::Sensor { void set_pga_ib(uint8_t pga_ib) { pga_ib_ = pga_ib; } // Set input gains - void set_vgain(uint32_t vgain) { vgain_ = vgain; } + void set_vgain(uint32_t vgain) { + // Datasheet says: "to avoid discrepancies in other registers, + // if AVGAIN is set then BVGAIN should be set to the same value." + avgain_ = vgain; + bvgain_ = vgain; + } void set_aigain(uint32_t aigain) { aigain_ = aigain; } void set_bigain(uint32_t bigain) { bigain_ = bigain; } void set_awgain(uint32_t awgain) { awgain_ = awgain; } @@ -100,7 +105,8 @@ class ADE7953 : public PollingComponent, public sensor::Sensor { uint8_t pga_v_; uint8_t pga_ia_; uint8_t pga_ib_; - uint32_t vgain_; + uint32_t avgain_; + uint32_t bvgain_; uint32_t aigain_; uint32_t bigain_; uint32_t awgain_; From 4335fcdb72cddb820b75e6ba20dc41b01739cce2 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Sun, 30 Nov 2025 23:27:10 -0500 Subject: [PATCH 42/57] [psram] Add C5 support (#12215) Co-authored-by: Claude --- esphome/components/psram/__init__.py | 3 +++ tests/component_tests/psram/test_psram.py | 7 ++++--- tests/components/psram/test.esp32-c5-idf.yaml | 8 ++++++++ .../build_components_base.esp32-c5-idf.yaml | 17 +++++++++++++++++ 4 files changed, 32 insertions(+), 3 deletions(-) create mode 100644 tests/components/psram/test.esp32-c5-idf.yaml create mode 100644 tests/test_build_components/build_components_base.esp32-c5-idf.yaml diff --git a/esphome/components/psram/__init__.py b/esphome/components/psram/__init__.py index c50c5998550..4ee4e97696c 100644 --- a/esphome/components/psram/__init__.py +++ b/esphome/components/psram/__init__.py @@ -11,6 +11,7 @@ from esphome.components.esp32 import ( get_esp32_variant, ) from esphome.components.esp32.const import ( + VARIANT_ESP32C5, VARIANT_ESP32P4, VARIANT_ESP32S2, VARIANT_ESP32S3, @@ -55,6 +56,7 @@ SPIRAM_MODES = { VARIANT_ESP32: (TYPE_QUAD,), VARIANT_ESP32S2: (TYPE_QUAD,), VARIANT_ESP32S3: (TYPE_QUAD, TYPE_OCTAL), + VARIANT_ESP32C5: (TYPE_QUAD,), VARIANT_ESP32P4: (TYPE_HEX,), } @@ -63,6 +65,7 @@ SPIRAM_SPEEDS = { VARIANT_ESP32: (40, 80, 120), VARIANT_ESP32S2: (40, 80, 120), VARIANT_ESP32S3: (40, 80, 120), + VARIANT_ESP32C5: (40, 80, 120), VARIANT_ESP32P4: (20, 100, 200), } diff --git a/tests/component_tests/psram/test_psram.py b/tests/component_tests/psram/test_psram.py index f8ad013689b..86bc29cc843 100644 --- a/tests/component_tests/psram/test_psram.py +++ b/tests/component_tests/psram/test_psram.py @@ -23,22 +23,23 @@ from tests.component_tests.types import SetCoreConfigCallable UNSUPPORTED_PSRAM_VARIANTS = [ VARIANT_ESP32C2, VARIANT_ESP32C3, - VARIANT_ESP32C5, VARIANT_ESP32C6, VARIANT_ESP32H2, ] SUPPORTED_PSRAM_VARIANTS = [ VARIANT_ESP32, + VARIANT_ESP32C5, + VARIANT_ESP32P4, VARIANT_ESP32S2, VARIANT_ESP32S3, - VARIANT_ESP32P4, ] SUPPORTED_PSRAM_MODES = { VARIANT_ESP32: ["quad"], + VARIANT_ESP32C5: ["quad"], + VARIANT_ESP32P4: ["hex"], VARIANT_ESP32S2: ["quad"], VARIANT_ESP32S3: ["quad", "octal"], - VARIANT_ESP32P4: ["hex"], } diff --git a/tests/components/psram/test.esp32-c5-idf.yaml b/tests/components/psram/test.esp32-c5-idf.yaml new file mode 100644 index 00000000000..fbd0132e2dd --- /dev/null +++ b/tests/components/psram/test.esp32-c5-idf.yaml @@ -0,0 +1,8 @@ +esp32: + cpu_frequency: 240MHz + framework: + type: esp-idf + +psram: + speed: 120MHz + ignore_not_found: false diff --git a/tests/test_build_components/build_components_base.esp32-c5-idf.yaml b/tests/test_build_components/build_components_base.esp32-c5-idf.yaml new file mode 100644 index 00000000000..6468297e9ad --- /dev/null +++ b/tests/test_build_components/build_components_base.esp32-c5-idf.yaml @@ -0,0 +1,17 @@ +esphome: + name: componenttestesp32c5idf + friendly_name: $component_name + +esp32: + board: esp32-c5-devkitc-1 + framework: + type: esp-idf + +logger: + level: VERY_VERBOSE + +packages: + component_under_test: !include + file: $component_test_file + vars: + component_test_file: $component_test_file From 161a18b3269032e95c883dc67bf0928a77b7581e Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Mon, 1 Dec 2025 00:33:23 -0600 Subject: [PATCH 43/57] [uart] Add `wake_loop_on_rx` flag for low latency processing (#12172) Co-authored-by: J. Nick Koston --- esphome/components/uart/__init__.py | 39 ++++++++++- .../uart/uart_component_esp_idf.cpp | 70 ++++++++++++++++++- .../components/uart/uart_component_esp_idf.h | 8 +++ esphome/core/defines.h | 1 + 4 files changed, 116 insertions(+), 2 deletions(-) diff --git a/esphome/components/uart/__init__.py b/esphome/components/uart/__init__.py index 7b0d9726b84..a1c78dd45c9 100644 --- a/esphome/components/uart/__init__.py +++ b/esphome/components/uart/__init__.py @@ -1,3 +1,4 @@ +from dataclasses import dataclass from logging import getLogger import math import re @@ -32,13 +33,15 @@ from esphome.const import ( PLATFORM_HOST, PlatformFramework, ) -from esphome.core import CORE, ID +from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority import esphome.final_validate as fv from esphome.yaml_util import make_data_base _LOGGER = getLogger(__name__) CODEOWNERS = ["@esphome/core"] +DOMAIN = "uart" + uart_ns = cg.esphome_ns.namespace("uart") UARTComponent = uart_ns.class_("UARTComponent") @@ -52,6 +55,7 @@ LibreTinyUARTComponent = uart_ns.class_( ) HostUartComponent = uart_ns.class_("HostUartComponent", UARTComponent, cg.Component) + NATIVE_UART_CLASSES = ( str(IDFUARTComponent), str(ESP8266UartComponent), @@ -100,6 +104,30 @@ MULTI_CONF = True MULTI_CONF_NO_DEFAULT = True +@dataclass +class UARTData: + """State data for UART component configuration generation.""" + + wake_loop_on_rx: bool = False + + +def _get_data() -> UARTData: + """Get UART component data from CORE.data.""" + if DOMAIN not in CORE.data: + CORE.data[DOMAIN] = UARTData() + return CORE.data[DOMAIN] + + +def request_wake_loop_on_rx() -> None: + """Request that the UART wake the main loop when data is received. + + Components that need low-latency notification of incoming UART data + should call this function during their code generation. + This enables the RX event task which wakes the main loop when data arrives. + """ + _get_data().wake_loop_on_rx = True + + def validate_raw_data(value): if isinstance(value, str): return value.encode("utf-8") @@ -335,6 +363,8 @@ async def to_code(config): if CONF_DEBUG in config: await debug_to_code(config[CONF_DEBUG], var) + CORE.add_job(final_step) + # A schema to use for all UART devices, all UART integrations must extend this! UART_DEVICE_SCHEMA = cv.Schema( @@ -472,6 +502,13 @@ async def uart_write_to_code(config, action_id, template_arg, args): return var +@coroutine_with_priority(CoroPriority.FINAL) +async def final_step(): + """Final code generation step to configure optional UART features.""" + if _get_data().wake_loop_on_rx: + cg.add_define("USE_UART_WAKE_LOOP_ON_RX") + + FILTER_SOURCE_FILES = filter_source_files_from_platform( { "uart_component_esp_idf.cpp": { diff --git a/esphome/components/uart/uart_component_esp_idf.cpp b/esphome/components/uart/uart_component_esp_idf.cpp index 61ca8c1c0ce..c6efe862c4c 100644 --- a/esphome/components/uart/uart_component_esp_idf.cpp +++ b/esphome/components/uart/uart_component_esp_idf.cpp @@ -112,6 +112,12 @@ void IDFUARTComponent::load_settings(bool dump_config) { esp_err_t err; if (uart_is_driver_installed(this->uart_num_)) { +#ifdef USE_UART_WAKE_LOOP_ON_RX + if (this->rx_event_task_handle_ != nullptr) { + vTaskDelete(this->rx_event_task_handle_); + this->rx_event_task_handle_ = nullptr; + } +#endif err = uart_driver_delete(this->uart_num_); if (err != ESP_OK) { ESP_LOGW(TAG, "uart_driver_delete failed: %s", esp_err_to_name(err)); @@ -204,6 +210,11 @@ void IDFUARTComponent::load_settings(bool dump_config) { return; } +#ifdef USE_UART_WAKE_LOOP_ON_RX + // Start the RX event task to enable low-latency data notifications + this->start_rx_event_task_(); +#endif // USE_UART_WAKE_LOOP_ON_RX + if (dump_config) { ESP_LOGCONFIG(TAG, "Reloaded UART %u", this->uart_num_); this->dump_config(); @@ -226,7 +237,11 @@ void IDFUARTComponent::dump_config() { " Baud Rate: %" PRIu32 " baud\n" " Data Bits: %u\n" " Parity: %s\n" - " Stop bits: %u", + " Stop bits: %u" +#ifdef USE_UART_WAKE_LOOP_ON_RX + "\n Wake on data RX: ENABLED" +#endif + , this->baud_rate_, this->data_bits_, LOG_STR_ARG(parity_to_str(this->parity_)), this->stop_bits_); this->check_logger_conflict(); } @@ -337,6 +352,59 @@ void IDFUARTComponent::flush() { void IDFUARTComponent::check_logger_conflict() {} +#ifdef USE_UART_WAKE_LOOP_ON_RX +void IDFUARTComponent::start_rx_event_task_() { + // Create FreeRTOS task to monitor UART events + BaseType_t result = xTaskCreate(rx_event_task_func, // Task function + "uart_rx_evt", // Task name (max 16 chars) + 2240, // Stack size in bytes (~2.2KB); increase if needed for logging + this, // Task parameter (this pointer) + tskIDLE_PRIORITY + 1, // Priority (low, just above idle) + &this->rx_event_task_handle_ // Task handle + ); + + if (result != pdPASS) { + ESP_LOGE(TAG, "Failed to create RX event task"); + return; + } + + ESP_LOGV(TAG, "RX event task started"); +} + +void IDFUARTComponent::rx_event_task_func(void *param) { + auto *self = static_cast(param); + uart_event_t event; + + ESP_LOGV(TAG, "RX event task running"); + + // Run forever - task lifecycle matches component lifecycle + while (true) { + // Wait for UART events (blocks efficiently) + if (xQueueReceive(self->uart_event_queue_, &event, portMAX_DELAY) == pdTRUE) { + switch (event.type) { + case UART_DATA: + // Data available in UART RX buffer - wake the main loop + ESP_LOGVV(TAG, "Data event: %d bytes", event.size); + App.wake_loop_threadsafe(); + break; + + case UART_FIFO_OVF: + case UART_BUFFER_FULL: + ESP_LOGW(TAG, "FIFO overflow or ring buffer full - clearing"); + uart_flush_input(self->uart_num_); + App.wake_loop_threadsafe(); + break; + + default: + // Ignore other event types + ESP_LOGVV(TAG, "Event type: %d", event.type); + break; + } + } + } +} +#endif // USE_UART_WAKE_LOOP_ON_RX + } // namespace uart } // namespace esphome diff --git a/esphome/components/uart/uart_component_esp_idf.h b/esphome/components/uart/uart_component_esp_idf.h index a2ba2aa9682..e47a323979c 100644 --- a/esphome/components/uart/uart_component_esp_idf.h +++ b/esphome/components/uart/uart_component_esp_idf.h @@ -53,6 +53,14 @@ class IDFUARTComponent : public UARTComponent, public Component { bool has_peek_{false}; uint8_t peek_byte_; + +#ifdef USE_UART_WAKE_LOOP_ON_RX + // RX notification support + void start_rx_event_task_(); + static void rx_event_task_func(void *param); + + TaskHandle_t rx_event_task_handle_{nullptr}; +#endif // USE_UART_WAKE_LOOP_ON_RX }; } // namespace uart diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 538d4e3d6e5..12dfdba5cee 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -107,6 +107,7 @@ #define USE_TIME #define USE_TOUCHSCREEN #define USE_UART_DEBUGGER +#define USE_UART_WAKE_LOOP_ON_RX #define USE_UPDATE #define USE_VALVE #define USE_ZWAVE_PROXY From dbc16ce46884dd54e5f62a8f37d15394eb6b8613 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Mon, 1 Dec 2025 02:48:47 -0600 Subject: [PATCH 44/57] [wifi_info] Fix compilation error when using only mac_address sensor, add tests (#12222) --- esphome/components/wifi_info/wifi_info_text_sensor.cpp | 4 ++++ esphome/components/wifi_info/wifi_info_text_sensor.h | 2 ++ tests/components/wifi_info/common-mac.yaml | 8 ++++++++ tests/components/wifi_info/common.yaml | 2 +- tests/components/wifi_info/test-mac.esp32-idf.yaml | 4 ++++ tests/components/wifi_info/test-mac.esp8266-ard.yaml | 4 ++++ tests/components/wifi_info/test-mac.rp2040-ard.yaml | 4 ++++ 7 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 tests/components/wifi_info/common-mac.yaml create mode 100644 tests/components/wifi_info/test-mac.esp32-idf.yaml create mode 100644 tests/components/wifi_info/test-mac.esp8266-ard.yaml create mode 100644 tests/components/wifi_info/test-mac.rp2040-ard.yaml diff --git a/esphome/components/wifi_info/wifi_info_text_sensor.cpp b/esphome/components/wifi_info/wifi_info_text_sensor.cpp index 92d3ea29f59..6c9d0c00e57 100644 --- a/esphome/components/wifi_info/wifi_info_text_sensor.cpp +++ b/esphome/components/wifi_info/wifi_info_text_sensor.cpp @@ -6,6 +6,8 @@ namespace esphome::wifi_info { static const char *const TAG = "wifi_info"; +#ifdef USE_WIFI_LISTENERS + static constexpr size_t MAX_STATE_LENGTH = 255; /******************** @@ -98,6 +100,8 @@ void BSSIDWiFiInfo::on_wifi_connect_state(const std::string &ssid, const wifi::b this->publish_state(buf); } +#endif + /********************* * MacAddressWifiInfo ********************/ diff --git a/esphome/components/wifi_info/wifi_info_text_sensor.h b/esphome/components/wifi_info/wifi_info_text_sensor.h index 74d951f9226..f1f85c114fa 100644 --- a/esphome/components/wifi_info/wifi_info_text_sensor.h +++ b/esphome/components/wifi_info/wifi_info_text_sensor.h @@ -9,6 +9,7 @@ namespace esphome::wifi_info { +#ifdef USE_WIFI_LISTENERS class IPAddressWiFiInfo final : public Component, public text_sensor::TextSensor, public wifi::WiFiIPStateListener { public: void setup() override; @@ -62,6 +63,7 @@ class BSSIDWiFiInfo final : public Component, public text_sensor::TextSensor, pu // WiFiConnectStateListener interface void on_wifi_connect_state(const std::string &ssid, const wifi::bssid_t &bssid) override; }; +#endif class MacAddressWifiInfo final : public Component, public text_sensor::TextSensor { public: diff --git a/tests/components/wifi_info/common-mac.yaml b/tests/components/wifi_info/common-mac.yaml new file mode 100644 index 00000000000..3571cd08c41 --- /dev/null +++ b/tests/components/wifi_info/common-mac.yaml @@ -0,0 +1,8 @@ +wifi: + ssid: MySSID + password: password1 + +text_sensor: + - platform: wifi_info + mac_address: + name: MAC Address diff --git a/tests/components/wifi_info/common.yaml b/tests/components/wifi_info/common.yaml index cf5ea563ba4..f87d381d0cf 100644 --- a/tests/components/wifi_info/common.yaml +++ b/tests/components/wifi_info/common.yaml @@ -13,6 +13,6 @@ text_sensor: bssid: name: BSSID mac_address: - name: Mac Address + name: MAC Address dns_address: name: DNS ADdress diff --git a/tests/components/wifi_info/test-mac.esp32-idf.yaml b/tests/components/wifi_info/test-mac.esp32-idf.yaml new file mode 100644 index 00000000000..9d561ca2ce0 --- /dev/null +++ b/tests/components/wifi_info/test-mac.esp32-idf.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml + +<<: !include common-mac.yaml diff --git a/tests/components/wifi_info/test-mac.esp8266-ard.yaml b/tests/components/wifi_info/test-mac.esp8266-ard.yaml new file mode 100644 index 00000000000..05f6344fb68 --- /dev/null +++ b/tests/components/wifi_info/test-mac.esp8266-ard.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml + +<<: !include common-mac.yaml diff --git a/tests/components/wifi_info/test-mac.rp2040-ard.yaml b/tests/components/wifi_info/test-mac.rp2040-ard.yaml new file mode 100644 index 00000000000..d2d54def9de --- /dev/null +++ b/tests/components/wifi_info/test-mac.rp2040-ard.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml + +<<: !include common-mac.yaml From 664881bc130f070d84cc88f59ed9a2baf7dc2654 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Mon, 1 Dec 2025 06:57:18 -0600 Subject: [PATCH 45/57] [uart] Convert to C++17 namespace style (#12220) --- esphome/components/uart/automation.h | 6 ++---- esphome/components/uart/button/uart_button.cpp | 6 ++---- esphome/components/uart/button/uart_button.h | 6 ++---- .../components/uart/packet_transport/uart_transport.cpp | 7 +++---- esphome/components/uart/packet_transport/uart_transport.h | 6 ++---- esphome/components/uart/switch/uart_switch.cpp | 6 ++---- esphome/components/uart/switch/uart_switch.h | 6 ++---- esphome/components/uart/uart.cpp | 6 ++---- esphome/components/uart/uart.h | 6 ++---- esphome/components/uart/uart_component.cpp | 6 ++---- esphome/components/uart/uart_component.h | 6 ++---- esphome/components/uart/uart_component_esp8266.cpp | 6 ++---- esphome/components/uart/uart_component_esp8266.h | 7 ++----- esphome/components/uart/uart_component_esp_idf.cpp | 8 +++----- esphome/components/uart/uart_component_esp_idf.h | 7 ++----- esphome/components/uart/uart_component_host.cpp | 7 ++----- esphome/components/uart/uart_component_host.h | 7 ++----- esphome/components/uart/uart_component_libretiny.cpp | 7 ++----- esphome/components/uart/uart_component_libretiny.h | 7 ++----- esphome/components/uart/uart_component_rp2040.cpp | 7 ++----- esphome/components/uart/uart_component_rp2040.h | 7 ++----- esphome/components/uart/uart_debugger.cpp | 6 ++---- esphome/components/uart/uart_debugger.h | 6 ++---- 23 files changed, 48 insertions(+), 101 deletions(-) diff --git a/esphome/components/uart/automation.h b/esphome/components/uart/automation.h index c2eb308eb8d..c99caac97b2 100644 --- a/esphome/components/uart/automation.h +++ b/esphome/components/uart/automation.h @@ -5,8 +5,7 @@ #include -namespace esphome { -namespace uart { +namespace esphome::uart { template class UARTWriteAction : public Action, public Parented { public: @@ -41,5 +40,4 @@ template class UARTWriteAction : public Action, public Pa } code_; }; -} // namespace uart -} // namespace esphome +} // namespace esphome::uart diff --git a/esphome/components/uart/button/uart_button.cpp b/esphome/components/uart/button/uart_button.cpp index dd228b9bb75..809ceaabb03 100644 --- a/esphome/components/uart/button/uart_button.cpp +++ b/esphome/components/uart/button/uart_button.cpp @@ -1,8 +1,7 @@ #include "uart_button.h" #include "esphome/core/log.h" -namespace esphome { -namespace uart { +namespace esphome::uart { static const char *const TAG = "uart.button"; @@ -13,5 +12,4 @@ void UARTButton::press_action() { void UARTButton::dump_config() { LOG_BUTTON("", "UART Button", this); } -} // namespace uart -} // namespace esphome +} // namespace esphome::uart diff --git a/esphome/components/uart/button/uart_button.h b/esphome/components/uart/button/uart_button.h index 8c7d762a05f..2b530d3c4bc 100644 --- a/esphome/components/uart/button/uart_button.h +++ b/esphome/components/uart/button/uart_button.h @@ -6,8 +6,7 @@ #include -namespace esphome { -namespace uart { +namespace esphome::uart { class UARTButton : public button::Button, public UARTDevice, public Component { public: @@ -21,5 +20,4 @@ class UARTButton : public button::Button, public UARTDevice, public Component { std::vector data_; }; -} // namespace uart -} // namespace esphome +} // namespace esphome::uart diff --git a/esphome/components/uart/packet_transport/uart_transport.cpp b/esphome/components/uart/packet_transport/uart_transport.cpp index 423b657532b..4a9aa0fe47a 100644 --- a/esphome/components/uart/packet_transport/uart_transport.cpp +++ b/esphome/components/uart/packet_transport/uart_transport.cpp @@ -2,8 +2,7 @@ #include "esphome/core/application.h" #include "uart_transport.h" -namespace esphome { -namespace uart { +namespace esphome::uart { static const char *const TAG = "uart_transport"; @@ -84,5 +83,5 @@ void UARTTransport::send_packet(const std::vector &buf) const { this->write_byte_(crc >> 8); this->parent_->write_byte(FLAG_BYTE); } -} // namespace uart -} // namespace esphome + +} // namespace esphome::uart diff --git a/esphome/components/uart/packet_transport/uart_transport.h b/esphome/components/uart/packet_transport/uart_transport.h index f1431e948c6..e84bed95e63 100644 --- a/esphome/components/uart/packet_transport/uart_transport.h +++ b/esphome/components/uart/packet_transport/uart_transport.h @@ -5,8 +5,7 @@ #include #include "../uart.h" -namespace esphome { -namespace uart { +namespace esphome::uart { /** * A transport protocol for sending and receiving packets over a UART connection. @@ -37,5 +36,4 @@ class UARTTransport : public packet_transport::PacketTransport, public UARTDevic bool rx_control_{}; }; -} // namespace uart -} // namespace esphome +} // namespace esphome::uart diff --git a/esphome/components/uart/switch/uart_switch.cpp b/esphome/components/uart/switch/uart_switch.cpp index 4f5ff9fc99b..642bd19772f 100644 --- a/esphome/components/uart/switch/uart_switch.cpp +++ b/esphome/components/uart/switch/uart_switch.cpp @@ -2,8 +2,7 @@ #include "esphome/core/log.h" #include "esphome/core/application.h" -namespace esphome { -namespace uart { +namespace esphome::uart { static const char *const TAG = "uart.switch"; @@ -58,5 +57,4 @@ void UARTSwitch::dump_config() { } } -} // namespace uart -} // namespace esphome +} // namespace esphome::uart diff --git a/esphome/components/uart/switch/uart_switch.h b/esphome/components/uart/switch/uart_switch.h index 909307d57e5..5730fc9b4b2 100644 --- a/esphome/components/uart/switch/uart_switch.h +++ b/esphome/components/uart/switch/uart_switch.h @@ -7,8 +7,7 @@ #include #include -namespace esphome { -namespace uart { +namespace esphome::uart { class UARTSwitch : public switch_::Switch, public UARTDevice, public Component { public: @@ -33,5 +32,4 @@ class UARTSwitch : public switch_::Switch, public UARTDevice, public Component { uint32_t last_transmission_; }; -} // namespace uart -} // namespace esphome +} // namespace esphome::uart diff --git a/esphome/components/uart/uart.cpp b/esphome/components/uart/uart.cpp index b18454bf9df..6cfd6537a59 100644 --- a/esphome/components/uart/uart.cpp +++ b/esphome/components/uart/uart.cpp @@ -5,8 +5,7 @@ #include "esphome/core/log.h" #include -namespace esphome { -namespace uart { +namespace esphome::uart { static const char *const TAG = "uart"; @@ -43,5 +42,4 @@ const LogString *parity_to_str(UARTParityOptions parity) { } } -} // namespace uart -} // namespace esphome +} // namespace esphome::uart diff --git a/esphome/components/uart/uart.h b/esphome/components/uart/uart.h index e2912db122b..72c282f1c4f 100644 --- a/esphome/components/uart/uart.h +++ b/esphome/components/uart/uart.h @@ -6,8 +6,7 @@ #include "esphome/core/log.h" #include "uart_component.h" -namespace esphome { -namespace uart { +namespace esphome::uart { class UARTDevice { public: @@ -74,5 +73,4 @@ class UARTDevice { UARTComponent *parent_{nullptr}; }; -} // namespace uart -} // namespace esphome +} // namespace esphome::uart diff --git a/esphome/components/uart/uart_component.cpp b/esphome/components/uart/uart_component.cpp index 8f670275d43..30fc208fc91 100644 --- a/esphome/components/uart/uart_component.cpp +++ b/esphome/components/uart/uart_component.cpp @@ -1,7 +1,6 @@ #include "uart_component.h" -namespace esphome { -namespace uart { +namespace esphome::uart { static const char *const TAG = "uart"; @@ -28,5 +27,4 @@ void UARTComponent::set_rx_full_threshold_ms(uint8_t time) { this->set_rx_full_threshold(val); } -} // namespace uart -} // namespace esphome +} // namespace esphome::uart diff --git a/esphome/components/uart/uart_component.h b/esphome/components/uart/uart_component.h index 452688b3e97..fd528e228f8 100644 --- a/esphome/components/uart/uart_component.h +++ b/esphome/components/uart/uart_component.h @@ -11,8 +11,7 @@ #include "esphome/core/automation.h" #endif -namespace esphome { -namespace uart { +namespace esphome::uart { enum UARTParityOptions { UART_CONFIG_PARITY_NONE, @@ -199,5 +198,4 @@ class UARTComponent { #endif }; -} // namespace uart -} // namespace esphome +} // namespace esphome::uart diff --git a/esphome/components/uart/uart_component_esp8266.cpp b/esphome/components/uart/uart_component_esp8266.cpp index c84a877ef41..c78daa74627 100644 --- a/esphome/components/uart/uart_component_esp8266.cpp +++ b/esphome/components/uart/uart_component_esp8266.cpp @@ -9,8 +9,7 @@ #include "esphome/components/logger/logger.h" #endif -namespace esphome { -namespace uart { +namespace esphome::uart { static const char *const TAG = "uart.arduino_esp8266"; bool ESP8266UartComponent::serial0_in_use = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) @@ -331,6 +330,5 @@ int ESP8266SoftwareSerial::available() { return avail; } -} // namespace uart -} // namespace esphome +} // namespace esphome::uart #endif // USE_ESP8266 diff --git a/esphome/components/uart/uart_component_esp8266.h b/esphome/components/uart/uart_component_esp8266.h index 749dd4c61ef..e33dd00644a 100644 --- a/esphome/components/uart/uart_component_esp8266.h +++ b/esphome/components/uart/uart_component_esp8266.h @@ -9,8 +9,7 @@ #include "esphome/core/log.h" #include "uart_component.h" -namespace esphome { -namespace uart { +namespace esphome::uart { class ESP8266SoftwareSerial { public: @@ -88,7 +87,5 @@ class ESP8266UartComponent : public UARTComponent, public Component { static bool serial0_in_use; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) }; -} // namespace uart -} // namespace esphome - +} // namespace esphome::uart #endif // USE_ESP8266 diff --git a/esphome/components/uart/uart_component_esp_idf.cpp b/esphome/components/uart/uart_component_esp_idf.cpp index c6efe862c4c..b438e4f7a60 100644 --- a/esphome/components/uart/uart_component_esp_idf.cpp +++ b/esphome/components/uart/uart_component_esp_idf.cpp @@ -14,8 +14,8 @@ #include "esphome/components/logger/logger.h" #endif -namespace esphome { -namespace uart { +namespace esphome::uart { + static const char *const TAG = "uart.idf"; uart_config_t IDFUARTComponent::get_config_() { @@ -405,7 +405,5 @@ void IDFUARTComponent::rx_event_task_func(void *param) { } #endif // USE_UART_WAKE_LOOP_ON_RX -} // namespace uart -} // namespace esphome - +} // namespace esphome::uart #endif // USE_ESP32 diff --git a/esphome/components/uart/uart_component_esp_idf.h b/esphome/components/uart/uart_component_esp_idf.h index e47a323979c..bd6d0c792e6 100644 --- a/esphome/components/uart/uart_component_esp_idf.h +++ b/esphome/components/uart/uart_component_esp_idf.h @@ -6,8 +6,7 @@ #include "esphome/core/component.h" #include "uart_component.h" -namespace esphome { -namespace uart { +namespace esphome::uart { class IDFUARTComponent : public UARTComponent, public Component { public: @@ -63,7 +62,5 @@ class IDFUARTComponent : public UARTComponent, public Component { #endif // USE_UART_WAKE_LOOP_ON_RX }; -} // namespace uart -} // namespace esphome - +} // namespace esphome::uart #endif // USE_ESP32 diff --git a/esphome/components/uart/uart_component_host.cpp b/esphome/components/uart/uart_component_host.cpp index adb11266c57..69b24607d18 100644 --- a/esphome/components/uart/uart_component_host.cpp +++ b/esphome/components/uart/uart_component_host.cpp @@ -96,8 +96,7 @@ speed_t get_baud(int baud) { } // namespace -namespace esphome { -namespace uart { +namespace esphome::uart { static const char *const TAG = "uart.host"; @@ -296,7 +295,5 @@ void HostUartComponent::update_error_(const std::string &error) { ESP_LOGE(TAG, "Port error: %s", error.c_str()); } -} // namespace uart -} // namespace esphome - +} // namespace esphome::uart #endif // USE_HOST diff --git a/esphome/components/uart/uart_component_host.h b/esphome/components/uart/uart_component_host.h index c1f1dd0d2c7..a4a6946c0c2 100644 --- a/esphome/components/uart/uart_component_host.h +++ b/esphome/components/uart/uart_component_host.h @@ -6,8 +6,7 @@ #include "esphome/core/log.h" #include "uart_component.h" -namespace esphome { -namespace uart { +namespace esphome::uart { class HostUartComponent : public UARTComponent, public Component { public: @@ -32,7 +31,5 @@ class HostUartComponent : public UARTComponent, public Component { uint8_t peek_byte_; }; -} // namespace uart -} // namespace esphome - +} // namespace esphome::uart #endif // USE_HOST diff --git a/esphome/components/uart/uart_component_libretiny.cpp b/esphome/components/uart/uart_component_libretiny.cpp index 1e408b169bc..01c7063fe86 100644 --- a/esphome/components/uart/uart_component_libretiny.cpp +++ b/esphome/components/uart/uart_component_libretiny.cpp @@ -14,8 +14,7 @@ #include #endif -namespace esphome { -namespace uart { +namespace esphome::uart { static const char *const TAG = "uart.lt"; @@ -187,7 +186,5 @@ void LibreTinyUARTComponent::check_logger_conflict() { #endif } -} // namespace uart -} // namespace esphome - +} // namespace esphome::uart #endif // USE_LIBRETINY diff --git a/esphome/components/uart/uart_component_libretiny.h b/esphome/components/uart/uart_component_libretiny.h index 00982fd2979..ec13e7da5a3 100644 --- a/esphome/components/uart/uart_component_libretiny.h +++ b/esphome/components/uart/uart_component_libretiny.h @@ -8,8 +8,7 @@ #include "esphome/core/log.h" #include "uart_component.h" -namespace esphome { -namespace uart { +namespace esphome::uart { class LibreTinyUARTComponent : public UARTComponent, public Component { public: @@ -37,7 +36,5 @@ class LibreTinyUARTComponent : public UARTComponent, public Component { int8_t hardware_idx_{-1}; }; -} // namespace uart -} // namespace esphome - +} // namespace esphome::uart #endif // USE_LIBRETINY diff --git a/esphome/components/uart/uart_component_rp2040.cpp b/esphome/components/uart/uart_component_rp2040.cpp index cd3905b5c17..5799d26a54a 100644 --- a/esphome/components/uart/uart_component_rp2040.cpp +++ b/esphome/components/uart/uart_component_rp2040.cpp @@ -11,8 +11,7 @@ #include "esphome/components/logger/logger.h" #endif -namespace esphome { -namespace uart { +namespace esphome::uart { static const char *const TAG = "uart.arduino_rp2040"; @@ -193,7 +192,5 @@ void RP2040UartComponent::flush() { this->serial_->flush(); } -} // namespace uart -} // namespace esphome - +} // namespace esphome::uart #endif // USE_RP2040 diff --git a/esphome/components/uart/uart_component_rp2040.h b/esphome/components/uart/uart_component_rp2040.h index f26c913cffd..d626d11a2e4 100644 --- a/esphome/components/uart/uart_component_rp2040.h +++ b/esphome/components/uart/uart_component_rp2040.h @@ -11,8 +11,7 @@ #include "esphome/core/log.h" #include "uart_component.h" -namespace esphome { -namespace uart { +namespace esphome::uart { class RP2040UartComponent : public UARTComponent, public Component { public: @@ -40,7 +39,5 @@ class RP2040UartComponent : public UARTComponent, public Component { HardwareSerial *serial_{nullptr}; }; -} // namespace uart -} // namespace esphome - +} // namespace esphome::uart #endif // USE_RP2040 diff --git a/esphome/components/uart/uart_debugger.cpp b/esphome/components/uart/uart_debugger.cpp index e2d92eac60d..b51a57d68ee 100644 --- a/esphome/components/uart/uart_debugger.cpp +++ b/esphome/components/uart/uart_debugger.cpp @@ -6,8 +6,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace uart { +namespace esphome::uart { static const char *const TAG = "uart_debug"; @@ -197,6 +196,5 @@ void UARTDebug::log_binary(UARTDirection direction, std::vector bytes, delay(10); } -} // namespace uart -} // namespace esphome +} // namespace esphome::uart #endif diff --git a/esphome/components/uart/uart_debugger.h b/esphome/components/uart/uart_debugger.h index 4f9b6d09df3..df876559622 100644 --- a/esphome/components/uart/uart_debugger.h +++ b/esphome/components/uart/uart_debugger.h @@ -8,8 +8,7 @@ #include "uart.h" #include "uart_component.h" -namespace esphome { -namespace uart { +namespace esphome::uart { /// The UARTDebugger class adds debugging support to a UART bus. /// @@ -96,6 +95,5 @@ class UARTDebug { static void log_binary(UARTDirection direction, std::vector bytes, uint8_t separator); }; -} // namespace uart -} // namespace esphome +} // namespace esphome::uart #endif From 065c1bfc6a438230d10d4595a79570c1bb414f96 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 1 Dec 2025 08:34:07 -0600 Subject: [PATCH 46/57] [core] Fix status_momentary API misuse and optimize parameter type (#12216) --- .../components/demo/demo_alarm_control_panel.h | 4 ++-- esphome/components/es8388/es8388.cpp | 4 ++-- esphome/components/esphome/ota/ota_esphome.cpp | 2 +- .../micro_wake_word/micro_wake_word.cpp | 5 ++--- esphome/components/sound_level/sound_level.cpp | 4 ++-- esphome/core/component.cpp | 4 ++-- esphome/core/component.h | 18 ++++++++++++++++-- 7 files changed, 27 insertions(+), 14 deletions(-) diff --git a/esphome/components/demo/demo_alarm_control_panel.h b/esphome/components/demo/demo_alarm_control_panel.h index 9902d27882e..f59434830b1 100644 --- a/esphome/components/demo/demo_alarm_control_panel.h +++ b/esphome/components/demo/demo_alarm_control_panel.h @@ -33,7 +33,7 @@ class DemoAlarmControlPanel : public AlarmControlPanel, public Component { case ACP_STATE_ARMED_AWAY: if (this->get_requires_code_to_arm() && call.get_code().has_value()) { if (call.get_code().value() != "1234") { - this->status_momentary_error("Invalid code", 5000); + this->status_momentary_error("invalid_code", 5000); return; } } @@ -42,7 +42,7 @@ class DemoAlarmControlPanel : public AlarmControlPanel, public Component { case ACP_STATE_DISARMED: if (this->get_requires_code() && call.get_code().has_value()) { if (call.get_code().value() != "1234") { - this->status_momentary_error("Invalid code", 5000); + this->status_momentary_error("invalid_code", 5000); return; } } diff --git a/esphome/components/es8388/es8388.cpp b/esphome/components/es8388/es8388.cpp index 69c16a9615b..5abe7a5e5f2 100644 --- a/esphome/components/es8388/es8388.cpp +++ b/esphome/components/es8388/es8388.cpp @@ -225,7 +225,7 @@ bool ES8388::set_dac_output(DacOutputLine line) { optional ES8388::get_dac_power() { uint8_t dac_power; if (!this->read_byte(ES8388_DACPOWER, &dac_power)) { - this->status_momentary_warning("Failed to read ES8388_DACPOWER"); + this->status_momentary_warning("dacpower_read"); return {}; } switch (dac_power) { @@ -268,7 +268,7 @@ bool ES8388::set_adc_input_mic(AdcInputMicLine line) { optional ES8388::get_mic_input() { uint8_t mic_input; if (!this->read_byte(ES8388_ADCCONTROL2, &mic_input)) { - this->status_momentary_warning("Failed to read ES8388_ADCCONTROL2"); + this->status_momentary_warning("adccontrol2_read"); return {}; } switch (mic_input) { diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index eb6c61a69be..852a50cc228 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -402,7 +402,7 @@ error: this->backend_->abort(); } - this->status_momentary_error("onerror", 5000); + this->status_momentary_error("err", 5000); #ifdef USE_OTA_STATE_CALLBACK this->state_callback_.call(ota::OTA_ERROR, 0.0f, static_cast(error_code)); #endif diff --git a/esphome/components/micro_wake_word/micro_wake_word.cpp b/esphome/components/micro_wake_word/micro_wake_word.cpp index a0547b158ef..ec8fa34da4a 100644 --- a/esphome/components/micro_wake_word/micro_wake_word.cpp +++ b/esphome/components/micro_wake_word/micro_wake_word.cpp @@ -298,8 +298,7 @@ void MicroWakeWord::loop() { // uses floating point operations. if (!FrontendPopulateState(&this->frontend_config_, &this->frontend_state_, this->microphone_source_->get_audio_stream_info().get_sample_rate())) { - this->status_momentary_error( - "Failed to allocate buffers for spectrogram feature processor, attempting again in 1 second", 1000); + this->status_momentary_error("frontend_alloc", 1000); return; } @@ -308,7 +307,7 @@ void MicroWakeWord::loop() { if (this->inference_task_handle_ == nullptr) { FrontendFreeStateContents(&this->frontend_state_); // Deallocate frontend state - this->status_momentary_error("Task failed to start, attempting again in 1 second", 1000); + this->status_momentary_error("task_start", 1000); } } break; diff --git a/esphome/components/sound_level/sound_level.cpp b/esphome/components/sound_level/sound_level.cpp index db6b168bbc7..2719172409d 100644 --- a/esphome/components/sound_level/sound_level.cpp +++ b/esphome/components/sound_level/sound_level.cpp @@ -167,7 +167,7 @@ bool SoundLevelComponent::start_() { this->audio_buffer_ = audio::AudioSourceTransferBuffer::create( this->microphone_source_->get_audio_stream_info().ms_to_bytes(AUDIO_BUFFER_DURATION_MS)); if (this->audio_buffer_ == nullptr) { - this->status_momentary_error("Failed to allocate transfer buffer", 15000); + this->status_momentary_error("transfer_buffer", 15000); return false; } @@ -176,7 +176,7 @@ bool SoundLevelComponent::start_() { std::shared_ptr temp_ring_buffer = RingBuffer::create(this->microphone_source_->get_audio_stream_info().ms_to_bytes(RING_BUFFER_DURATION_MS)); if (temp_ring_buffer.use_count() == 0) { - this->status_momentary_error("Failed to allocate ring buffer", 15000); + this->status_momentary_error("ring_buffer", 15000); this->stop_(); return false; } else { diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index 5e6ace8873a..b7c0cedb76f 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -369,11 +369,11 @@ void Component::status_clear_error() { this->component_state_ &= ~STATUS_LED_ERROR; ESP_LOGE(TAG, "%s cleared Error flag", LOG_STR_ARG(this->get_component_log_str())); } -void Component::status_momentary_warning(const std::string &name, uint32_t length) { +void Component::status_momentary_warning(const char *name, uint32_t length) { this->status_set_warning(); this->set_timeout(name, length, [this]() { this->status_clear_warning(); }); } -void Component::status_momentary_error(const std::string &name, uint32_t length) { +void Component::status_momentary_error(const char *name, uint32_t length) { this->status_set_error(); this->set_timeout(name, length, [this]() { this->status_clear_error(); }); } diff --git a/esphome/core/component.h b/esphome/core/component.h index 51a9290e8bc..3d45a020c4e 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -241,9 +241,23 @@ class Component { void status_clear_error(); - void status_momentary_warning(const std::string &name, uint32_t length = 5000); + /** Set warning status flag and automatically clear it after a timeout. + * + * @param name Identifier for the timeout (used to cancel/replace existing timeouts with the same name). + * Must be a static string literal (stored in flash/rodata), not a temporary or dynamic string. + * This is NOT a message to display - use status_set_warning() with a message if logging is needed. + * @param length Duration in milliseconds before the warning is automatically cleared. + */ + void status_momentary_warning(const char *name, uint32_t length = 5000); - void status_momentary_error(const std::string &name, uint32_t length = 5000); + /** Set error status flag and automatically clear it after a timeout. + * + * @param name Identifier for the timeout (used to cancel/replace existing timeouts with the same name). + * Must be a static string literal (stored in flash/rodata), not a temporary or dynamic string. + * This is NOT a message to display - use status_set_error() with a message if logging is needed. + * @param length Duration in milliseconds before the error is automatically cleared. + */ + void status_momentary_error(const char *name, uint32_t length = 5000); bool has_overridden_loop() const; From b322622ef17a08fe40b8d2b84ef57c9c3766982f Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Mon, 1 Dec 2025 16:47:00 +0100 Subject: [PATCH 47/57] [micronova] Convert to C++17 namespace style (#12229) --- esphome/components/micronova/button/micronova_button.cpp | 6 ++---- esphome/components/micronova/button/micronova_button.h | 6 ++---- esphome/components/micronova/micronova.cpp | 6 ++---- esphome/components/micronova/micronova.h | 6 ++---- esphome/components/micronova/number/micronova_number.cpp | 6 ++---- esphome/components/micronova/number/micronova_number.h | 6 ++---- esphome/components/micronova/sensor/micronova_sensor.cpp | 6 ++---- esphome/components/micronova/sensor/micronova_sensor.h | 6 ++---- esphome/components/micronova/switch/micronova_switch.cpp | 6 ++---- esphome/components/micronova/switch/micronova_switch.h | 6 ++---- .../micronova/text_sensor/micronova_text_sensor.cpp | 6 ++---- .../micronova/text_sensor/micronova_text_sensor.h | 6 ++---- 12 files changed, 24 insertions(+), 48 deletions(-) diff --git a/esphome/components/micronova/button/micronova_button.cpp b/esphome/components/micronova/button/micronova_button.cpp index c1903fd8781..147fef37bd6 100644 --- a/esphome/components/micronova/button/micronova_button.cpp +++ b/esphome/components/micronova/button/micronova_button.cpp @@ -1,7 +1,6 @@ #include "micronova_button.h" -namespace esphome { -namespace micronova { +namespace esphome::micronova { void MicroNovaButton::press_action() { switch (this->get_function()) { @@ -14,5 +13,4 @@ void MicroNovaButton::press_action() { this->micronova_->update(); } -} // namespace micronova -} // namespace esphome +} // namespace esphome::micronova diff --git a/esphome/components/micronova/button/micronova_button.h b/esphome/components/micronova/button/micronova_button.h index 77649051d69..5c1d7d8455a 100644 --- a/esphome/components/micronova/button/micronova_button.h +++ b/esphome/components/micronova/button/micronova_button.h @@ -4,8 +4,7 @@ #include "esphome/core/component.h" #include "esphome/components/button/button.h" -namespace esphome { -namespace micronova { +namespace esphome::micronova { class MicroNovaButton : public Component, public button::Button, public MicroNovaButtonListener { public: @@ -19,5 +18,4 @@ class MicroNovaButton : public Component, public button::Button, public MicroNov void press_action() override; }; -} // namespace micronova -} // namespace esphome +} // namespace esphome::micronova diff --git a/esphome/components/micronova/micronova.cpp b/esphome/components/micronova/micronova.cpp index b96798ed127..52b719bff22 100644 --- a/esphome/components/micronova/micronova.cpp +++ b/esphome/components/micronova/micronova.cpp @@ -1,8 +1,7 @@ #include "micronova.h" #include "esphome/core/log.h" -namespace esphome { -namespace micronova { +namespace esphome::micronova { void MicroNova::setup() { if (this->enable_rx_pin_ != nullptr) { @@ -144,5 +143,4 @@ void MicroNova::write_address(uint8_t location, uint8_t address, uint8_t data) { } } -} // namespace micronova -} // namespace esphome +} // namespace esphome::micronova diff --git a/esphome/components/micronova/micronova.h b/esphome/components/micronova/micronova.h index fc68d941cfe..acb85fad3ce 100644 --- a/esphome/components/micronova/micronova.h +++ b/esphome/components/micronova/micronova.h @@ -8,8 +8,7 @@ #include -namespace esphome { -namespace micronova { +namespace esphome::micronova { static const char *const TAG = "micronova"; static const int STOVE_REPLY_DELAY = 60; @@ -160,5 +159,4 @@ class MicroNova : public PollingComponent, public uart::UARTDevice { MicroNovaSwitchListener *stove_switch_{nullptr}; }; -} // namespace micronova -} // namespace esphome +} // namespace esphome::micronova diff --git a/esphome/components/micronova/number/micronova_number.cpp b/esphome/components/micronova/number/micronova_number.cpp index 244eb7ee9f6..b311c85b99d 100644 --- a/esphome/components/micronova/number/micronova_number.cpp +++ b/esphome/components/micronova/number/micronova_number.cpp @@ -1,7 +1,6 @@ #include "micronova_number.h" -namespace esphome { -namespace micronova { +namespace esphome::micronova { void MicroNovaNumber::process_value_from_stove(int value_from_stove) { float new_sensor_value = 0; @@ -41,5 +40,4 @@ void MicroNovaNumber::control(float value) { this->micronova_->update(); } -} // namespace micronova -} // namespace esphome +} // namespace esphome::micronova diff --git a/esphome/components/micronova/number/micronova_number.h b/esphome/components/micronova/number/micronova_number.h index 49c63582553..79e59dbc28c 100644 --- a/esphome/components/micronova/number/micronova_number.h +++ b/esphome/components/micronova/number/micronova_number.h @@ -3,8 +3,7 @@ #include "esphome/components/micronova/micronova.h" #include "esphome/components/number/number.h" -namespace esphome { -namespace micronova { +namespace esphome::micronova { class MicroNovaNumber : public number::Number, public MicroNovaSensorListener { public: @@ -24,5 +23,4 @@ class MicroNovaNumber : public number::Number, public MicroNovaSensorListener { uint8_t memory_write_location_ = 0; }; -} // namespace micronova -} // namespace esphome +} // namespace esphome::micronova diff --git a/esphome/components/micronova/sensor/micronova_sensor.cpp b/esphome/components/micronova/sensor/micronova_sensor.cpp index 3f0c0feaf86..9fd8832f294 100644 --- a/esphome/components/micronova/sensor/micronova_sensor.cpp +++ b/esphome/components/micronova/sensor/micronova_sensor.cpp @@ -1,7 +1,6 @@ #include "micronova_sensor.h" -namespace esphome { -namespace micronova { +namespace esphome::micronova { void MicroNovaSensor::process_value_from_stove(int value_from_stove) { if (value_from_stove == -1) { @@ -31,5 +30,4 @@ void MicroNovaSensor::process_value_from_stove(int value_from_stove) { this->publish_state(new_sensor_value); } -} // namespace micronova -} // namespace esphome +} // namespace esphome::micronova diff --git a/esphome/components/micronova/sensor/micronova_sensor.h b/esphome/components/micronova/sensor/micronova_sensor.h index 9d5ae96b879..081e68b09d5 100644 --- a/esphome/components/micronova/sensor/micronova_sensor.h +++ b/esphome/components/micronova/sensor/micronova_sensor.h @@ -3,8 +3,7 @@ #include "esphome/components/micronova/micronova.h" #include "esphome/components/sensor/sensor.h" -namespace esphome { -namespace micronova { +namespace esphome::micronova { class MicroNovaSensor : public sensor::Sensor, public MicroNovaSensorListener { public: @@ -23,5 +22,4 @@ class MicroNovaSensor : public sensor::Sensor, public MicroNovaSensorListener { int fan_speed_offset_ = 0; }; -} // namespace micronova -} // namespace esphome +} // namespace esphome::micronova diff --git a/esphome/components/micronova/switch/micronova_switch.cpp b/esphome/components/micronova/switch/micronova_switch.cpp index 28674acd96b..81d36adccb8 100644 --- a/esphome/components/micronova/switch/micronova_switch.cpp +++ b/esphome/components/micronova/switch/micronova_switch.cpp @@ -1,7 +1,6 @@ #include "micronova_switch.h" -namespace esphome { -namespace micronova { +namespace esphome::micronova { void MicroNovaSwitch::write_state(bool state) { switch (this->get_function()) { @@ -31,5 +30,4 @@ void MicroNovaSwitch::write_state(bool state) { } } -} // namespace micronova -} // namespace esphome +} // namespace esphome::micronova diff --git a/esphome/components/micronova/switch/micronova_switch.h b/esphome/components/micronova/switch/micronova_switch.h index b0ca33b497f..70190843554 100644 --- a/esphome/components/micronova/switch/micronova_switch.h +++ b/esphome/components/micronova/switch/micronova_switch.h @@ -4,8 +4,7 @@ #include "esphome/core/component.h" #include "esphome/components/switch/switch.h" -namespace esphome { -namespace micronova { +namespace esphome::micronova { class MicroNovaSwitch : public Component, public switch_::Switch, public MicroNovaSwitchListener { public: @@ -25,5 +24,4 @@ class MicroNovaSwitch : public Component, public switch_::Switch, public MicroNo void write_state(bool state) override; }; -} // namespace micronova -} // namespace esphome +} // namespace esphome::micronova diff --git a/esphome/components/micronova/text_sensor/micronova_text_sensor.cpp b/esphome/components/micronova/text_sensor/micronova_text_sensor.cpp index 03b192ffd17..b62fb1afcef 100644 --- a/esphome/components/micronova/text_sensor/micronova_text_sensor.cpp +++ b/esphome/components/micronova/text_sensor/micronova_text_sensor.cpp @@ -1,7 +1,6 @@ #include "micronova_text_sensor.h" -namespace esphome { -namespace micronova { +namespace esphome::micronova { void MicroNovaTextSensor::process_value_from_stove(int value_from_stove) { if (value_from_stove == -1) { @@ -27,5 +26,4 @@ void MicroNovaTextSensor::process_value_from_stove(int value_from_stove) { } } -} // namespace micronova -} // namespace esphome +} // namespace esphome::micronova diff --git a/esphome/components/micronova/text_sensor/micronova_text_sensor.h b/esphome/components/micronova/text_sensor/micronova_text_sensor.h index b4e5de9bb3a..352f0496544 100644 --- a/esphome/components/micronova/text_sensor/micronova_text_sensor.h +++ b/esphome/components/micronova/text_sensor/micronova_text_sensor.h @@ -3,8 +3,7 @@ #include "esphome/components/micronova/micronova.h" #include "esphome/components/text_sensor/text_sensor.h" -namespace esphome { -namespace micronova { +namespace esphome::micronova { class MicroNovaTextSensor : public text_sensor::TextSensor, public MicroNovaSensorListener { public: @@ -16,5 +15,4 @@ class MicroNovaTextSensor : public text_sensor::TextSensor, public MicroNovaSens void process_value_from_stove(int value_from_stove) override; }; -} // namespace micronova -} // namespace esphome +} // namespace esphome::micronova From 6d336676a2390491827ea0920376cff73955f71f Mon Sep 17 00:00:00 2001 From: Juri Berlanda Date: Mon, 1 Dec 2025 18:09:58 +0100 Subject: [PATCH 48/57] [remote_transmitter, remote_receiver] Add RP2040 support (#12048) 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> --- esphome/components/remote_receiver/__init__.py | 2 ++ .../components/remote_receiver/remote_receiver.cpp | 2 +- esphome/components/remote_receiver/remote_receiver.h | 4 ++-- esphome/components/remote_transmitter/__init__.py | 1 + .../remote_transmitter/remote_transmitter.cpp | 6 +++--- .../remote_transmitter/remote_transmitter.h | 2 +- .../components/remote_receiver/test.rp2040-ard.yaml | 12 ++++++++++++ .../remote_transmitter/test.rp2040-ard.yaml | 7 +++++++ 8 files changed, 29 insertions(+), 7 deletions(-) create mode 100644 tests/components/remote_receiver/test.rp2040-ard.yaml create mode 100644 tests/components/remote_transmitter/test.rp2040-ard.yaml diff --git a/esphome/components/remote_receiver/__init__.py b/esphome/components/remote_receiver/__init__.py index cd2b440645f..e79b3f91edc 100644 --- a/esphome/components/remote_receiver/__init__.py +++ b/esphome/components/remote_receiver/__init__.py @@ -114,6 +114,7 @@ CONFIG_SCHEMA = remote_base.validate_triggers( bk72xx="1000b", ln882x="1000b", rtl87xx="1000b", + rp2040="1000b", ): cv.validate_bytes, cv.Optional(CONF_FILTER, default="50us"): cv.All( cv.positive_time_period_microseconds, @@ -213,6 +214,7 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform( PlatformFramework.BK72XX_ARDUINO, PlatformFramework.RTL87XX_ARDUINO, PlatformFramework.LN882X_ARDUINO, + PlatformFramework.RP2040_ARDUINO, }, } ) diff --git a/esphome/components/remote_receiver/remote_receiver.cpp b/esphome/components/remote_receiver/remote_receiver.cpp index a8438e20d73..53bfb0890f1 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) +#if defined(USE_LIBRETINY) || defined(USE_ESP8266) || defined(USE_RP2040) namespace esphome { namespace remote_receiver { diff --git a/esphome/components/remote_receiver/remote_receiver.h b/esphome/components/remote_receiver/remote_receiver.h index 3ddcf353c76..3d2f7f0ef9f 100644 --- a/esphome/components/remote_receiver/remote_receiver.h +++ b/esphome/components/remote_receiver/remote_receiver.h @@ -12,7 +12,7 @@ namespace esphome { namespace remote_receiver { -#if defined(USE_ESP8266) || defined(USE_LIBRETINY) +#if defined(USE_ESP8266) || defined(USE_LIBRETINY) || defined(USE_RP2040) struct RemoteReceiverComponentStore { static void gpio_intr(RemoteReceiverComponentStore *arg); @@ -84,7 +84,7 @@ class RemoteReceiverComponent : public remote_base::RemoteReceiverBase, std::string error_string_{""}; #endif -#if defined(USE_ESP8266) || defined(USE_LIBRETINY) || defined(USE_ESP32) +#if defined(USE_ESP8266) || defined(USE_LIBRETINY) || defined(USE_ESP32) || defined(USE_RP2040) RemoteReceiverComponentStore store_; HighFrequencyLoopRequester high_freq_; #endif diff --git a/esphome/components/remote_transmitter/__init__.py b/esphome/components/remote_transmitter/__init__.py index faa6c827f72..ff055b959bf 100644 --- a/esphome/components/remote_transmitter/__init__.py +++ b/esphome/components/remote_transmitter/__init__.py @@ -156,6 +156,7 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform( PlatformFramework.BK72XX_ARDUINO, PlatformFramework.RTL87XX_ARDUINO, PlatformFramework.LN882X_ARDUINO, + PlatformFramework.RP2040_ARDUINO, }, } ) diff --git a/esphome/components/remote_transmitter/remote_transmitter.cpp b/esphome/components/remote_transmitter/remote_transmitter.cpp index 347e9d9d333..576143bcbcb 100644 --- a/esphome/components/remote_transmitter/remote_transmitter.cpp +++ b/esphome/components/remote_transmitter/remote_transmitter.cpp @@ -2,7 +2,7 @@ #include "esphome/core/log.h" #include "esphome/core/application.h" -#if defined(USE_LIBRETINY) || defined(USE_ESP8266) +#if defined(USE_LIBRETINY) || defined(USE_ESP8266) || defined(USE_RP2040) namespace esphome { namespace remote_transmitter { @@ -40,8 +40,8 @@ void RemoteTransmitterComponent::await_target_time_() { if (this->target_time_ == 0) { this->target_time_ = current_time; } else if ((int32_t) (this->target_time_ - current_time) > 0) { -#if defined(USE_LIBRETINY) - // busy loop for libretiny is required (see the comment inside micros() in wiring.c) +#if defined(USE_LIBRETINY) || defined(USE_RP2040) + // busy loop is required for libretiny and rp2040 as interrupts are disabled while ((int32_t) (this->target_time_ - micros()) > 0) ; #else diff --git a/esphome/components/remote_transmitter/remote_transmitter.h b/esphome/components/remote_transmitter/remote_transmitter.h index cc3b82ad61f..dd6a849e4c2 100644 --- a/esphome/components/remote_transmitter/remote_transmitter.h +++ b/esphome/components/remote_transmitter/remote_transmitter.h @@ -62,7 +62,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) +#if defined(USE_ESP8266) || defined(USE_LIBRETINY) || defined(USE_RP2040) 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); diff --git a/tests/components/remote_receiver/test.rp2040-ard.yaml b/tests/components/remote_receiver/test.rp2040-ard.yaml new file mode 100644 index 00000000000..c9784ae0036 --- /dev/null +++ b/tests/components/remote_receiver/test.rp2040-ard.yaml @@ -0,0 +1,12 @@ +remote_receiver: + id: rcvr + pin: GPIO5 + 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.rp2040-ard.yaml b/tests/components/remote_transmitter/test.rp2040-ard.yaml new file mode 100644 index 00000000000..19759360f47 --- /dev/null +++ b/tests/components/remote_transmitter/test.rp2040-ard.yaml @@ -0,0 +1,7 @@ +remote_transmitter: + id: xmitr + pin: GPIO5 + carrier_duty_percent: 50% + +packages: + buttons: !include common-buttons.yaml From 2b7695ba3fea894d9ff2787150e87c119ac42e46 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 1 Dec 2025 12:40:56 -0500 Subject: [PATCH 49/57] [core] Fix clean all windows (#12217) Co-authored-by: Claude Co-authored-by: J. Nick Koston --- esphome/writer.py | 35 ++++++++--- tests/unit_tests/test_writer.py | 103 ++++++++++++++++++++++++++++++++ 2 files changed, 129 insertions(+), 9 deletions(-) diff --git a/esphome/writer.py b/esphome/writer.py index 3124e9e12ce..721db07f96c 100644 --- a/esphome/writer.py +++ b/esphome/writer.py @@ -1,8 +1,12 @@ +from collections.abc import Callable import importlib import logging import os from pathlib import Path import re +import shutil +import stat +from types import TracebackType from esphome import loader from esphome.config import iter_component_configs, iter_components @@ -301,9 +305,24 @@ def clean_cmake_cache(): pioenvs_cmake_path.unlink() -def clean_build(clear_pio_cache: bool = True): - import shutil +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 clean_build(clear_pio_cache: bool = True): # Allow skipping cache cleaning for integration tests if os.environ.get("ESPHOME_SKIP_CLEAN_BUILD"): _LOGGER.warning("Skipping build cleaning (ESPHOME_SKIP_CLEAN_BUILD set)") @@ -312,11 +331,11 @@ def clean_build(clear_pio_cache: bool = True): pioenvs = CORE.relative_pioenvs_path() if pioenvs.is_dir(): _LOGGER.info("Deleting %s", pioenvs) - shutil.rmtree(pioenvs) + shutil.rmtree(pioenvs, onerror=_rmtree_error_handler) piolibdeps = CORE.relative_piolibdeps_path() if piolibdeps.is_dir(): _LOGGER.info("Deleting %s", piolibdeps) - shutil.rmtree(piolibdeps) + shutil.rmtree(piolibdeps, onerror=_rmtree_error_handler) dependencies_lock = CORE.relative_build_path("dependencies.lock") if dependencies_lock.is_file(): _LOGGER.info("Deleting %s", dependencies_lock) @@ -337,12 +356,10 @@ def clean_build(clear_pio_cache: bool = True): cache_dir = Path(config.get("platformio", "cache_dir")) if cache_dir.is_dir(): _LOGGER.info("Deleting PlatformIO cache %s", cache_dir) - shutil.rmtree(cache_dir) + shutil.rmtree(cache_dir, onerror=_rmtree_error_handler) def clean_all(configuration: list[str]): - import shutil - data_dirs = [] for config in configuration: item = Path(config) @@ -364,7 +381,7 @@ def clean_all(configuration: list[str]): if item.is_file() and not item.name.endswith(".json"): item.unlink() elif item.is_dir() and item.name != "storage": - shutil.rmtree(item) + shutil.rmtree(item, onerror=_rmtree_error_handler) # Clean PlatformIO project files try: @@ -378,7 +395,7 @@ def clean_all(configuration: list[str]): path = Path(config.get("platformio", pio_dir)) if path.is_dir(): _LOGGER.info("Deleting PlatformIO %s %s", pio_dir, path) - shutil.rmtree(path) + shutil.rmtree(path, onerror=_rmtree_error_handler) GITIGNORE_CONTENT = """# Gitignore settings for ESPHome diff --git a/tests/unit_tests/test_writer.py b/tests/unit_tests/test_writer.py index a2a358f4d35..9fa60c06ecd 100644 --- a/tests/unit_tests/test_writer.py +++ b/tests/unit_tests/test_writer.py @@ -1,7 +1,9 @@ """Test writer module functionality.""" from collections.abc import Callable +import os from pathlib import Path +import stat from typing import Any from unittest.mock import MagicMock, patch @@ -15,6 +17,7 @@ from esphome.writer import ( CPP_INCLUDE_BEGIN, CPP_INCLUDE_END, GITIGNORE_CONTENT, + clean_all, clean_build, clean_cmake_cache, storage_should_clean, @@ -1062,3 +1065,103 @@ def test_clean_all_preserves_json_files( # Verify logging mentions cleaning assert "Cleaning" in caplog.text assert str(build_dir) in caplog.text + + +@patch("esphome.writer.CORE") +def test_clean_build_handles_readonly_files( + mock_core: MagicMock, + tmp_path: Path, +) -> None: + """Test clean_build handles read-only files (e.g., git pack files on Windows).""" + # Create directory structure with read-only files + pioenvs_dir = tmp_path / ".pioenvs" + pioenvs_dir.mkdir() + git_dir = pioenvs_dir / ".git" / "objects" / "pack" + git_dir.mkdir(parents=True) + + # Create a read-only file (simulating git pack files on Windows) + readonly_file = git_dir / "pack-abc123.pack" + readonly_file.write_text("pack data") + os.chmod(readonly_file, stat.S_IRUSR) # Read-only + + # Setup mocks + mock_core.relative_pioenvs_path.return_value = pioenvs_dir + mock_core.relative_piolibdeps_path.return_value = tmp_path / ".piolibdeps" + mock_core.relative_build_path.return_value = tmp_path / "dependencies.lock" + + # Verify file is read-only + assert not os.access(readonly_file, os.W_OK) + + # Call the function - should not crash + clean_build() + + # Verify directory was removed despite read-only files + assert not pioenvs_dir.exists() + + +@patch("esphome.writer.CORE") +def test_clean_all_handles_readonly_files( + mock_core: MagicMock, + tmp_path: Path, +) -> None: + """Test clean_all handles read-only files.""" + # Create config directory + config_dir = tmp_path / "config" + config_dir.mkdir() + + build_dir = config_dir / ".esphome" + build_dir.mkdir() + + # Create a subdirectory with read-only files + subdir = build_dir / "subdir" + subdir.mkdir() + readonly_file = subdir / "readonly.txt" + readonly_file.write_text("content") + os.chmod(readonly_file, stat.S_IRUSR) # Read-only + + # Verify file is read-only + assert not os.access(readonly_file, os.W_OK) + + # Call the function - should not crash + clean_all([str(config_dir)]) + + # Verify directory was removed despite read-only files + assert not subdir.exists() + assert build_dir.exists() # .esphome dir itself is preserved + + +@patch("esphome.writer.CORE") +def test_clean_build_reraises_for_other_errors( + mock_core: MagicMock, + tmp_path: Path, +) -> None: + """Test clean_build re-raises errors that are not read-only permission issues.""" + # Create directory structure with a read-only subdirectory + # This prevents file deletion and triggers the error handler + pioenvs_dir = tmp_path / ".pioenvs" + pioenvs_dir.mkdir() + subdir = pioenvs_dir / "subdir" + subdir.mkdir() + test_file = subdir / "test.txt" + test_file.write_text("content") + + # Make subdir read-only so files inside can't be deleted + os.chmod(subdir, stat.S_IRUSR | stat.S_IXUSR) + + # Setup mocks + mock_core.relative_pioenvs_path.return_value = pioenvs_dir + mock_core.relative_piolibdeps_path.return_value = tmp_path / ".piolibdeps" + mock_core.relative_build_path.return_value = tmp_path / "dependencies.lock" + + try: + # Mock os.access in writer module to return True (writable) + # This simulates a case where the error is NOT due to read-only permissions + # so the error handler should re-raise instead of trying to fix permissions + with ( + patch("esphome.writer.os.access", return_value=True), + pytest.raises(PermissionError), + ): + clean_build() + finally: + # Cleanup - restore write permission so tmp_path cleanup works + os.chmod(subdir, stat.S_IRWXU) From 6a79ce8eff9cf6312ee4d0333df732ab948af518 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Mon, 1 Dec 2025 14:16:39 -0600 Subject: [PATCH 50/57] [uart] Automatically enable the socket wake infrastructure when RX wake requested (#12221) --- esphome/components/uart/__init__.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/esphome/components/uart/__init__.py b/esphome/components/uart/__init__.py index a1c78dd45c9..6494aaa286e 100644 --- a/esphome/components/uart/__init__.py +++ b/esphome/components/uart/__init__.py @@ -42,6 +42,17 @@ _LOGGER = getLogger(__name__) CODEOWNERS = ["@esphome/core"] DOMAIN = "uart" + +def AUTO_LOAD() -> list[str]: + """Ideally, we would only auto-load socket only when wake_loop_on_rx is requested; + however, AUTO_LOAD is examined before wake_loop_on_rx is set, so instead, since ESP32 + always uses socket select support in the main app, we'll just ensure it's loaded here. + """ + if CORE.is_esp32: + return ["socket"] + return [] + + uart_ns = cg.esphome_ns.namespace("uart") UARTComponent = uart_ns.class_("UARTComponent") @@ -125,7 +136,15 @@ def request_wake_loop_on_rx() -> None: should call this function during their code generation. This enables the RX event task which wakes the main loop when data arrives. """ - _get_data().wake_loop_on_rx = True + data = _get_data() + if not data.wake_loop_on_rx: + data.wake_loop_on_rx = True + + # UART RX event task uses wake_loop_threadsafe() to notify the main loop + # Automatically enable the socket wake infrastructure when RX wake is requested + from esphome.components import socket + + socket.require_wake_loop_threadsafe() def validate_raw_data(value): From 52fe3de78f81bea1d44fb4f87ed90e51ddc8ac92 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Mon, 1 Dec 2025 14:27:20 -0600 Subject: [PATCH 51/57] [zwave_proxy] Use new socket wake infrastructure to reduce latency, convert to C++17 namespace style (#12135) Co-authored-by: J. Nick Koston --- esphome/components/zwave_proxy/__init__.py | 3 +++ esphome/components/zwave_proxy/zwave_proxy.cpp | 8 ++++---- esphome/components/zwave_proxy/zwave_proxy.h | 6 ++---- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/esphome/components/zwave_proxy/__init__.py b/esphome/components/zwave_proxy/__init__.py index d88f9f7041e..5be05bb4646 100644 --- a/esphome/components/zwave_proxy/__init__.py +++ b/esphome/components/zwave_proxy/__init__.py @@ -41,3 +41,6 @@ async def to_code(config): await cg.register_component(var, config) await uart.register_uart_device(var, config) cg.add_define("USE_ZWAVE_PROXY") + + # Request UART to wake the main loop when data arrives for low-latency processing + uart.request_wake_loop_on_rx() diff --git a/esphome/components/zwave_proxy/zwave_proxy.cpp b/esphome/components/zwave_proxy/zwave_proxy.cpp index a26a9b23355..e0ca5529b8e 100644 --- a/esphome/components/zwave_proxy/zwave_proxy.cpp +++ b/esphome/components/zwave_proxy/zwave_proxy.cpp @@ -5,8 +5,7 @@ #include "esphome/core/log.h" #include "esphome/core/util.h" -namespace esphome { -namespace zwave_proxy { +namespace esphome::zwave_proxy { static const char *const TAG = "zwave_proxy"; @@ -144,6 +143,7 @@ void ZWaveProxy::zwave_proxy_request(api::APIConnection *api_connection, api::en this->api_connection_ = api_connection; ESP_LOGV(TAG, "API connection is now subscribed"); break; + case api::enums::ZWAVE_PROXY_REQUEST_TYPE_UNSUBSCRIBE: if (this->api_connection_ != api_connection) { ESP_LOGV(TAG, "API connection is not subscribed"); @@ -151,6 +151,7 @@ void ZWaveProxy::zwave_proxy_request(api::APIConnection *api_connection, api::en } this->api_connection_ = nullptr; break; + default: ESP_LOGW(TAG, "Unknown request type: %d", type); break; @@ -342,5 +343,4 @@ bool ZWaveProxy::response_handler_() { ZWaveProxy *global_zwave_proxy = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -} // namespace zwave_proxy -} // namespace esphome +} // namespace esphome::zwave_proxy diff --git a/esphome/components/zwave_proxy/zwave_proxy.h b/esphome/components/zwave_proxy/zwave_proxy.h index 20d9090d98c..e23e202bea5 100644 --- a/esphome/components/zwave_proxy/zwave_proxy.h +++ b/esphome/components/zwave_proxy/zwave_proxy.h @@ -8,8 +8,7 @@ #include -namespace esphome { -namespace zwave_proxy { +namespace esphome::zwave_proxy { static constexpr size_t MAX_ZWAVE_FRAME_SIZE = 257; // Maximum Z-Wave frame size @@ -89,5 +88,4 @@ class ZWaveProxy : public uart::UARTDevice, public Component { extern ZWaveProxy *global_zwave_proxy; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -} // namespace zwave_proxy -} // namespace esphome +} // namespace esphome::zwave_proxy From 78df884bb54feacb181ca66ac7907de551d08ab8 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 1 Dec 2025 16:03:00 -0500 Subject: [PATCH 52/57] [rtl87xx] Fix AsyncTCP compilation by upgrading FreeRTOS to 8.2.3 (#12230) Co-authored-by: Claude --- esphome/components/rtl87xx/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/esphome/components/rtl87xx/__init__.py b/esphome/components/rtl87xx/__init__.py index 109c986f75c..d24ffcea3d3 100644 --- a/esphome/components/rtl87xx/__init__.py +++ b/esphome/components/rtl87xx/__init__.py @@ -6,6 +6,7 @@ # in schema.py file in this directory. from esphome import pins +import esphome.codegen as cg from esphome.components import libretiny from esphome.components.libretiny.const import ( COMPONENT_RTL87XX, @@ -45,6 +46,9 @@ CONFIG_SCHEMA.prepend_extra(_set_core_data) async def to_code(config): + # Use FreeRTOS 8.2.3+ for xTaskNotifyGive/ulTaskNotifyTake required by AsyncTCP 3.4.3+ + # https://github.com/esphome/esphome/issues/10220 + cg.add_platformio_option("custom_versions.freertos", "8.2.3") return await libretiny.component_to_code(config) From d4bd282bb4edc3f2f5c2d28e4e65b496edd0ef83 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Mon, 1 Dec 2025 16:08:49 -0600 Subject: [PATCH 53/57] [helpers] Fix unit tests following #12135 (#12237) --- script/helpers.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/script/helpers.py b/script/helpers.py index 1039ef39ac2..06a50a30923 100644 --- a/script/helpers.py +++ b/script/helpers.py @@ -630,7 +630,12 @@ def get_all_dependencies(component_names: set[str]) -> set[str]: Returns: Set of all components including dependencies and auto-loaded components """ - from esphome.const import KEY_CORE + from esphome.const import ( + KEY_CORE, + KEY_TARGET_FRAMEWORK, + KEY_TARGET_PLATFORM, + PLATFORM_HOST, + ) from esphome.core import CORE from esphome.loader import get_component @@ -642,7 +647,10 @@ def get_all_dependencies(component_names: set[str]) -> set[str]: # Set up fake config path for component loading root = Path(__file__).parent.parent CORE.config_path = root - CORE.data[KEY_CORE] = {} + CORE.data[KEY_CORE] = { + KEY_TARGET_PLATFORM: PLATFORM_HOST, + KEY_TARGET_FRAMEWORK: "host-native", + } # Keep finding dependencies until no new ones are found while True: From d332edfacadc08f8b3d232a323dbe5cad824ae15 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 1 Dec 2025 16:50:03 -0600 Subject: [PATCH 54/57] [datetime] Convert to C++17 nested namespace style (#12235) --- esphome/components/datetime/date_entity.cpp | 6 ++---- esphome/components/datetime/date_entity.h | 6 ++---- esphome/components/datetime/datetime_base.h | 6 ++---- esphome/components/datetime/datetime_entity.cpp | 6 ++---- esphome/components/datetime/datetime_entity.h | 6 ++---- esphome/components/datetime/time_entity.cpp | 6 ++---- esphome/components/datetime/time_entity.h | 6 ++---- 7 files changed, 14 insertions(+), 28 deletions(-) diff --git a/esphome/components/datetime/date_entity.cpp b/esphome/components/datetime/date_entity.cpp index 2c2775ecf40..c061bc81f7f 100644 --- a/esphome/components/datetime/date_entity.cpp +++ b/esphome/components/datetime/date_entity.cpp @@ -5,8 +5,7 @@ #include "esphome/core/log.h" -namespace esphome { -namespace datetime { +namespace esphome::datetime { static const char *const TAG = "datetime.date_entity"; @@ -129,7 +128,6 @@ void DateEntityRestoreState::apply(DateEntity *date) { date->publish_state(); } -} // namespace datetime -} // namespace esphome +} // namespace esphome::datetime #endif // USE_DATETIME_DATE diff --git a/esphome/components/datetime/date_entity.h b/esphome/components/datetime/date_entity.h index ba2edb127ad..069116d1626 100644 --- a/esphome/components/datetime/date_entity.h +++ b/esphome/components/datetime/date_entity.h @@ -10,8 +10,7 @@ #include "datetime_base.h" -namespace esphome { -namespace datetime { +namespace esphome::datetime { #define LOG_DATETIME_DATE(prefix, type, obj) \ if ((obj) != nullptr) { \ @@ -111,7 +110,6 @@ template class DateSetAction : public Action, public Pare } }; -} // namespace datetime -} // namespace esphome +} // namespace esphome::datetime #endif // USE_DATETIME_DATE diff --git a/esphome/components/datetime/datetime_base.h b/esphome/components/datetime/datetime_base.h index b5f54ac96f6..7b9b281ea43 100644 --- a/esphome/components/datetime/datetime_base.h +++ b/esphome/components/datetime/datetime_base.h @@ -8,8 +8,7 @@ #include "esphome/components/time/real_time_clock.h" #endif -namespace esphome { -namespace datetime { +namespace esphome::datetime { class DateTimeBase : public EntityBase { public: @@ -37,5 +36,4 @@ class DateTimeStateTrigger : public Trigger { } }; -} // namespace datetime -} // namespace esphome +} // namespace esphome::datetime diff --git a/esphome/components/datetime/datetime_entity.cpp b/esphome/components/datetime/datetime_entity.cpp index 8606a47fa72..694f9c57210 100644 --- a/esphome/components/datetime/datetime_entity.cpp +++ b/esphome/components/datetime/datetime_entity.cpp @@ -5,8 +5,7 @@ #include "esphome/core/log.h" -namespace esphome { -namespace datetime { +namespace esphome::datetime { static const char *const TAG = "datetime.datetime_entity"; @@ -250,7 +249,6 @@ bool OnDateTimeTrigger::matches_(const ESPTime &time) const { } #endif -} // namespace datetime -} // namespace esphome +} // namespace esphome::datetime #endif // USE_DATETIME_TIME diff --git a/esphome/components/datetime/datetime_entity.h b/esphome/components/datetime/datetime_entity.h index 43bff5a1812..018346b34b6 100644 --- a/esphome/components/datetime/datetime_entity.h +++ b/esphome/components/datetime/datetime_entity.h @@ -10,8 +10,7 @@ #include "datetime_base.h" -namespace esphome { -namespace datetime { +namespace esphome::datetime { #define LOG_DATETIME_DATETIME(prefix, type, obj) \ if ((obj) != nullptr) { \ @@ -146,7 +145,6 @@ class OnDateTimeTrigger : public Trigger<>, public Component, public Parented, public Component, public Parented Date: Mon, 1 Dec 2025 16:50:29 -0600 Subject: [PATCH 55/57] [button] Convert to C++17 nested namespace style (#12233) Co-authored-by: Keith Burzinski --- esphome/components/button/automation.h | 6 ++---- esphome/components/button/button.cpp | 6 ++---- esphome/components/button/button.h | 6 ++---- 3 files changed, 6 insertions(+), 12 deletions(-) diff --git a/esphome/components/button/automation.h b/esphome/components/button/automation.h index 3b792eb5d72..6a54b141a35 100644 --- a/esphome/components/button/automation.h +++ b/esphome/components/button/automation.h @@ -4,8 +4,7 @@ #include "esphome/core/automation.h" #include "esphome/core/component.h" -namespace esphome { -namespace button { +namespace esphome::button { template class PressAction : public Action { public: @@ -24,5 +23,4 @@ class ButtonPressTrigger : public Trigger<> { } }; -} // namespace button -} // namespace esphome +} // namespace esphome::button diff --git a/esphome/components/button/button.cpp b/esphome/components/button/button.cpp index c968d310888..87a222776ea 100644 --- a/esphome/components/button/button.cpp +++ b/esphome/components/button/button.cpp @@ -1,8 +1,7 @@ #include "button.h" #include "esphome/core/log.h" -namespace esphome { -namespace button { +namespace esphome::button { static const char *const TAG = "button"; @@ -26,5 +25,4 @@ void Button::press() { } void Button::add_on_press_callback(std::function &&callback) { this->press_callback_.add(std::move(callback)); } -} // namespace button -} // namespace esphome +} // namespace esphome::button diff --git a/esphome/components/button/button.h b/esphome/components/button/button.h index 75b76f9dcf3..18122f6f2f6 100644 --- a/esphome/components/button/button.h +++ b/esphome/components/button/button.h @@ -4,8 +4,7 @@ #include "esphome/core/entity_base.h" #include "esphome/core/helpers.h" -namespace esphome { -namespace button { +namespace esphome::button { class Button; void log_button(const char *tag, const char *prefix, const char *type, Button *obj); @@ -45,5 +44,4 @@ class Button : public EntityBase, public EntityBase_DeviceClass { CallbackManager press_callback_{}; }; -} // namespace button -} // namespace esphome +} // namespace esphome::button From e42cf9a4f452ed80ec43782b11fd73543d41c164 Mon Sep 17 00:00:00 2001 From: Peter Popovec Date: Tue, 2 Dec 2025 00:06:47 +0100 Subject: [PATCH 56/57] [mqtt] Enable support for the RTL87XX platform (#7697) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- esphome/components/mqtt/__init__.py | 3 ++- tests/components/mqtt/test.rtl87xx-ard.yaml | 2 ++ .../build_components_base.rtl87xx-ard.yaml | 15 +++++++++++++++ 3 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 tests/components/mqtt/test.rtl87xx-ard.yaml create mode 100644 tests/test_build_components/build_components_base.rtl87xx-ard.yaml diff --git a/esphome/components/mqtt/__init__.py b/esphome/components/mqtt/__init__.py index 1fc0c30db1c..237ed2ce385 100644 --- a/esphome/components/mqtt/__init__.py +++ b/esphome/components/mqtt/__init__.py @@ -55,6 +55,7 @@ from esphome.const import ( PLATFORM_BK72XX, PLATFORM_ESP32, PLATFORM_ESP8266, + PLATFORM_RTL87XX, PlatformFramework, ) from esphome.core import CORE, CoroPriority, coroutine_with_priority @@ -316,7 +317,7 @@ CONFIG_SCHEMA = cv.All( } ), validate_config, - cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_BK72XX]), + cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_BK72XX, PLATFORM_RTL87XX]), _consume_mqtt_sockets, ) diff --git a/tests/components/mqtt/test.rtl87xx-ard.yaml b/tests/components/mqtt/test.rtl87xx-ard.yaml new file mode 100644 index 00000000000..25cb37a0b42 --- /dev/null +++ b/tests/components/mqtt/test.rtl87xx-ard.yaml @@ -0,0 +1,2 @@ +packages: + common: !include common.yaml diff --git a/tests/test_build_components/build_components_base.rtl87xx-ard.yaml b/tests/test_build_components/build_components_base.rtl87xx-ard.yaml new file mode 100644 index 00000000000..1720ef700dd --- /dev/null +++ b/tests/test_build_components/build_components_base.rtl87xx-ard.yaml @@ -0,0 +1,15 @@ +esphome: + name: componenttestesprtl87xx + friendly_name: $component_name + +rtl87xx: + board: generic-rtl8710bn-2mb-788k + +logger: + level: VERY_VERBOSE + +packages: + component_under_test: !include + file: $component_test_file + vars: + component_test_file: $component_test_file From df58e832e56dbae50baa2a6218166c92bef5c631 Mon Sep 17 00:00:00 2001 From: Djordje Mandic <6750655+DjordjeMandic@users.noreply.github.com> Date: Tue, 2 Dec 2025 00:44:33 +0100 Subject: [PATCH 57/57] [esp8266] Allow IN&OUT pin config for ESP8266 (#12238) --- esphome/components/esp8266/gpio.cpp | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/esphome/components/esp8266/gpio.cpp b/esphome/components/esp8266/gpio.cpp index 17a495bc1d7..124df39ce3d 100644 --- a/esphome/components/esp8266/gpio.cpp +++ b/esphome/components/esp8266/gpio.cpp @@ -8,11 +8,13 @@ namespace esphome::esp8266 { static const char *const TAG = "esp8266"; static int flags_to_mode(gpio::Flags flags, uint8_t pin) { - if (flags == gpio::FLAG_INPUT) { // NOLINT(bugprone-branch-clone) - return INPUT; - } else if (flags == gpio::FLAG_OUTPUT) { + if (flags == gpio::FLAG_OUTPUT || flags == (gpio::FLAG_OUTPUT | gpio::FLAG_INPUT)) { return OUTPUT; - } else if (flags == (gpio::FLAG_INPUT | gpio::FLAG_PULLUP)) { + } + if (flags == gpio::FLAG_INPUT) { + return INPUT; + } + if (flags == (gpio::FLAG_INPUT | gpio::FLAG_PULLUP)) { if (pin == 16) { // GPIO16 doesn't have a pullup, so pinMode would fail. // However, sometimes this method is called with pullup mode anyway @@ -21,13 +23,14 @@ static int flags_to_mode(gpio::Flags flags, uint8_t pin) { return INPUT; } return INPUT_PULLUP; - } else if (flags == (gpio::FLAG_INPUT | gpio::FLAG_PULLDOWN)) { - return INPUT_PULLDOWN_16; - } else if (flags == (gpio::FLAG_OUTPUT | gpio::FLAG_OPEN_DRAIN)) { - return OUTPUT_OPEN_DRAIN; - } else { - return 0; } + if (flags == (gpio::FLAG_INPUT | gpio::FLAG_PULLDOWN)) { + return INPUT_PULLDOWN_16; + } + if (flags == (gpio::FLAG_OUTPUT | gpio::FLAG_OPEN_DRAIN)) { + return OUTPUT_OPEN_DRAIN; + } + return INPUT; } struct ISRPinArg {