From b02f0e3c5feb2112e82302ac543a77caee451b80 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Fri, 20 Mar 2026 11:39:10 +1000 Subject: [PATCH 001/166] [sdl] Fix get_width()/height() when rotation used (#14950) --- esphome/components/sdl/sdl_esphome.cpp | 37 ++++++++++++++++++++++++++ esphome/components/sdl/sdl_esphome.h | 4 +-- 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/esphome/components/sdl/sdl_esphome.cpp b/esphome/components/sdl/sdl_esphome.cpp index f235e4e68c..74ca2ce39a 100644 --- a/esphome/components/sdl/sdl_esphome.cpp +++ b/esphome/components/sdl/sdl_esphome.cpp @@ -5,6 +5,30 @@ namespace esphome { namespace sdl { +int Sdl::get_width() { + switch (this->rotation_) { + case display::DISPLAY_ROTATION_90_DEGREES: + case display::DISPLAY_ROTATION_270_DEGREES: + return this->get_height_internal(); + case display::DISPLAY_ROTATION_0_DEGREES: + case display::DISPLAY_ROTATION_180_DEGREES: + default: + return this->get_width_internal(); + } +} + +int Sdl::get_height() { + switch (this->rotation_) { + case display::DISPLAY_ROTATION_0_DEGREES: + case display::DISPLAY_ROTATION_180_DEGREES: + return this->get_height_internal(); + case display::DISPLAY_ROTATION_90_DEGREES: + case display::DISPLAY_ROTATION_270_DEGREES: + default: + return this->get_width_internal(); + } +} + void Sdl::setup() { SDL_Init(SDL_INIT_VIDEO); this->window_ = SDL_CreateWindow(App.get_name().c_str(), this->pos_x_, this->pos_y_, this->width_, this->height_, @@ -49,6 +73,19 @@ void Sdl::draw_pixel_at(int x, int y, Color color) { if (!this->get_clipping().inside(x, y)) return; + if (this->rotation_ == display::DISPLAY_ROTATION_180_DEGREES) { + x = this->width_ - x - 1; + y = this->height_ - y - 1; + } else if (this->rotation_ == display::DISPLAY_ROTATION_90_DEGREES) { + auto tmp = x; + x = this->width_ - y - 1; + y = tmp; + } else if (this->rotation_ == display::DISPLAY_ROTATION_270_DEGREES) { + auto tmp = y; + y = this->height_ - x - 1; + x = tmp; + } + SDL_Rect rect{x, y, 1, 1}; auto data = (display::ColorUtil::color_to_565(color, display::COLOR_ORDER_RGB)); SDL_UpdateTexture(this->texture_, &rect, &data, 2); diff --git a/esphome/components/sdl/sdl_esphome.h b/esphome/components/sdl/sdl_esphome.h index c025e8ff6e..ce34cb817e 100644 --- a/esphome/components/sdl/sdl_esphome.h +++ b/esphome/components/sdl/sdl_esphome.h @@ -33,8 +33,8 @@ class Sdl : public display::Display { this->pos_x_ = pos_x; this->pos_y_ = pos_y; } - int get_width() override { return this->width_; } - int get_height() override { return this->height_; } + int get_width() override; + int get_height() override; float get_setup_priority() const override { return setup_priority::HARDWARE; } void dump_config() override { LOG_DISPLAY("", "SDL", this); } template void add_key_listener(int32_t keycode, F &&callback) { From 7df550f2a9a9049e742496298f94c9a6bb46cf9e Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Fri, 20 Mar 2026 11:52:52 +1000 Subject: [PATCH 002/166] Ensure lvgl libs available when editing for host (#14987) --- .clang-tidy.hash | 2 +- platformio.ini | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.clang-tidy.hash b/.clang-tidy.hash index 72023e511d..c32978d411 100644 --- a/.clang-tidy.hash +++ b/.clang-tidy.hash @@ -1 +1 @@ -44c877ff43765562ac8298902bf2208799643b77facf09c1c0c3c8c4e17187eb +9f5d763f95ff720024f3fdddba2fad3801e2bfe00b7cc2124e6d68c17d3504c6 diff --git a/platformio.ini b/platformio.ini index c5a4c630df..d3a482b652 100644 --- a/platformio.ini +++ b/platformio.ini @@ -546,6 +546,7 @@ extends = common platform = platformio/native lib_deps = esphome/noise-c@0.1.11 ; used by api + lvgl/lvgl@9.5.0 ; lvgl build_flags = ${common.build_flags} -DUSE_HOST From 6e87f8eb4e350f4a7bbcb0d6dd6fc3c8d9d2b11e Mon Sep 17 00:00:00 2001 From: Kent Gibson Date: Fri, 20 Mar 2026 10:06:58 +0800 Subject: [PATCH 003/166] [template] alarm_control_panel collapse SensorDataStore and bypassed_sensor_indicies into SensorInfo (#14852) Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: J. Nick Koston --- .../template_alarm_control_panel.cpp | 51 ++++++++++--------- .../template_alarm_control_panel.h | 24 ++++----- 2 files changed, 38 insertions(+), 37 deletions(-) diff --git a/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp b/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp index 651aa3c489..a224ab8459 100644 --- a/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp +++ b/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp @@ -16,17 +16,15 @@ static const char *const TAG = "template.alarm_control_panel"; TemplateAlarmControlPanel::TemplateAlarmControlPanel(){}; #ifdef USE_BINARY_SENSOR -void TemplateAlarmControlPanel::add_sensor(binary_sensor::BinarySensor *sensor, uint16_t flags, AlarmSensorType type) { - // Save the flags and type. Assign a store index for the per sensor data type. - SensorDataStore sd; - sd.last_chime_state = false; +void TemplateAlarmControlPanel::add_sensor(binary_sensor::BinarySensor *sensor, uint8_t flags, AlarmSensorType type) { + // Save the sensor pointer, flags, and type in the per-sensor info structure. AlarmSensor alarm_sensor; alarm_sensor.sensor = sensor; alarm_sensor.info.flags = flags; alarm_sensor.info.type = type; - alarm_sensor.info.store_index = this->next_store_index_++; + alarm_sensor.info.chime_active = false; + alarm_sensor.info.auto_bypassed = false; this->sensors_.push_back(alarm_sensor); - this->sensor_data_.push_back(sd); }; // Alarm sensor type strings indexed by AlarmSensorType enum (0-3): DELAYED, INSTANT, DELAYED_FOLLOWER, INSTANT_ALWAYS @@ -55,7 +53,7 @@ void TemplateAlarmControlPanel::dump_config() { (this->trigger_time_ / 1000), this->get_supported_features()); #ifdef USE_BINARY_SENSOR for (const auto &alarm_sensor : this->sensors_) { - const uint16_t flags = alarm_sensor.info.flags; + const uint8_t flags = alarm_sensor.info.flags; ESP_LOGCONFIG(TAG, " Binary Sensor:\n" " Name: %s\n" @@ -95,7 +93,7 @@ void TemplateAlarmControlPanel::loop() { delay = this->arming_night_time_; } if ((millis() - this->last_update_) > delay) { - this->bypass_before_arming(); + this->auto_bypass_sensors_(); this->publish_state(this->desired_state_); } return; @@ -117,26 +115,25 @@ void TemplateAlarmControlPanel::loop() { #ifdef USE_BINARY_SENSOR // Test all of the sensors regardless of the alarm panel state - for (const auto &alarm_sensor : this->sensors_) { - const auto &info = alarm_sensor.info; + for (auto &alarm_sensor : this->sensors_) { + auto &info = alarm_sensor.info; auto *sensor = alarm_sensor.sensor; // Check for chime zones if (info.flags & BINARY_SENSOR_MODE_CHIME) { // Look for the transition from closed to open - if ((!this->sensor_data_[info.store_index].last_chime_state) && (sensor->state)) { + if ((!info.chime_active) && (sensor->state)) { // Must be disarmed to chime if (this->current_state_ == ACP_STATE_DISARMED) { this->chime_callback_.call(); } } // Record the sensor state change - this->sensor_data_[info.store_index].last_chime_state = sensor->state; + info.chime_active = sensor->state; } // Check for faulted sensors if (sensor->state) { // Skip if auto bypassed - if (std::count(this->bypassed_sensor_indicies_.begin(), this->bypassed_sensor_indicies_.end(), - info.store_index) == 1) { + if (info.auto_bypassed) { continue; } // Skip if bypass armed home @@ -239,23 +236,33 @@ void TemplateAlarmControlPanel::arm_(optional code, alarm_control_p if (delay > 0) { this->publish_state(ACP_STATE_ARMING); } else { - this->bypass_before_arming(); + this->auto_bypass_sensors_(); this->publish_state(state); } } -void TemplateAlarmControlPanel::bypass_before_arming() { +void TemplateAlarmControlPanel::auto_bypass_sensors_() { #ifdef USE_BINARY_SENSOR - for (const auto &alarm_sensor : this->sensors_) { + for (auto &alarm_sensor : this->sensors_) { + auto &info = alarm_sensor.info; + auto *sensor = alarm_sensor.sensor; // Check for faulted bypass_auto sensors and remove them from monitoring - if ((alarm_sensor.info.flags & BINARY_SENSOR_MODE_BYPASS_AUTO) && (alarm_sensor.sensor->state)) { - ESP_LOGW(TAG, "'%s' is faulted and will be automatically bypassed", alarm_sensor.sensor->get_name().c_str()); - this->bypassed_sensor_indicies_.push_back(alarm_sensor.info.store_index); + if ((info.flags & BINARY_SENSOR_MODE_BYPASS_AUTO) && (sensor->state)) { + ESP_LOGW(TAG, "'%s' is faulted and will be automatically bypassed", sensor->get_name().c_str()); + info.auto_bypassed = true; } } #endif } +void TemplateAlarmControlPanel::clear_auto_bypassed_sensors_() { +#ifdef USE_BINARY_SENSOR + for (auto &alarm_sensor : this->sensors_) { + alarm_sensor.info.auto_bypassed = false; + } +#endif +} + void TemplateAlarmControlPanel::control(const AlarmControlPanelCall &call) { auto opt_state = call.get_state(); if (opt_state) { @@ -273,9 +280,7 @@ void TemplateAlarmControlPanel::control(const AlarmControlPanelCall &call) { } this->desired_state_ = ACP_STATE_DISARMED; this->publish_state(ACP_STATE_DISARMED); -#ifdef USE_BINARY_SENSOR - this->bypassed_sensor_indicies_.clear(); -#endif + this->clear_auto_bypassed_sensors_(); } else if (state == ACP_STATE_TRIGGERED) { this->publish_state(ACP_STATE_TRIGGERED); } else if (state == ACP_STATE_PENDING) { diff --git a/esphome/components/template/alarm_control_panel/template_alarm_control_panel.h b/esphome/components/template/alarm_control_panel/template_alarm_control_panel.h index 4f32e99fd7..57a99f2830 100644 --- a/esphome/components/template/alarm_control_panel/template_alarm_control_panel.h +++ b/esphome/components/template/alarm_control_panel/template_alarm_control_panel.h @@ -18,7 +18,7 @@ namespace esphome::template_ { #ifdef USE_BINARY_SENSOR -enum BinarySensorFlags : uint16_t { +enum BinarySensorFlags : uint8_t { BINARY_SENSOR_MODE_NORMAL = 1 << 0, BINARY_SENSOR_MODE_BYPASS_ARMED_HOME = 1 << 1, BINARY_SENSOR_MODE_BYPASS_ARMED_NIGHT = 1 << 2, @@ -41,14 +41,11 @@ enum TemplateAlarmControlPanelRestoreMode { }; #ifdef USE_BINARY_SENSOR -struct SensorDataStore { - bool last_chime_state; -}; - struct SensorInfo { - uint16_t flags; + uint8_t flags; AlarmSensorType type; - uint8_t store_index; + bool chime_active; + bool auto_bypassed; }; struct AlarmSensor { @@ -68,7 +65,9 @@ class TemplateAlarmControlPanel final : public alarm_control_panel::AlarmControl bool get_requires_code_to_arm() const override { return this->requires_code_to_arm_; } bool get_all_sensors_ready() { return this->sensors_ready_; }; void set_restore_mode(TemplateAlarmControlPanelRestoreMode restore_mode) { this->restore_mode_ = restore_mode; } - void bypass_before_arming(); + // Remove before 2026.10.0 + ESPDEPRECATED("bypass_before_arming() is deprecated and will be removed in 2026.10.0", "2026.4.0") + void bypass_before_arming() { this->auto_bypass_sensors_(); } #ifdef USE_BINARY_SENSOR /** Initialize the sensors vector with the specified capacity. @@ -83,7 +82,7 @@ class TemplateAlarmControlPanel final : public alarm_control_panel::AlarmControl * @param flags The OR of BinarySensorFlags for the sensor. * @param type The sensor type which determines its triggering behaviour. */ - void add_sensor(binary_sensor::BinarySensor *sensor, uint16_t flags = 0, + void add_sensor(binary_sensor::BinarySensor *sensor, uint8_t flags = 0, AlarmSensorType type = ALARM_SENSOR_TYPE_DELAYED); #endif @@ -141,11 +140,6 @@ class TemplateAlarmControlPanel final : public alarm_control_panel::AlarmControl #ifdef USE_BINARY_SENSOR // List of binary sensors with their alarm-specific info FixedVector sensors_; - // a list of automatically bypassed sensors - std::vector bypassed_sensor_indicies_; - // Per sensor data store - std::vector sensor_data_; - uint8_t next_store_index_ = 0; #endif TemplateAlarmControlPanelRestoreMode restore_mode_{}; @@ -170,6 +164,8 @@ class TemplateAlarmControlPanel final : public alarm_control_panel::AlarmControl bool is_code_valid_(optional code); void arm_(optional code, alarm_control_panel::AlarmControlPanelState state, uint32_t delay); + void auto_bypass_sensors_(); + void clear_auto_bypassed_sensors_(); }; } // namespace esphome::template_ From 02ada93ea5aeb5cee2eb79dff8853269331981ec Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 19 Mar 2026 18:40:33 -1000 Subject: [PATCH 004/166] [wifi] Reject WiFi config on RP2040/RP2350 boards without CYW43 chip (#14990) --- esphome/components/rp2040/__init__.py | 18 ++++++++ esphome/components/rp2040/boards.py | 10 +++++ esphome/components/rp2040/generate_boards.py | 8 +++- esphome/components/wifi/__init__.py | 8 ++++ .../components/test_rp2040_generate_boards.py | 45 +++++++++++++++++-- 5 files changed, 84 insertions(+), 5 deletions(-) diff --git a/esphome/components/rp2040/__init__.py b/esphome/components/rp2040/__init__.py index 71e5f1488c..0bb1811069 100644 --- a/esphome/components/rp2040/__init__.py +++ b/esphome/components/rp2040/__init__.py @@ -24,6 +24,7 @@ from esphome.const import ( from esphome.core import CORE, CoroPriority, EsphomeError, coroutine_with_priority from esphome.helpers import copy_file_if_changed, read_file, write_file_if_changed +from . import boards from .const import KEY_BOARD, KEY_PIO_FILES, KEY_RP2040, rp2040_ns # force import gpio to register pin schema @@ -35,6 +36,23 @@ AUTO_LOAD = ["preferences"] IS_TARGET_PLATFORM = True +def get_board() -> str: + """Return the configured board name.""" + return CORE.data[KEY_RP2040][KEY_BOARD] + + +def board_has_wifi() -> bool: + """Return True if the configured board has WiFi (CYW43 wireless chip). + + Returns True for unknown/custom boards to avoid rejecting valid + configurations for boards not in the generated list. + """ + board_info = boards.BOARDS.get(get_board()) + if board_info is None: + return True + return board_info.get("wifi", False) + + def set_core_data(config): CORE.data[KEY_RP2040] = {} CORE.data[KEY_CORE][KEY_TARGET_PLATFORM] = PLATFORM_RP2040 diff --git a/esphome/components/rp2040/boards.py b/esphome/components/rp2040/boards.py index c99934567a..aac12eae5a 100644 --- a/esphome/components/rp2040/boards.py +++ b/esphome/components/rp2040/boards.py @@ -1910,6 +1910,7 @@ BOARDS = { "name": "Pimoroni PicoPlus2W", "mcu": "rp2350", "max_pin": 47, + "wifi": True, "max_virtual_pin": 64, }, "pimoroni_plasma2040": { @@ -1926,6 +1927,7 @@ BOARDS = { "name": "Pimoroni Plasma2350W", "mcu": "rp2350", "max_pin": 47, + "wifi": True, }, "pimoroni_servo2040": { "name": "Pimoroni Servo2040", @@ -1976,12 +1978,14 @@ BOARDS = { "name": "Raspberry Pi Pico 2W", "mcu": "rp2350", "max_pin": 47, + "wifi": True, "max_virtual_pin": 64, }, "rpipicow": { "name": "Raspberry Pi Pico W", "mcu": "rp2040", "max_pin": 29, + "wifi": True, "max_virtual_pin": 64, }, "sea_picro": { @@ -2013,6 +2017,7 @@ BOARDS = { "name": "Soldered Electronics NULA RP2350", "mcu": "rp2350", "max_pin": 47, + "wifi": True, }, "solderparty_rp2040_stamp": { "name": "Solder Party RP2040 Stamp", @@ -2038,6 +2043,7 @@ BOARDS = { "name": "SparkFun IoT RedBoard RP2350", "mcu": "rp2350", "max_pin": 47, + "wifi": True, }, "sparkfun_micromodrp2040": { "name": "SparkFun MicroMod RP2040", @@ -2063,18 +2069,21 @@ BOARDS = { "name": "SparkFun Thing Plus RP2350", "mcu": "rp2350", "max_pin": 47, + "wifi": True, "max_virtual_pin": 64, }, "sparkfun_xrp_controller": { "name": "SparkFun XRP Controller", "mcu": "rp2350", "max_pin": 47, + "wifi": True, "max_virtual_pin": 64, }, "sparkfun_xrp_controller_beta": { "name": "SparkFun XRP Controller (Beta)", "mcu": "rp2040", "max_pin": 29, + "wifi": True, "max_virtual_pin": 64, }, "upesy_rp2040_devkit": { @@ -2161,6 +2170,7 @@ BOARDS = { "name": "Waveshare RP2350B Plus W", "mcu": "rp2350", "max_pin": 47, + "wifi": True, }, "wiznet_5100s_evb_pico": { "name": "WIZnet W5100S-EVB-Pico", diff --git a/esphome/components/rp2040/generate_boards.py b/esphome/components/rp2040/generate_boards.py index 7ea02d185e..8af261396c 100644 --- a/esphome/components/rp2040/generate_boards.py +++ b/esphome/components/rp2040/generate_boards.py @@ -78,11 +78,17 @@ def load_boards(arduino_pico_path: Path) -> tuple[dict, dict]: display_name = f"{vendor} {name}".strip() if vendor else name - boards[board_name] = { + extra_flags = build.get("extra_flags", "") + has_wifi = "PICO_CYW43_SUPPORTED=1" in extra_flags + + board_entry: dict = { "name": display_name, "mcu": mcu, "max_pin": MCU_MAX_PIN.get(mcu, DEFAULT_MAX_PIN), } + if has_wifi: + board_entry["wifi"] = True + boards[board_name] = board_entry # Get pins for this variant if variant not in variant_pins_cache: diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index 9f73b1cc6f..33557f03c7 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -235,6 +235,14 @@ def validate_variant(_): variant = get_esp32_variant() if variant in NO_WIFI_VARIANTS and "esp32_hosted" not in fv.full_config.get(): raise cv.Invalid(f"WiFi requires component esp32_hosted on {variant}") + if CORE.is_rp2040: + from esphome.components.rp2040 import board_has_wifi, get_board + + if not board_has_wifi(): + raise cv.Invalid( + f"Board '{get_board()}' does not have WiFi support (no CYW43 wireless chip). " + f"Use a WiFi-capable board like 'rpipicow' or 'rpipico2w'." + ) def _apply_min_auth_mode_default(config): diff --git a/tests/unit_tests/components/test_rp2040_generate_boards.py b/tests/unit_tests/components/test_rp2040_generate_boards.py index 2e40ed08ba..551e88f6f6 100644 --- a/tests/unit_tests/components/test_rp2040_generate_boards.py +++ b/tests/unit_tests/components/test_rp2040_generate_boards.py @@ -59,6 +59,7 @@ def _add_board( vendor: str = "", name: str | None = None, pins_header: str | None = None, + extra_flags: str = "", ) -> None: """Add a board JSON and variant to the fake arduino-pico tree.""" if variant is None: @@ -69,11 +70,15 @@ def _add_board( json_dir = arduino_pico / "tools" / "json" variants_dir = arduino_pico / "variants" + build: dict = { + "mcu": mcu, + "variant": variant, + } + if extra_flags: + build["extra_flags"] = extra_flags + board_json = { - "build": { - "mcu": mcu, - "variant": variant, - }, + "build": build, "name": name, "vendor": vendor, } @@ -271,3 +276,35 @@ def test_placeholder_pins_not_treated_as_virtual(arduino_pico: Path) -> None: assert "MISO" not in board_pins["badpin"] assert boards["badpin"]["max_virtual_pin"] == 64 + + +def test_cyw43_supported_flag_sets_wifi(arduino_pico: Path) -> None: + """Boards with PICO_CYW43_SUPPORTED=1 in extra_flags should have wifi=True.""" + _add_board( + arduino_pico, + "rpipicow", + vendor="Raspberry Pi", + name="Pico W", + pins_header=PICOW_PINS_HEADER, + extra_flags="-DARDUINO_RASPBERRY_PI_PICO_W -DPICO_CYW43_SUPPORTED=1 -DCYW43_PIN_WL_DYNAMIC=1", + ) + + _, boards = load_boards(arduino_pico) + + assert boards["rpipicow"]["wifi"] is True + + +def test_board_without_cyw43_has_no_wifi(arduino_pico: Path) -> None: + """Boards without PICO_CYW43_SUPPORTED should not have wifi field.""" + _add_board( + arduino_pico, + "rpipico", + vendor="Raspberry Pi", + name="Pico", + pins_header=PICO_PINS_HEADER, + extra_flags="-DARDUINO_RASPBERRY_PI_PICO", + ) + + _, boards = load_boards(arduino_pico) + + assert "wifi" not in boards["rpipico"] From d59c006ff9067c8f7b6a439548f5ee0ef0c85e43 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 19 Mar 2026 20:56:51 -1000 Subject: [PATCH 005/166] [uart] Fix UART0 default pin IOMUX loopback on ESP32 (#14978) --- .../uart/uart_component_esp_idf.cpp | 33 ++++++++++++++++--- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/esphome/components/uart/uart_component_esp_idf.cpp b/esphome/components/uart/uart_component_esp_idf.cpp index 47ddf1a38d..8168e49805 100644 --- a/esphome/components/uart/uart_component_esp_idf.cpp +++ b/esphome/components/uart/uart_component_esp_idf.cpp @@ -7,7 +7,9 @@ #include "esphome/core/log.h" #include "esphome/core/gpio.h" #include "driver/gpio.h" +#include "esp_private/gpio.h" #include "soc/gpio_num.h" +#include "soc/uart_pins.h" #ifdef USE_UART_WAKE_LOOP_ON_RX #include "esphome/core/application.h" @@ -21,6 +23,20 @@ namespace esphome::uart { static const char *const TAG = "uart.idf"; +/// Check if a pin number matches one of the default UART0 GPIO pins. +/// These pins may have residual IOMUX state from the ROM bootloader that +/// must be cleared before UART reconfiguration. +/// +/// ESP-IDF's uart_set_pin() has an asymmetry: when routing TX via GPIO matrix, +/// it calls gpio_func_sel(PIN_FUNC_GPIO) to clear IOMUX, but for RX it only +/// calls gpio_input_enable() which does NOT clear the IOMUX function select. +/// If a default UART0 TX pin (configured as TX via IOMUX during boot) is later +/// reassigned as RX via GPIO matrix, the old IOMUX TX function remains active, +/// causing TX data to loop back into RX on the same pin. +static constexpr bool is_default_uart0_pin(int8_t pin_num) { + return pin_num == U0TXD_GPIO_NUM || pin_num == U0RXD_GPIO_NUM; +} + uart_config_t IDFUARTComponent::get_config_() { uart_parity_t parity = UART_PARITY_DISABLE; if (this->parity_ == UART_CONFIG_PARITY_EVEN) { @@ -131,6 +147,19 @@ void IDFUARTComponent::load_settings(bool dump_config) { return; } + int8_t tx = this->tx_pin_ != nullptr ? this->tx_pin_->get_pin() : -1; + int8_t rx = this->rx_pin_ != nullptr ? this->rx_pin_->get_pin() : -1; + int8_t flow_control = this->flow_control_pin_ != nullptr ? this->flow_control_pin_->get_pin() : -1; + + // Clear residual IOMUX function on UART0 default pins left by the ROM bootloader. + // See is_default_uart0_pin() comment for details on the ESP-IDF uart_set_pin() bug. + if (is_default_uart0_pin(tx)) { + gpio_func_sel(static_cast(tx), PIN_FUNC_GPIO); + } + if (is_default_uart0_pin(rx)) { + gpio_func_sel(static_cast(rx), PIN_FUNC_GPIO); + } + auto setup_pin_if_needed = [](InternalGPIOPin *pin) { if (!pin) { return; @@ -146,10 +175,6 @@ void IDFUARTComponent::load_settings(bool dump_config) { setup_pin_if_needed(this->tx_pin_); } - int8_t tx = this->tx_pin_ != nullptr ? this->tx_pin_->get_pin() : -1; - int8_t rx = this->rx_pin_ != nullptr ? this->rx_pin_->get_pin() : -1; - int8_t flow_control = this->flow_control_pin_ != nullptr ? this->flow_control_pin_->get_pin() : -1; - uint32_t invert = 0; if (this->tx_pin_ != nullptr && this->tx_pin_->is_inverted()) { invert |= UART_SIGNAL_TXD_INV; From 12b3aec5672312332c77eaf184f64b2ec32e74a5 Mon Sep 17 00:00:00 2001 From: Keith Roehrenbeck Date: Fri, 20 Mar 2026 15:11:57 -0500 Subject: [PATCH 006/166] [ld2450] Fix zone target counts including untracked ghost targets (#15026) --- esphome/components/ld2450/ld2450.cpp | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/esphome/components/ld2450/ld2450.cpp b/esphome/components/ld2450/ld2450.cpp index 0a1147c924..6230a8c30b 100644 --- a/esphome/components/ld2450/ld2450.cpp +++ b/esphome/components/ld2450/ld2450.cpp @@ -530,10 +530,11 @@ void LD2450Component::handle_periodic_data_() { } #endif - // Store target info for zone target count - this->target_info_[index].x = tx; - this->target_info_[index].y = ty; - this->target_info_[index].is_moving = is_moving; + // Store target info for zone target count. Zero out untracked targets (td==0) + // so stale coordinates don't produce ghost counts in count_targets_in_zone_(). + this->target_info_[index].x = (td > 0) ? tx : 0; + this->target_info_[index].y = (td > 0) ? ty : 0; + this->target_info_[index].is_moving = (td > 0) && is_moving; } // End loop thru targets From 5a9977cf5c1ff918f6c20e2281301f82ed253ecd Mon Sep 17 00:00:00 2001 From: Javier Peletier Date: Fri, 20 Mar 2026 21:35:41 +0100 Subject: [PATCH 007/166] [lvgl] Fix arc indicator widget not registered in widget_map (#14986) --- esphome/components/lvgl/widgets/meter.py | 2 +- tests/components/lvgl/lvgl-package.yaml | 12 +++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/esphome/components/lvgl/widgets/meter.py b/esphome/components/lvgl/widgets/meter.py index d45371b3a7..63cc645f22 100644 --- a/esphome/components/lvgl/widgets/meter.py +++ b/esphome/components/lvgl/widgets/meter.py @@ -406,7 +406,7 @@ class MeterType(WidgetType): lv.scale_section_set_style( tvar, LV_PART.MAIN, await arc_style.get_var() ) - lw = Widget(tvar, arc_indicator_type) + lw = Widget.create(iid, tvar, arc_indicator_type) await set_indicator_values(lw, v) if t == CONF_TICK_STYLE: diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml index 7d96b12a01..606f57d6a1 100644 --- a/tests/components/lvgl/lvgl-package.yaml +++ b/tests/components/lvgl/lvgl-package.yaml @@ -37,7 +37,11 @@ lvgl: on_resume: logger.log: LVGL has resumed on_boot: - logger.log: LVGL has started + - logger.log: LVGL has started + - lvgl.indicator.update: + id: meter_arc_indicator + start_value: 0 + end_value: 180 bg_color: light_blue disp_bg_color: color_id disp_bg_image: cat_image @@ -1110,6 +1114,12 @@ lvgl: color: 0xA0A0A0 length: 80% opa: 0% + - arc: + id: meter_arc_indicator + color: 0xFF0000 + width: 6 + start_value: 0 + end_value: 360 - id: page3 layout: Horizontal pad_all: 6px From 7257bed1e95ddc4bde5a9c61d83a93b57c88566a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 20 Mar 2026 10:41:20 -1000 Subject: [PATCH 008/166] Bump CodSpeedHQ/action from 4.11.1 to 4.12.1 (#15024) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ead87ad087..965e23870d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -339,7 +339,7 @@ jobs: echo "binary=$BINARY" >> $GITHUB_OUTPUT - name: Run CodSpeed benchmarks - uses: CodSpeedHQ/action@281164b0f014a4e7badd2c02cecad9b595b70537 # v4 + uses: CodSpeedHQ/action@1c8ae4843586d3ba879736b7f6b7b0c990757fab # v4 with: run: ${{ steps.build.outputs.binary }} mode: simulation From a3fd1d5d00714968e630ec692893752b3d16e2e7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 20 Mar 2026 10:41:45 -1000 Subject: [PATCH 009/166] Bump github/codeql-action from 4.33.0 to 4.34.1 (#15023) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/codeql.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 2ef1a5af31..6baab70b42 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -58,7 +58,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@b1bff81932f5cdfc8695c7752dcee935dcd061c8 # v4.33.0 + uses: github/codeql-action/init@38697555549f1db7851b81482ff19f1fa5c4fedc # v4.34.1 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} @@ -86,6 +86,6 @@ jobs: exit 1 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@b1bff81932f5cdfc8695c7752dcee935dcd061c8 # v4.33.0 + uses: github/codeql-action/analyze@38697555549f1db7851b81482ff19f1fa5c4fedc # v4.34.1 with: category: "/language:${{matrix.language}}" From 9e7cdaf4758ff997cf5f385946aab97c992213f2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 20 Mar 2026 11:10:40 -1000 Subject: [PATCH 010/166] Bump aioesphomeapi from 44.6.1 to 44.6.2 (#15027) 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 9e2f4efe3e..10e56c3b49 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.19 esptool==5.2.0 click==8.3.1 esphome-dashboard==20260210.0 -aioesphomeapi==44.6.1 +aioesphomeapi==44.6.2 zeroconf==0.148.0 puremagic==1.30 ruamel.yaml==0.19.1 # dashboard_import From 896b6ec8c9acd581a2d95d66ea00b9d617ad36ed Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 20 Mar 2026 12:06:23 -1000 Subject: [PATCH 011/166] [api] Increase noise handshake timeout to 60s for slow WiFi environments (#15022) --- esphome/components/api/api_connection.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index d55b5dffb6..40c27b224b 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -64,7 +64,11 @@ static constexpr uint32_t KEEPALIVE_DISCONNECT_TIMEOUT = (KEEPALIVE_TIMEOUT_MS * // A stalled handshake from a buggy client or network glitch holds a connection // slot, which can prevent legitimate clients from reconnecting. Also hardens // against the less likely case of intentional connection slot exhaustion. -static constexpr uint32_t HANDSHAKE_TIMEOUT_MS = 15000; +// +// 60s is intentionally high: on ESP8266 with power_save_mode: LIGHT and weak +// WiFi (-70 dBm+), TCP retransmissions push real-world handshake times to +// 28-30s. See https://github.com/esphome/esphome/issues/14999 +static constexpr uint32_t HANDSHAKE_TIMEOUT_MS = 60000; static constexpr auto ESPHOME_VERSION_REF = StringRef::from_lit(ESPHOME_VERSION); From 5e516e78e43e7b9b61fb334f5832ac671ec20b74 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 20 Mar 2026 12:13:49 -1000 Subject: [PATCH 012/166] [wifi] Fix ESP8266 power_save_mode mapping (LIGHT/HIGH were swapped) (#15029) --- esphome/components/wifi/wifi_component_esp8266.cpp | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index 0bf7934878..5514f1c6be 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -92,13 +92,23 @@ bool WiFiComponent::wifi_mode_(optional sta, optional ap) { return ret; } bool WiFiComponent::wifi_apply_power_save_() { + // ESP8266 sleep types have confusing names — LIGHT_SLEEP_T is the MORE aggressive mode. + // SDK enum: NONE_SLEEP_T=0, LIGHT_SLEEP_T=1, MODEM_SLEEP_T=2 + // https://github.com/esp8266/Arduino/blob/3.1.2/tools/sdk/include/user_interface.h#L447-L451 + // Arduino ESP32 compat confirms: WIFI_PS_MIN_MODEM=MODEM_SLEEP, WIFI_PS_MAX_MODEM=LIGHT_SLEEP + // https://github.com/esp8266/Arduino/blob/3.1.2/libraries/ESP8266WiFi/src/ESP8266WiFiType.h#L53-L55 sleep_type_t power_save; switch (this->power_save_) { case WIFI_POWER_SAVE_LIGHT: - power_save = LIGHT_SLEEP_T; + // MODEM_SLEEP_T: only the WiFi modem sleeps between DTIM beacons, CPU stays active. + // Matches ESP32's WIFI_PS_MIN_MODEM. + power_save = MODEM_SLEEP_T; break; case WIFI_POWER_SAVE_HIGH: - power_save = MODEM_SLEEP_T; + // LIGHT_SLEEP_T: both WiFi modem AND CPU suspend between DTIM beacons. + // Most aggressive — prevents TCP processing during sleep. Matches ESP32's WIFI_PS_MAX_MODEM. + // See https://github.com/esphome/esphome/issues/14999 + power_save = LIGHT_SLEEP_T; break; case WIFI_POWER_SAVE_NONE: default: From ed8c062d9fc688e97758e702fb743d966334db5b Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 20 Mar 2026 19:53:02 -0400 Subject: [PATCH 013/166] [esp32_touch] Fix initial state never published when sensor untouched (#15032) --- esphome/components/esp32_touch/esp32_touch.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/esphome/components/esp32_touch/esp32_touch.cpp b/esphome/components/esp32_touch/esp32_touch.cpp index e7124ce92f..0d331b29d6 100644 --- a/esphome/components/esp32_touch/esp32_touch.cpp +++ b/esphome/components/esp32_touch/esp32_touch.cpp @@ -360,11 +360,16 @@ void ESP32TouchComponent::loop() { } // Publish initial OFF state for sensors that haven't received events yet + bool all_initial_published = true; for (auto *child : this->children_) { this->publish_initial_state_if_needed_(child, now); + if (!child->initial_state_published_) { + all_initial_published = false; + } } - if (!this->setup_mode_) { + // Only disable loop once all initial states are published + if (!this->setup_mode_ && all_initial_published) { this->disable_loop(); } } From 0b01f9fc42aa7e848dfbab78500d8ba97778dc29 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 20 Mar 2026 13:57:51 -1000 Subject: [PATCH 014/166] [web_server] Increase httpd task stack size to prevent stack overflow (#14997) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/web_server_idf/web_server_idf.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/esphome/components/web_server_idf/web_server_idf.cpp b/esphome/components/web_server_idf/web_server_idf.cpp index 60816fc6dd..fb0c17c854 100644 --- a/esphome/components/web_server_idf/web_server_idf.cpp +++ b/esphome/components/web_server_idf/web_server_idf.cpp @@ -121,7 +121,10 @@ void AsyncWebServer::begin() { if (this->server_) { this->end(); } + // Default httpd stack is defined by ESP-IDF. Increase to accommodate SerializationBuffer's + // 640-byte stack buffer used by web_server JSON request handlers. httpd_config_t config = HTTPD_DEFAULT_CONFIG(); + config.stack_size = config.stack_size + 256; config.server_port = this->port_; config.uri_match_fn = [](const char * /*unused*/, const char * /*unused*/, size_t /*unused*/) { return true; }; // Always enable LRU purging to handle socket exhaustion gracefully. From 8fa2e75afaacb55d6b8aaa3ca49f746789ab8b83 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 20 Mar 2026 13:58:02 -1000 Subject: [PATCH 015/166] [core] Add copy() method to StringRef for std::string compatibility (#15028) --- esphome/core/string_ref.h | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/esphome/core/string_ref.h b/esphome/core/string_ref.h index 6047202753..34ba2474b2 100644 --- a/esphome/core/string_ref.h +++ b/esphome/core/string_ref.h @@ -76,6 +76,15 @@ class StringRef { constexpr bool empty() const { return len_ == 0; } constexpr const_reference operator[](size_type pos) const { return *(base_ + pos); } + /// Copy characters to destination buffer (std::string::copy-like, but returns 0 instead of throwing on out-of-range) + size_type copy(char *dest, size_type count, size_type pos = 0) const { + if (pos >= len_) + return 0; + size_type actual = (count > len_ - pos) ? len_ - pos : count; + std::memcpy(dest, base_ + pos, actual); + return actual; + } + std::string str() const { return std::string(base_, len_); } const uint8_t *byte() const { return reinterpret_cast(base_); } From a9a8f4cb3bf9f304abcd872995dc91aa988859ac Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 20 Mar 2026 13:58:14 -1000 Subject: [PATCH 016/166] [time] Fix timezone_offset() and recalc_timestamp_local() always returning UTC (#14996) --- esphome/core/time.cpp | 16 ++++++---------- esphome/core/time.h | 2 ++ 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/esphome/core/time.cpp b/esphome/core/time.cpp index 73ba0a9be7..6add82e7d1 100644 --- a/esphome/core/time.cpp +++ b/esphome/core/time.cpp @@ -283,19 +283,15 @@ void ESPTime::recalc_timestamp_local() { bool dst_valid = time::is_in_dst(utc_if_dst, tz); bool std_valid = !time::is_in_dst(utc_if_std, tz); - if (dst_valid && std_valid) { - // Ambiguous time (repeated hour during fall-back) - prefer standard time - this->timestamp = utc_if_std; - } else if (dst_valid) { + if (dst_valid && !std_valid) { // Only DST interpretation is valid this->timestamp = utc_if_dst; - } else if (std_valid) { - // Only standard interpretation is valid - this->timestamp = utc_if_std; } else { - // Invalid time (skipped hour during spring-forward) - // libc normalizes forward: 02:30 CST -> 08:30 UTC -> 03:30 CDT - // Using std offset achieves this since the UTC result falls during DST + // All other cases use standard offset: + // - Both valid (ambiguous fall-back repeated hour): prefer standard time + // - Only standard valid: straightforward + // - Neither valid (spring-forward skipped hour): std offset normalizes + // forward to match libc mktime(), e.g. 02:30 CST -> 03:30 CDT this->timestamp = utc_if_std; } #else diff --git a/esphome/core/time.h b/esphome/core/time.h index 874f0db4b4..1716c51ffd 100644 --- a/esphome/core/time.h +++ b/esphome/core/time.h @@ -1,5 +1,7 @@ #pragma once +#include "esphome/core/defines.h" + #include #include #include From 2d39cc2540e877f7bec755b74c2cdf60e625c6b0 Mon Sep 17 00:00:00 2001 From: Daniel Kent <129895318+danielkent-net@users.noreply.github.com> Date: Fri, 20 Mar 2026 20:38:04 -0400 Subject: [PATCH 017/166] [spa06_i2c] Add SPA06-003 Temperature and Pressure Sensor - I2C support (Part 2 of 3) (#14522) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- CODEOWNERS | 1 + esphome/components/spa06_i2c/__init__.py | 0 esphome/components/spa06_i2c/sensor.py | 23 +++++++++++++++++++ esphome/components/spa06_i2c/spa06_i2c.cpp | 14 +++++++++++ esphome/components/spa06_i2c/spa06_i2c.h | 20 ++++++++++++++++ tests/components/spa06_i2c/common.yaml | 15 ++++++++++++ .../components/spa06_i2c/test.esp32-idf.yaml | 4 ++++ .../spa06_i2c/test.esp8266-ard.yaml | 4 ++++ .../components/spa06_i2c/test.rp2040-ard.yaml | 4 ++++ 9 files changed, 85 insertions(+) create mode 100644 esphome/components/spa06_i2c/__init__.py create mode 100644 esphome/components/spa06_i2c/sensor.py create mode 100644 esphome/components/spa06_i2c/spa06_i2c.cpp create mode 100644 esphome/components/spa06_i2c/spa06_i2c.h create mode 100644 tests/components/spa06_i2c/common.yaml create mode 100644 tests/components/spa06_i2c/test.esp32-idf.yaml create mode 100644 tests/components/spa06_i2c/test.esp8266-ard.yaml create mode 100644 tests/components/spa06_i2c/test.rp2040-ard.yaml diff --git a/CODEOWNERS b/CODEOWNERS index 5869925d7c..e3e09cbc11 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -458,6 +458,7 @@ esphome/components/socket/* @esphome/core esphome/components/sonoff_d1/* @anatoly-savchenkov esphome/components/sound_level/* @kahrendt esphome/components/spa06_base/* @danielkent-net +esphome/components/spa06_i2c/* @danielkent-net esphome/components/speaker/* @jesserockz @kahrendt esphome/components/speaker/media_player/* @kahrendt @synesthesiam esphome/components/speaker_source/* @kahrendt diff --git a/esphome/components/spa06_i2c/__init__.py b/esphome/components/spa06_i2c/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/spa06_i2c/sensor.py b/esphome/components/spa06_i2c/sensor.py new file mode 100644 index 0000000000..b48a5bca50 --- /dev/null +++ b/esphome/components/spa06_i2c/sensor.py @@ -0,0 +1,23 @@ +import esphome.codegen as cg +from esphome.components import i2c +import esphome.config_validation as cv + +from ..spa06_base import CONFIG_SCHEMA_BASE, to_code_base + +AUTO_LOAD = ["spa06_base"] +CODEOWNERS = ["@danielkent-net"] +DEPENDENCIES = ["i2c"] + +spa06_ns = cg.esphome_ns.namespace("spa06_i2c") +SPA06I2CComponent = spa06_ns.class_( + "SPA06I2CComponent", cg.PollingComponent, i2c.I2CDevice +) + +CONFIG_SCHEMA = CONFIG_SCHEMA_BASE.extend( + i2c.i2c_device_schema(default_address=0x77) +).extend({cv.GenerateID(): cv.declare_id(SPA06I2CComponent)}) + + +async def to_code(config): + var = await to_code_base(config) + await i2c.register_i2c_device(var, config) diff --git a/esphome/components/spa06_i2c/spa06_i2c.cpp b/esphome/components/spa06_i2c/spa06_i2c.cpp new file mode 100644 index 0000000000..4970b0822d --- /dev/null +++ b/esphome/components/spa06_i2c/spa06_i2c.cpp @@ -0,0 +1,14 @@ +#include "spa06_i2c.h" +#include "esphome/core/hal.h" +#include "esphome/core/log.h" + +namespace esphome::spa06_i2c { + +static const char *const TAG = "spa06_i2c"; + +void SPA06I2CComponent::dump_config() { + LOG_I2C_DEVICE(this); + SPA06Component::dump_config(); +} + +} // namespace esphome::spa06_i2c diff --git a/esphome/components/spa06_i2c/spa06_i2c.h b/esphome/components/spa06_i2c/spa06_i2c.h new file mode 100644 index 0000000000..6b4bce3a4e --- /dev/null +++ b/esphome/components/spa06_i2c/spa06_i2c.h @@ -0,0 +1,20 @@ +#pragma once +#include "esphome/components/spa06_base/spa06_base.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome::spa06_i2c { + +class SPA06I2CComponent : public spa06_base::SPA06Component, public i2c::I2CDevice { + public: + bool spa_read_byte(uint8_t a_register, uint8_t *data) override { return read_byte(a_register, data); } + bool spa_write_byte(uint8_t a_register, uint8_t data) override { return write_byte(a_register, data); } + bool spa_read_bytes(uint8_t a_register, uint8_t *data, size_t len) override { + return read_bytes(a_register, data, len); + } + bool spa_write_bytes(uint8_t a_register, uint8_t *data, size_t len) override { + return write_bytes(a_register, data, len); + } + void dump_config() override; +}; + +} // namespace esphome::spa06_i2c diff --git a/tests/components/spa06_i2c/common.yaml b/tests/components/spa06_i2c/common.yaml new file mode 100644 index 0000000000..d2be0e3ac9 --- /dev/null +++ b/tests/components/spa06_i2c/common.yaml @@ -0,0 +1,15 @@ +sensor: + - platform: spa06_i2c + i2c_id: i2c_bus + address: 0x77 + temperature: + id: spa06_i2c_temperature + name: Outside Temperature + sample_rate: 1 + oversampling: NONE + pressure: + name: Outside Pressure + id: spa06_i2c_pressure + sample_rate: 25p4 + oversampling: 16X + update_interval: 15s diff --git a/tests/components/spa06_i2c/test.esp32-idf.yaml b/tests/components/spa06_i2c/test.esp32-idf.yaml new file mode 100644 index 0000000000..b47e39c389 --- /dev/null +++ b/tests/components/spa06_i2c/test.esp32-idf.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml + +<<: !include common.yaml diff --git a/tests/components/spa06_i2c/test.esp8266-ard.yaml b/tests/components/spa06_i2c/test.esp8266-ard.yaml new file mode 100644 index 0000000000..4a98b9388a --- /dev/null +++ b/tests/components/spa06_i2c/test.esp8266-ard.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml + +<<: !include common.yaml diff --git a/tests/components/spa06_i2c/test.rp2040-ard.yaml b/tests/components/spa06_i2c/test.rp2040-ard.yaml new file mode 100644 index 0000000000..319a7c71a6 --- /dev/null +++ b/tests/components/spa06_i2c/test.rp2040-ard.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml + +<<: !include common.yaml From 12ead0408ab97d21f2d01fc691db2baed7a99d9f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 20 Mar 2026 15:00:56 -1000 Subject: [PATCH 018/166] [gpio] Use constexpr uint32_t timer ID for interlock timeout (#15010) --- esphome/components/gpio/switch/gpio_switch.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/esphome/components/gpio/switch/gpio_switch.cpp b/esphome/components/gpio/switch/gpio_switch.cpp index 9043a6a493..d461fab051 100644 --- a/esphome/components/gpio/switch/gpio_switch.cpp +++ b/esphome/components/gpio/switch/gpio_switch.cpp @@ -5,6 +5,7 @@ namespace esphome { namespace gpio { static const char *const TAG = "switch.gpio"; +static constexpr uint32_t INTERLOCK_TIMEOUT_ID = 0; float GPIOSwitch::get_setup_priority() const { return setup_priority::HARDWARE; } void GPIOSwitch::setup() { @@ -51,7 +52,7 @@ void GPIOSwitch::write_state(bool state) { } } if (found && this->interlock_wait_time_ != 0) { - this->set_timeout("interlock", this->interlock_wait_time_, [this, state] { + this->set_timeout(INTERLOCK_TIMEOUT_ID, this->interlock_wait_time_, [this, state] { // Don't write directly, call the function again // (some other switch may have changed state while we were waiting) this->write_state(state); @@ -61,7 +62,7 @@ void GPIOSwitch::write_state(bool state) { } else if (this->interlock_wait_time_ != 0) { // If we are switched off during the interlock wait time, cancel any pending // re-activations - this->cancel_timeout("interlock"); + this->cancel_timeout(INTERLOCK_TIMEOUT_ID); } this->pin_->digital_write(state); From 391ffe34f87158a0b308634018be3a03ac33814a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 20 Mar 2026 15:01:11 -1000 Subject: [PATCH 019/166] [rp2040] Fix get_mac_address_raw to use ethernet MAC when WiFi unavailable (#15033) --- esphome/components/rp2040/helpers.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/esphome/components/rp2040/helpers.cpp b/esphome/components/rp2040/helpers.cpp index ad69192af9..e360b45ebb 100644 --- a/esphome/components/rp2040/helpers.cpp +++ b/esphome/components/rp2040/helpers.cpp @@ -10,6 +10,7 @@ #include // For cyw43_arch_lwip_begin/end (LwIPLock) #elif defined(USE_ETHERNET) #include // For ethernet_arch_lwip_begin/end (LwIPLock) +#include "esphome/components/ethernet/ethernet_component.h" #endif #include #include @@ -71,6 +72,8 @@ LwIPLock::~LwIPLock() {} void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter) #ifdef USE_WIFI WiFi.macAddress(mac); +#elif defined(USE_ETHERNET) + ethernet::global_eth_component->get_eth_mac_address_raw(mac); #endif } From 51335e88301d8ae4f6872cb8546c799ef300f964 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 20 Mar 2026 15:01:30 -1000 Subject: [PATCH 020/166] [ledc] Fix deprecated intr_type warning on ESP-IDF 6.0+ (#15009) --- esphome/components/ledc/ledc_output.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/components/ledc/ledc_output.cpp b/esphome/components/ledc/ledc_output.cpp index d2f2d72acb..5b7b6c7ee6 100644 --- a/esphome/components/ledc/ledc_output.cpp +++ b/esphome/components/ledc/ledc_output.cpp @@ -193,7 +193,9 @@ void LEDCOutput::setup() { chan_conf.gpio_num = static_cast(this->pin_->get_pin()); chan_conf.speed_mode = speed_mode; chan_conf.channel = chan_num; +#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(6, 0, 0) chan_conf.intr_type = LEDC_INTR_DISABLE; +#endif chan_conf.timer_sel = timer_num; chan_conf.duty = this->inverted_ == this->pin_->is_inverted() ? 0 : (1U << this->bit_depth_); chan_conf.hpoint = hpoint; From edf5542559a4868c57df319ad89e4a7ebe829658 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 20 Mar 2026 15:05:17 -1000 Subject: [PATCH 021/166] [analyze-memory] Attribute extern C symbols to components via source file mapping (#15006) --- esphome/analyze_memory/__init__.py | 125 +++++++++++++++++++++++++++++ esphome/analyze_memory/const.py | 1 - 2 files changed, 125 insertions(+), 1 deletion(-) diff --git a/esphome/analyze_memory/__init__.py b/esphome/analyze_memory/__init__.py index 7954c22822..48ecf2c1dc 100644 --- a/esphome/analyze_memory/__init__.py +++ b/esphome/analyze_memory/__init__.py @@ -201,6 +201,9 @@ class MemoryAnalyzer: self._cswtch_symbols: list[tuple[str, int, str, str]] = [] # Library symbol mapping: symbol_name -> library_name self._lib_symbol_map: dict[str, str] = {} + # Source file symbol mapping: symbol_name -> component_name + # Used for extern "C" and other symbols without C++ namespace + self._source_symbol_map: dict[str, str] = {} # Library dir to name mapping: "lib641" -> "espsoftwareserial", # "espressif__mdns" -> "mdns" self._lib_hash_to_name: dict[str, str] = {} @@ -214,6 +217,7 @@ class MemoryAnalyzer: self._parse_sections() self._parse_symbols() self._scan_libraries() + self._scan_source_symbols() self._categorize_symbols() self._analyze_cswtch_symbols() self._analyze_sdk_libraries() @@ -363,6 +367,11 @@ class MemoryAnalyzer: if lib_name := self._lib_symbol_map.get(symbol_name): return f"{_COMPONENT_PREFIX_LIB}{lib_name}" + # Check source file mapping (catches extern "C" functions in ESPHome sources) + # Must be before heuristic patterns since source attribution is authoritative + if component := self._source_symbol_map.get(symbol_name): + return component + # Check against symbol patterns for component, patterns in SYMBOL_PATTERNS.items(): if any(pattern in symbol_name for pattern in patterns): @@ -653,6 +662,7 @@ class MemoryAnalyzer: return None symbol_map: dict[str, str] = {} + source_symbol_map: dict[str, str] = {} current_symbol: str | None = None section_prefixes = (".text.", ".rodata.", ".data.", ".bss.", ".literal.") @@ -688,9 +698,18 @@ class MemoryAnalyzer: if dir_key in source_path: symbol_map[current_symbol] = lib_name break + else: + # Map ESPHome source files to components for extern "C" + # and other symbols without C++ namespace + component = self._source_file_to_component(source_path) + if component.startswith( + (_COMPONENT_PREFIX_ESPHOME, _COMPONENT_PREFIX_EXTERNAL) + ): + source_symbol_map[current_symbol] = component current_symbol = None + self._source_symbol_map = source_symbol_map return symbol_map or None def _scan_libraries(self) -> None: @@ -741,6 +760,112 @@ class MemoryAnalyzer: len(libraries), ) + def _scan_source_symbols(self) -> None: + """Scan ESPHome source object files to map extern "C" symbols to components. + + When no linker map file is available, this uses ``nm`` to scan ``.o`` files + under ``src/esphome/`` and build a symbol-to-component mapping. This catches + ``extern "C"`` functions and other symbols that lack C++ namespace prefixes. + + Skips scanning if ``_source_symbol_map`` was already populated by + ``_parse_map_file()``. + """ + if self._source_symbol_map or not self.nm_path: + return + + obj_dir = self._find_object_files_dir() + if obj_dir is None: + return + + # Find ESPHome source object files + esphome_src_dir = obj_dir / "src" / "esphome" + if not esphome_src_dir.is_dir(): + return + + obj_files = sorted(esphome_src_dir.rglob("*.o")) + if not obj_files: + return + + # Run nm with --print-file-name to get file:symbol mapping + result = run_tool( + [self.nm_path, "--print-file-name", "-g", "--defined-only"] + + [str(f) for f in obj_files], + ) + if result is None or result.returncode != 0: + _LOGGER.debug("nm scan of source objects failed") + return + + self._source_symbol_map = self._parse_nm_source_output(result.stdout, obj_dir) + if self._source_symbol_map: + _LOGGER.info( + "Built source symbol map from nm: %d symbols", + len(self._source_symbol_map), + ) + + def _parse_nm_source_output(self, output: str, base_dir: Path) -> dict[str, str]: + """Parse nm output to map non-namespaced symbols to ESPHome components. + + Extracts global defined symbols from ESPHome source object files that + don't use C++ namespacing (e.g. ``extern "C"`` functions). + + Args: + output: Raw stdout from ``nm --print-file-name -g --defined-only`` + or ``nm --print-file-name -S``. + base_dir: Build directory for computing relative paths. + + Returns: + Dict mapping symbol names to component names. + """ + source_map: dict[str, str] = {} + for line in output.splitlines(): + # Format: /path/to/file.o: addr type name + # or: /path/to/file.o: addr size type name (with -S) + colon_idx = line.rfind(".o:") + if colon_idx == -1: + continue + + file_path = line[: colon_idx + 2] + fields = line[colon_idx + 3 :].split() + if len(fields) < 3: + continue + + # With -S flag, format is: addr size type name + # Without -S flag: addr type name + # type is a single char; size is hex digits + # Detect by checking if fields[1] is a single uppercase letter (type) + if len(fields[1]) == 1 and fields[1].isalpha(): + # addr type name + sym_type = fields[1] + symbol_name = fields[2] + elif len(fields) >= 4: + # addr size type name + sym_type = fields[2] + symbol_name = fields[3] + else: + continue + + # Only global defined symbols (uppercase type) + if not sym_type.isupper() or sym_type == "U": + continue + + # Skip symbols already in esphome:: namespace + if symbol_name.startswith("_ZN7esphome"): + continue + + # Make path relative to base_dir for _source_file_to_component + try: + rel_path = str(Path(file_path).relative_to(base_dir)) + except ValueError: + continue + + component = self._source_file_to_component(rel_path) + if component.startswith( + (_COMPONENT_PREFIX_ESPHOME, _COMPONENT_PREFIX_EXTERNAL) + ): + source_map[symbol_name] = component + + return source_map + def _find_object_files_dir(self) -> Path | None: """Find the directory containing object files for this build. diff --git a/esphome/analyze_memory/const.py b/esphome/analyze_memory/const.py index 3bdf555ae3..0c871d0727 100644 --- a/esphome/analyze_memory/const.py +++ b/esphome/analyze_memory/const.py @@ -408,7 +408,6 @@ SYMBOL_PATTERNS = { ], "arduino_core": [ "pinMode", - "resetPins", "millis", "micros", "delay(", # More specific - Arduino delay function with parenthesis From 564d155cb6980e1b24cbd640cb014128dcb1cc92 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 20 Mar 2026 15:08:33 -1000 Subject: [PATCH 022/166] [wifi] Use LOG_STR_LITERAL for scan complete log on ESP8266 (#15001) --- esphome/components/wifi/wifi_component_esp8266.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index 5514f1c6be..f2fabb9080 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -735,7 +735,7 @@ void WiFiComponent::wifi_scan_done_callback_(void *arg, STATUS status) { } } ESP_LOGV(TAG, "Scan complete: %zu found, %zu stored%s", total, this->scan_result_.size(), - needs_full ? "" : " (filtered)"); + needs_full ? LOG_STR_LITERAL("") : LOG_STR_LITERAL(" (filtered)")); this->scan_done_ = true; #ifdef USE_WIFI_SCAN_RESULTS_LISTENERS this->pending_.scan_complete = true; // Defer listener callbacks to main loop From 7f500c4b6ef14967771cec2cb2f8785cc552b891 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 20 Mar 2026 15:08:46 -1000 Subject: [PATCH 023/166] [modbus] Fix size_t format warning in clear_rx_buffer_ (#15002) --- esphome/components/modbus/modbus.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/modbus/modbus.cpp b/esphome/components/modbus/modbus.cpp index 7a61868e6e..4146a54c87 100644 --- a/esphome/components/modbus/modbus.cpp +++ b/esphome/components/modbus/modbus.cpp @@ -415,10 +415,10 @@ void Modbus::clear_rx_buffer_(const LogString *reason, bool warn) { size_t at = this->rx_buffer_.size(); if (at > 0) { if (warn) { - ESP_LOGW(TAG, "Clearing buffer of %" PRIu32 " bytes - %s %" PRIu32 "ms after last send", at, LOG_STR_ARG(reason), + ESP_LOGW(TAG, "Clearing buffer of %zu bytes - %s %" PRIu32 "ms after last send", at, LOG_STR_ARG(reason), millis() - this->last_send_); } else { - ESP_LOGV(TAG, "Clearing buffer of %" PRIu32 " bytes - %s %" PRIu32 "ms after last send", at, LOG_STR_ARG(reason), + ESP_LOGV(TAG, "Clearing buffer of %zu bytes - %s %" PRIu32 "ms after last send", at, LOG_STR_ARG(reason), millis() - this->last_send_); } this->rx_buffer_.clear(); From 51ccad8461069ac04cd568971b9bf4b1b148e26e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 20 Mar 2026 15:09:01 -1000 Subject: [PATCH 024/166] [preferences] Shorten TAG strings across all platforms (#15004) --- esphome/components/esp32/preferences.cpp | 2 +- esphome/components/esp8266/preferences.cpp | 2 +- esphome/components/host/preferences.cpp | 2 +- esphome/components/libretiny/preferences.cpp | 2 +- esphome/components/rp2040/preferences.cpp | 2 +- esphome/components/zephyr/preferences.cpp | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/esphome/components/esp32/preferences.cpp b/esphome/components/esp32/preferences.cpp index 7260bf54e0..e88ace3e6b 100644 --- a/esphome/components/esp32/preferences.cpp +++ b/esphome/components/esp32/preferences.cpp @@ -10,7 +10,7 @@ namespace esphome::esp32 { -static const char *const TAG = "esp32.preferences"; +static const char *const TAG = "preferences"; // Buffer size for converting uint32_t to string: max "4294967295" (10 chars) + null terminator + 1 padding static constexpr size_t KEY_BUFFER_SIZE = 12; diff --git a/esphome/components/esp8266/preferences.cpp b/esphome/components/esp8266/preferences.cpp index 0b31c53ff8..906fed2b29 100644 --- a/esphome/components/esp8266/preferences.cpp +++ b/esphome/components/esp8266/preferences.cpp @@ -13,7 +13,7 @@ extern "C" { namespace esphome::esp8266 { -static const char *const TAG = "esp8266.preferences"; +static const char *const TAG = "preferences"; static constexpr uint32_t ESP_RTC_USER_MEM_START = 0x60001200; static constexpr uint32_t ESP_RTC_USER_MEM_SIZE_WORDS = 128; diff --git a/esphome/components/host/preferences.cpp b/esphome/components/host/preferences.cpp index fce3d62dda..c0be270062 100644 --- a/esphome/components/host/preferences.cpp +++ b/esphome/components/host/preferences.cpp @@ -9,7 +9,7 @@ namespace esphome::host { namespace fs = std::filesystem; -static const char *const TAG = "host.preferences"; +static const char *const TAG = "preferences"; void HostPreferences::setup_() { if (this->setup_complete_) diff --git a/esphome/components/libretiny/preferences.cpp b/esphome/components/libretiny/preferences.cpp index f22c12f1fb..344ca4a8b3 100644 --- a/esphome/components/libretiny/preferences.cpp +++ b/esphome/components/libretiny/preferences.cpp @@ -9,7 +9,7 @@ namespace esphome::libretiny { -static const char *const TAG = "lt.preferences"; +static const char *const TAG = "preferences"; // Buffer size for converting uint32_t to string: max "4294967295" (10 chars) + null terminator + 1 padding static constexpr size_t KEY_BUFFER_SIZE = 12; diff --git a/esphome/components/rp2040/preferences.cpp b/esphome/components/rp2040/preferences.cpp index 0a91136a9f..cfc802b28f 100644 --- a/esphome/components/rp2040/preferences.cpp +++ b/esphome/components/rp2040/preferences.cpp @@ -14,7 +14,7 @@ namespace esphome::rp2040 { -static const char *const TAG = "rp2040.preferences"; +static const char *const TAG = "preferences"; static constexpr uint32_t RP2040_FLASH_STORAGE_SIZE = 512; diff --git a/esphome/components/zephyr/preferences.cpp b/esphome/components/zephyr/preferences.cpp index df69c0e652..c26a1d6d53 100644 --- a/esphome/components/zephyr/preferences.cpp +++ b/esphome/components/zephyr/preferences.cpp @@ -10,7 +10,7 @@ namespace esphome::zephyr { -static const char *const TAG = "zephyr.preferences"; +static const char *const TAG = "preferences"; bool ZephyrPreferenceBackend::save(const uint8_t *data, size_t len) { this->data.resize(len); From 2c872600464c2b0b168e09277bb5bbe16da4fac0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 20 Mar 2026 15:09:13 -1000 Subject: [PATCH 025/166] [core] Optimize Component::is_ready() with bitmask check (#15005) --- esphome/core/component.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index 00dda0cc26..89ac0c7a2a 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -378,9 +378,10 @@ void Component::set_retry(uint32_t initial_wait_time, uint8_t max_attempts, std: #pragma GCC diagnostic pop } bool Component::is_ready() const { - return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP || - (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP_DONE || - (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_SETUP; + // Bitmask check: valid states are SETUP(1), LOOP(2), LOOP_DONE(4) + // (1 << state) & 0b10110 checks membership in one instruction + return ((1u << (this->component_state_ & COMPONENT_STATE_MASK)) & + ((1u << COMPONENT_STATE_SETUP) | (1u << COMPONENT_STATE_LOOP) | (1u << COMPONENT_STATE_LOOP_DONE))) != 0; } bool Component::can_proceed() { return true; } bool Component::set_status_flag_(uint8_t flag) { From 32db055b98cbb964663396b6aa63239cc4f97591 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 20 Mar 2026 15:09:28 -1000 Subject: [PATCH 026/166] [number] Clean up NumberCall::perform() increment/decrement logic (#15000) --- esphome/components/number/number_call.cpp | 22 ++++++---------------- esphome/components/number/number_call.h | 3 +++ 2 files changed, 9 insertions(+), 16 deletions(-) diff --git a/esphome/components/number/number_call.cpp b/esphome/components/number/number_call.cpp index 27a857c112..aac9b2a23d 100644 --- a/esphome/components/number/number_call.cpp +++ b/esphome/components/number/number_call.cpp @@ -80,35 +80,25 @@ void NumberCall::perform() { target_value = max_value; } } else if (this->operation_ == NUMBER_OP_INCREMENT) { - ESP_LOGD(TAG, "'%s': Increment with%s cycling", name, this->cycle_ ? "" : "out"); + ESP_LOGD(TAG, "'%s': Increment with%s cycling", name, this->cycle_ ? LOG_STR_LITERAL("") : LOG_STR_LITERAL("out")); if (!parent->has_state()) { this->log_perform_warning_(LOG_STR("Can't increment, no state")); return; } auto step = traits.get_step(); target_value = parent->state + (std::isnan(step) ? 1 : step); - if (target_value > max_value) { - if (this->cycle_ && !std::isnan(min_value)) { - target_value = min_value; - } else { - target_value = max_value; - } - } + if (target_value > max_value) + target_value = this->cycle_or_clamp_(max_value, min_value); } else if (this->operation_ == NUMBER_OP_DECREMENT) { - ESP_LOGD(TAG, "'%s': Decrement with%s cycling", name, this->cycle_ ? "" : "out"); + ESP_LOGD(TAG, "'%s': Decrement with%s cycling", name, this->cycle_ ? LOG_STR_LITERAL("") : LOG_STR_LITERAL("out")); if (!parent->has_state()) { this->log_perform_warning_(LOG_STR("Can't decrement, no state")); return; } auto step = traits.get_step(); target_value = parent->state - (std::isnan(step) ? 1 : step); - if (target_value < min_value) { - if (this->cycle_ && !std::isnan(max_value)) { - target_value = max_value; - } else { - target_value = min_value; - } - } + if (target_value < min_value) + target_value = this->cycle_or_clamp_(min_value, max_value); } if (target_value < min_value) { diff --git a/esphome/components/number/number_call.h b/esphome/components/number/number_call.h index 584c13f413..29eaeb72d9 100644 --- a/esphome/components/number/number_call.h +++ b/esphome/components/number/number_call.h @@ -33,6 +33,9 @@ class NumberCall { NumberCall &with_cycle(bool cycle); protected: + float cycle_or_clamp_(float clamp, float opposite) const { + return (this->cycle_ && !std::isnan(opposite)) ? opposite : clamp; + } void log_perform_warning_(const LogString *message); void log_perform_warning_value_range_(const LogString *comparison, const LogString *limit_type, float val, float limit); From 21e384cafd89f1a441a7de2c67816338ca90c569 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 20 Mar 2026 15:10:18 -1000 Subject: [PATCH 027/166] [esp32] Disable PicolibC Newlib compatibility shim on IDF 6.0+ (#15008) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/esp32/__init__.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 64e5f44081..f85f13fe73 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -1920,6 +1920,18 @@ async def to_code(config): add_idf_sdkconfig_option("CONFIG_MBEDTLS_SHA384_C", False) add_idf_sdkconfig_option("CONFIG_MBEDTLS_SHA512_C", False) + # Disable PicolibC Newlib compatibility shim on IDF 6.0+ + # IDF 6.0 switched from Newlib to PicolibC. The shim provides thread-local + # stdin/stdout/stderr and getreent() for code compiled against Newlib. + # ESPHome doesn't link against Newlib-built libraries that use stdio. + # If a component needs it (e.g. precompiled Newlib binaries), re-enable via: + # esp32: + # framework: + # sdkconfig_options: + # CONFIG_LIBC_PICOLIBC_NEWLIB_COMPATIBILITY: "y" + if idf_version() >= cv.Version(6, 0, 0): + add_idf_sdkconfig_option("CONFIG_LIBC_PICOLIBC_NEWLIB_COMPATIBILITY", False) + # Disable regi2c control functions in IRAM # Only needed if using analog peripherals (ADC, DAC, etc.) from ISRs while cache is disabled if advanced[CONF_DISABLE_REGI2C_IN_IRAM]: From f3cddcee214fc8c510ecfc88dd3ef277c0d882df Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 20 Mar 2026 15:25:40 -1000 Subject: [PATCH 028/166] [core] Store parent pointers as members to enable inline Callback storage (#14923) --- esphome/components/cover/automation.h | 18 +++--- esphome/components/datetime/datetime_base.h | 7 ++- .../components/display_menu_base/automation.h | 35 ++++++++---- esphome/components/esp32_improv/automation.h | 55 ++++++++++++------- esphome/components/fan/automation.h | 49 ++++++++++------- .../graphical_display_menu.h | 7 ++- esphome/components/lock/automation.h | 9 ++- esphome/components/media_player/automation.h | 9 ++- esphome/components/mqtt/mqtt_fan.cpp | 3 +- esphome/components/valve/automation.h | 18 ++++-- 10 files changed, 134 insertions(+), 76 deletions(-) diff --git a/esphome/components/cover/automation.h b/esphome/components/cover/automation.h index 12ec46725d..f121e5c2d6 100644 --- a/esphome/components/cover/automation.h +++ b/esphome/components/cover/automation.h @@ -105,17 +105,18 @@ template using CoverIsClosedCondition = CoverPositionCondition class CoverPositionTrigger : public Trigger<> { public: - CoverPositionTrigger(Cover *a_cover) { - a_cover->add_on_state_callback([this, a_cover]() { - if (a_cover->position != this->last_position_) { - this->last_position_ = a_cover->position; - if (a_cover->position == (OPEN ? COVER_OPEN : COVER_CLOSED)) + CoverPositionTrigger(Cover *a_cover) : cover_(a_cover) { + a_cover->add_on_state_callback([this]() { + if (this->cover_->position != this->last_position_) { + this->last_position_ = this->cover_->position; + if (this->cover_->position == (OPEN ? COVER_OPEN : COVER_CLOSED)) this->trigger(); } }); } protected: + Cover *cover_; float last_position_{NAN}; }; @@ -124,9 +125,9 @@ using CoverClosedTrigger = CoverPositionTrigger; template class CoverTrigger : public Trigger<> { public: - CoverTrigger(Cover *a_cover) { - a_cover->add_on_state_callback([this, a_cover]() { - auto current_op = a_cover->current_operation; + CoverTrigger(Cover *a_cover) : cover_(a_cover) { + a_cover->add_on_state_callback([this]() { + auto current_op = this->cover_->current_operation; if (current_op == OP) { if (!this->last_operation_.has_value() || this->last_operation_.value() != OP) { this->trigger(); @@ -137,6 +138,7 @@ template class CoverTrigger : public Trigger<> { } protected: + Cover *cover_; optional last_operation_{}; }; } // namespace esphome::cover diff --git a/esphome/components/datetime/datetime_base.h b/esphome/components/datetime/datetime_base.h index 98f23aa713..6c0a33c842 100644 --- a/esphome/components/datetime/datetime_base.h +++ b/esphome/components/datetime/datetime_base.h @@ -33,9 +33,12 @@ class DateTimeBase : public EntityBase { class DateTimeStateTrigger : public Trigger { public: - explicit DateTimeStateTrigger(DateTimeBase *parent) { - parent->add_on_state_callback([this, parent]() { this->trigger(parent->state_as_esptime()); }); + explicit DateTimeStateTrigger(DateTimeBase *parent) : parent_(parent) { + parent->add_on_state_callback([this]() { this->trigger(this->parent_->state_as_esptime()); }); } + + protected: + DateTimeBase *parent_; }; } // namespace esphome::datetime diff --git a/esphome/components/display_menu_base/automation.h b/esphome/components/display_menu_base/automation.h index 9c64794cef..50c26c344c 100644 --- a/esphome/components/display_menu_base/automation.h +++ b/esphome/components/display_menu_base/automation.h @@ -96,37 +96,52 @@ template class IsActiveCondition : public Condition { class DisplayMenuOnEnterTrigger : public Trigger { public: - explicit DisplayMenuOnEnterTrigger(MenuItem *parent) { - parent->add_on_enter_callback([this, parent]() { this->trigger(parent); }); + explicit DisplayMenuOnEnterTrigger(MenuItem *parent) : parent_(parent) { + parent->add_on_enter_callback([this]() { this->trigger(this->parent_); }); } + + protected: + MenuItem *parent_; }; class DisplayMenuOnLeaveTrigger : public Trigger { public: - explicit DisplayMenuOnLeaveTrigger(MenuItem *parent) { - parent->add_on_leave_callback([this, parent]() { this->trigger(parent); }); + explicit DisplayMenuOnLeaveTrigger(MenuItem *parent) : parent_(parent) { + parent->add_on_leave_callback([this]() { this->trigger(this->parent_); }); } + + protected: + MenuItem *parent_; }; class DisplayMenuOnValueTrigger : public Trigger { public: - explicit DisplayMenuOnValueTrigger(MenuItem *parent) { - parent->add_on_value_callback([this, parent]() { this->trigger(parent); }); + explicit DisplayMenuOnValueTrigger(MenuItem *parent) : parent_(parent) { + parent->add_on_value_callback([this]() { this->trigger(this->parent_); }); } + + protected: + MenuItem *parent_; }; class DisplayMenuOnNextTrigger : public Trigger { public: - explicit DisplayMenuOnNextTrigger(MenuItemCustom *parent) { - parent->add_on_next_callback([this, parent]() { this->trigger(parent); }); + explicit DisplayMenuOnNextTrigger(MenuItemCustom *parent) : parent_(parent) { + parent->add_on_next_callback([this]() { this->trigger(this->parent_); }); } + + protected: + MenuItemCustom *parent_; }; class DisplayMenuOnPrevTrigger : public Trigger { public: - explicit DisplayMenuOnPrevTrigger(MenuItemCustom *parent) { - parent->add_on_prev_callback([this, parent]() { this->trigger(parent); }); + explicit DisplayMenuOnPrevTrigger(MenuItemCustom *parent) : parent_(parent) { + parent->add_on_prev_callback([this]() { this->trigger(this->parent_); }); } + + protected: + MenuItemCustom *parent_; }; } // namespace display_menu_base diff --git a/esphome/components/esp32_improv/automation.h b/esphome/components/esp32_improv/automation.h index 52c5da125b..cd2bd84c30 100644 --- a/esphome/components/esp32_improv/automation.h +++ b/esphome/components/esp32_improv/automation.h @@ -12,58 +12,73 @@ namespace esp32_improv { class ESP32ImprovProvisionedTrigger : public Trigger<> { public: - explicit ESP32ImprovProvisionedTrigger(ESP32ImprovComponent *parent) { - parent->add_on_state_callback([this, parent](improv::State state, improv::Error error) { - if (state == improv::STATE_PROVISIONED && !parent->is_failed()) { - trigger(); + explicit ESP32ImprovProvisionedTrigger(ESP32ImprovComponent *parent) : parent_(parent) { + parent->add_on_state_callback([this](improv::State state, improv::Error error) { + if (state == improv::STATE_PROVISIONED && !this->parent_->is_failed()) { + this->trigger(); } }); } + + protected: + ESP32ImprovComponent *parent_; }; class ESP32ImprovProvisioningTrigger : public Trigger<> { public: - explicit ESP32ImprovProvisioningTrigger(ESP32ImprovComponent *parent) { - parent->add_on_state_callback([this, parent](improv::State state, improv::Error error) { - if (state == improv::STATE_PROVISIONING && !parent->is_failed()) { - trigger(); + explicit ESP32ImprovProvisioningTrigger(ESP32ImprovComponent *parent) : parent_(parent) { + parent->add_on_state_callback([this](improv::State state, improv::Error error) { + if (state == improv::STATE_PROVISIONING && !this->parent_->is_failed()) { + this->trigger(); } }); } + + protected: + ESP32ImprovComponent *parent_; }; class ESP32ImprovStartTrigger : public Trigger<> { public: - explicit ESP32ImprovStartTrigger(ESP32ImprovComponent *parent) { - parent->add_on_state_callback([this, parent](improv::State state, improv::Error error) { + explicit ESP32ImprovStartTrigger(ESP32ImprovComponent *parent) : parent_(parent) { + parent->add_on_state_callback([this](improv::State state, improv::Error error) { if ((state == improv::STATE_AUTHORIZED || state == improv::STATE_AWAITING_AUTHORIZATION) && - !parent->is_failed()) { - trigger(); + !this->parent_->is_failed()) { + this->trigger(); } }); } + + protected: + ESP32ImprovComponent *parent_; }; class ESP32ImprovStateTrigger : public Trigger { public: - explicit ESP32ImprovStateTrigger(ESP32ImprovComponent *parent) { - parent->add_on_state_callback([this, parent](improv::State state, improv::Error error) { - if (!parent->is_failed()) { - trigger(state, error); + explicit ESP32ImprovStateTrigger(ESP32ImprovComponent *parent) : parent_(parent) { + parent->add_on_state_callback([this](improv::State state, improv::Error error) { + if (!this->parent_->is_failed()) { + this->trigger(state, error); } }); } + + protected: + ESP32ImprovComponent *parent_; }; class ESP32ImprovStoppedTrigger : public Trigger<> { public: - explicit ESP32ImprovStoppedTrigger(ESP32ImprovComponent *parent) { - parent->add_on_state_callback([this, parent](improv::State state, improv::Error error) { - if (state == improv::STATE_STOPPED && !parent->is_failed()) { - trigger(); + explicit ESP32ImprovStoppedTrigger(ESP32ImprovComponent *parent) : parent_(parent) { + parent->add_on_state_callback([this](improv::State state, improv::Error error) { + if (state == improv::STATE_STOPPED && !this->parent_->is_failed()) { + this->trigger(); } }); } + + protected: + ESP32ImprovComponent *parent_; }; } // namespace esp32_improv diff --git a/esphome/components/fan/automation.h b/esphome/components/fan/automation.h index 3c3b0ce519..3ee6f89e55 100644 --- a/esphome/components/fan/automation.h +++ b/esphome/components/fan/automation.h @@ -113,16 +113,19 @@ template class FanIsOffCondition : public Condition { class FanStateTrigger : public Trigger { public: - FanStateTrigger(Fan *state) { - state->add_on_state_callback([this, state]() { this->trigger(state); }); + FanStateTrigger(Fan *state) : fan_(state) { + state->add_on_state_callback([this]() { this->trigger(this->fan_); }); } + + protected: + Fan *fan_; }; class FanTurnOnTrigger : public Trigger<> { public: - FanTurnOnTrigger(Fan *state) { - state->add_on_state_callback([this, state]() { - auto is_on = state->state; + FanTurnOnTrigger(Fan *state) : fan_(state) { + state->add_on_state_callback([this]() { + auto is_on = this->fan_->state; auto should_trigger = is_on && !this->last_on_; this->last_on_ = is_on; if (should_trigger) { @@ -133,14 +136,15 @@ class FanTurnOnTrigger : public Trigger<> { } protected: + Fan *fan_; bool last_on_; }; class FanTurnOffTrigger : public Trigger<> { public: - FanTurnOffTrigger(Fan *state) { - state->add_on_state_callback([this, state]() { - auto is_on = state->state; + FanTurnOffTrigger(Fan *state) : fan_(state) { + state->add_on_state_callback([this]() { + auto is_on = this->fan_->state; auto should_trigger = !is_on && this->last_on_; this->last_on_ = is_on; if (should_trigger) { @@ -151,14 +155,15 @@ class FanTurnOffTrigger : public Trigger<> { } protected: + Fan *fan_; bool last_on_; }; class FanDirectionSetTrigger : public Trigger { public: - FanDirectionSetTrigger(Fan *state) { - state->add_on_state_callback([this, state]() { - auto direction = state->direction; + FanDirectionSetTrigger(Fan *state) : fan_(state) { + state->add_on_state_callback([this]() { + auto direction = this->fan_->direction; auto should_trigger = direction != this->last_direction_; this->last_direction_ = direction; if (should_trigger) { @@ -169,14 +174,15 @@ class FanDirectionSetTrigger : public Trigger { } protected: + Fan *fan_; FanDirection last_direction_; }; class FanOscillatingSetTrigger : public Trigger { public: - FanOscillatingSetTrigger(Fan *state) { - state->add_on_state_callback([this, state]() { - auto oscillating = state->oscillating; + FanOscillatingSetTrigger(Fan *state) : fan_(state) { + state->add_on_state_callback([this]() { + auto oscillating = this->fan_->oscillating; auto should_trigger = oscillating != this->last_oscillating_; this->last_oscillating_ = oscillating; if (should_trigger) { @@ -187,14 +193,15 @@ class FanOscillatingSetTrigger : public Trigger { } protected: + Fan *fan_; bool last_oscillating_; }; class FanSpeedSetTrigger : public Trigger { public: - FanSpeedSetTrigger(Fan *state) { - state->add_on_state_callback([this, state]() { - auto speed = state->speed; + FanSpeedSetTrigger(Fan *state) : fan_(state) { + state->add_on_state_callback([this]() { + auto speed = this->fan_->speed; auto should_trigger = speed != this->last_speed_; this->last_speed_ = speed; if (should_trigger) { @@ -205,14 +212,15 @@ class FanSpeedSetTrigger : public Trigger { } protected: + Fan *fan_; int last_speed_; }; class FanPresetSetTrigger : public Trigger { public: - FanPresetSetTrigger(Fan *state) { - state->add_on_state_callback([this, state]() { - auto preset_mode = state->get_preset_mode(); + FanPresetSetTrigger(Fan *state) : fan_(state) { + state->add_on_state_callback([this]() { + auto preset_mode = this->fan_->get_preset_mode(); auto should_trigger = preset_mode != this->last_preset_mode_; this->last_preset_mode_ = preset_mode; if (should_trigger) { @@ -223,6 +231,7 @@ class FanPresetSetTrigger : public Trigger { } protected: + Fan *fan_; StringRef last_preset_mode_{}; }; diff --git a/esphome/components/graphical_display_menu/graphical_display_menu.h b/esphome/components/graphical_display_menu/graphical_display_menu.h index 007889557d..ce1db18525 100644 --- a/esphome/components/graphical_display_menu/graphical_display_menu.h +++ b/esphome/components/graphical_display_menu/graphical_display_menu.h @@ -75,9 +75,12 @@ class GraphicalDisplayMenu : public display_menu_base::DisplayMenuComponent { class GraphicalDisplayMenuOnRedrawTrigger : public Trigger { public: - explicit GraphicalDisplayMenuOnRedrawTrigger(GraphicalDisplayMenu *parent) { - parent->add_on_redraw_callback([this, parent]() { this->trigger(parent); }); + explicit GraphicalDisplayMenuOnRedrawTrigger(GraphicalDisplayMenu *parent) : parent_(parent) { + parent->add_on_redraw_callback([this]() { this->trigger(this->parent_); }); } + + protected: + GraphicalDisplayMenu *parent_; }; } // namespace graphical_display_menu diff --git a/esphome/components/lock/automation.h b/esphome/components/lock/automation.h index 011c6cc6af..6f3c422693 100644 --- a/esphome/components/lock/automation.h +++ b/esphome/components/lock/automation.h @@ -51,13 +51,16 @@ template class LockCondition : public Condition { template class LockStateTrigger : public Trigger<> { public: - explicit LockStateTrigger(Lock *a_lock) { - a_lock->add_on_state_callback([this, a_lock]() { - if (a_lock->state == State) { + explicit LockStateTrigger(Lock *a_lock) : lock_(a_lock) { + a_lock->add_on_state_callback([this]() { + if (this->lock_->state == State) { this->trigger(); } }); } + + protected: + Lock *lock_; }; using LockLockTrigger = LockStateTrigger; diff --git a/esphome/components/media_player/automation.h b/esphome/components/media_player/automation.h index 90e7bf75b5..031f6657f4 100644 --- a/esphome/components/media_player/automation.h +++ b/esphome/components/media_player/automation.h @@ -80,12 +80,15 @@ class StateTrigger : public Trigger<> { template class MediaPlayerStateTrigger : public Trigger<> { public: - explicit MediaPlayerStateTrigger(MediaPlayer *player) { - player->add_on_state_callback([this, player]() { - if (player->state == State) + explicit MediaPlayerStateTrigger(MediaPlayer *player) : player_(player) { + player->add_on_state_callback([this]() { + if (this->player_->state == State) this->trigger(); }); } + + protected: + MediaPlayer *player_; }; using IdleTrigger = MediaPlayerStateTrigger; diff --git a/esphome/components/mqtt/mqtt_fan.cpp b/esphome/components/mqtt/mqtt_fan.cpp index ae2b8c4600..3a8658a55f 100644 --- a/esphome/components/mqtt/mqtt_fan.cpp +++ b/esphome/components/mqtt/mqtt_fan.cpp @@ -121,8 +121,7 @@ void MQTTFanComponent::setup() { }); } - auto f = std::bind(&MQTTFanComponent::publish_state, this); - this->state_->add_on_state_callback([this, f]() { this->defer("send", f); }); + this->state_->add_on_state_callback([this]() { this->defer("send", [this]() { this->publish_state(); }); }); } void MQTTFanComponent::dump_config() { diff --git a/esphome/components/valve/automation.h b/esphome/components/valve/automation.h index 87e9cde088..a064f375f7 100644 --- a/esphome/components/valve/automation.h +++ b/esphome/components/valve/automation.h @@ -87,24 +87,30 @@ template class ValveIsClosedCondition : public Condition class ValveOpenTrigger : public Trigger<> { public: - ValveOpenTrigger(Valve *a_valve) { - a_valve->add_on_state_callback([this, a_valve]() { - if (a_valve->is_fully_open()) { + ValveOpenTrigger(Valve *a_valve) : valve_(a_valve) { + a_valve->add_on_state_callback([this]() { + if (this->valve_->is_fully_open()) { this->trigger(); } }); } + + protected: + Valve *valve_; }; class ValveClosedTrigger : public Trigger<> { public: - ValveClosedTrigger(Valve *a_valve) { - a_valve->add_on_state_callback([this, a_valve]() { - if (a_valve->is_fully_closed()) { + ValveClosedTrigger(Valve *a_valve) : valve_(a_valve) { + a_valve->add_on_state_callback([this]() { + if (this->valve_->is_fully_closed()) { this->trigger(); } }); } + + protected: + Valve *valve_; }; } // namespace valve From 95dea59382d422f8d1a44020a64d79935bd6ac19 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 20 Mar 2026 15:25:54 -1000 Subject: [PATCH 029/166] [core] Use SplitMix32 PRNG for random_uint32() (#14984) --- esphome/components/esp32/helpers.cpp | 1 - esphome/components/host/helpers.cpp | 9 -------- esphome/components/libretiny/helpers.cpp | 2 -- esphome/components/rp2040/helpers.cpp | 9 -------- esphome/components/zephyr/__init__.py | 2 ++ esphome/components/zephyr/core.cpp | 1 - esphome/core/helpers.cpp | 26 ++++++++++++++++++++++++ esphome/core/helpers.h | 7 ++++++- 8 files changed, 34 insertions(+), 23 deletions(-) diff --git a/esphome/components/esp32/helpers.cpp b/esphome/components/esp32/helpers.cpp index 76f1c59c73..654a35b473 100644 --- a/esphome/components/esp32/helpers.cpp +++ b/esphome/components/esp32/helpers.cpp @@ -14,7 +14,6 @@ namespace esphome { -uint32_t random_uint32() { return esp_random(); } bool random_bytes(uint8_t *data, size_t len) { esp_fill_random(data, len); return true; diff --git a/esphome/components/host/helpers.cpp b/esphome/components/host/helpers.cpp index fdad4f5cb6..7e8849b3e1 100644 --- a/esphome/components/host/helpers.cpp +++ b/esphome/components/host/helpers.cpp @@ -8,8 +8,6 @@ #include #endif #include -#include -#include #include "esphome/core/defines.h" #include "esphome/core/log.h" @@ -18,13 +16,6 @@ namespace esphome { static const char *const TAG = "helpers.host"; -uint32_t random_uint32() { - std::random_device dev; - std::mt19937 rng(dev()); - std::uniform_int_distribution dist(0, std::numeric_limits::max()); - return dist(rng); -} - bool random_bytes(uint8_t *data, size_t len) { FILE *fp = fopen("/dev/urandom", "r"); if (fp == nullptr) { diff --git a/esphome/components/libretiny/helpers.cpp b/esphome/components/libretiny/helpers.cpp index ffbd181c54..52332ef53d 100644 --- a/esphome/components/libretiny/helpers.cpp +++ b/esphome/components/libretiny/helpers.cpp @@ -8,8 +8,6 @@ namespace esphome { -uint32_t random_uint32() { return rand(); } - bool random_bytes(uint8_t *data, size_t len) { lt_rand_bytes(data, len); return true; diff --git a/esphome/components/rp2040/helpers.cpp b/esphome/components/rp2040/helpers.cpp index e360b45ebb..8cb5f7c18d 100644 --- a/esphome/components/rp2040/helpers.cpp +++ b/esphome/components/rp2040/helpers.cpp @@ -17,15 +17,6 @@ namespace esphome { -uint32_t random_uint32() { - uint32_t result = 0; - for (uint8_t i = 0; i < 32; i++) { - result <<= 1; - result |= rosc_hw->randombit; - } - return result; -} - bool random_bytes(uint8_t *data, size_t len) { while (len-- != 0) { uint8_t result = 0; diff --git a/esphome/components/zephyr/__init__.py b/esphome/components/zephyr/__init__.py index b8a091feb9..348e7a3cf2 100644 --- a/esphome/components/zephyr/__init__.py +++ b/esphome/components/zephyr/__init__.py @@ -122,6 +122,8 @@ def zephyr_to_code(config: ConfigType) -> None: zephyr_add_prj_conf("FPU", True) zephyr_add_prj_conf("NEWLIB_LIBC_FLOAT_PRINTF", True) zephyr_add_prj_conf("STD_CPP20", True) + # random_bytes() uses sys_rand_get() which requires the entropy subsystem + zephyr_add_prj_conf("ENTROPY_GENERATOR", True) # os: ***** USAGE FAULT ***** # os: Illegal load of EXC_RETURN into PC diff --git a/esphome/components/zephyr/core.cpp b/esphome/components/zephyr/core.cpp index d7c77fdd2c..a3b0471ebc 100644 --- a/esphome/components/zephyr/core.cpp +++ b/esphome/components/zephyr/core.cpp @@ -75,7 +75,6 @@ IRAM_ATTR InterruptLock::~InterruptLock() { irq_unlock(state_); } // Zephyr LwIPLock is defined inline as a no-op in helpers.h -uint32_t random_uint32() { return rand(); } // NOLINT(cert-msc30-c, cert-msc50-cpp) bool random_bytes(uint8_t *data, size_t len) { sys_rand_get(data, len); return true; diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index 00b447ebf2..ee99c54196 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -156,6 +156,32 @@ uint32_t fnv1_hash(const char *str) { return hash; } +// SplitMix32 — a fast, non-cryptographic PRNG from the SplitMix family +// (Steele et al., 2014). Uses a Weyl sequence with golden-ratio increment +// and the MurmurHash3 32-bit finalizer as output mixing function. +// Reference: https://doi.org/10.1145/2714064.2660195 +// Test results: https://lemire.me/blog/2017/08/22/testing-non-cryptographic-random-number-generators-my-results/ +// Seeded lazily from the platform's secure RNG via random_bytes(). +// ESP8266 uses os_random() instead (defined in esp8266/helpers.cpp). +#ifndef USE_ESP8266 +static uint32_t splitmix32_state; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +uint32_t random_uint32() { + // State of 0 means unseeded. The state will wrap back to 0 after 2^32 calls, + // triggering one extra random_bytes() call — an acceptable trade-off vs. adding + // a separate bool flag (4 bytes BSS + branch on every call). + if (splitmix32_state == 0) { + random_bytes(reinterpret_cast(&splitmix32_state), sizeof(splitmix32_state)); + splitmix32_state |= 1; // ensure non-zero seed + } + splitmix32_state += 0x9e3779b9u; + uint32_t z = splitmix32_state; + z = (z ^ (z >> 16)) * 0x85ebca6bu; + z = (z ^ (z >> 13)) * 0xc2b2ae35u; + return z ^ (z >> 16); +} +#endif + float random_float() { return static_cast(random_uint32()) / static_cast(UINT32_MAX); } // Strings diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index a703b5a5f3..43431299de 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -842,10 +842,15 @@ template inline constexpr ESPHOME_ALWAYS_INLINE Ret } /// Return a random 32-bit unsigned integer. +/// Not thread-safe. Must only be called from the main loop. +/// Not suitable for cryptographic use; use random_bytes() instead. uint32_t random_uint32(); /// Return a random float between 0 and 1. +/// Not thread-safe. Must only be called from the main loop. +/// Not suitable for cryptographic use; use random_bytes() instead. float random_float(); -/// Generate \p len number of random bytes. +/// Generate \p len random bytes using the platform's secure RNG (hardware RNG or OS CSPRNG). +/// Thread-safe. Suitable for cryptographic use. bool random_bytes(uint8_t *data, size_t len); ///@} From 1920d8a887407a9d9f2f23f8c1d9f83480a497a5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 20 Mar 2026 17:35:17 -1000 Subject: [PATCH 030/166] [benchmark] Add noise encryption benchmarks (#15037) --- script/setup_codspeed_lib.py | 8 +- tests/benchmarks/components/api/__init__.py | 7 + .../components/api/bench_noise_encrypt.cpp | 177 ++++++++++++++++++ .../benchmarks/components/api/benchmark.yaml | 1 + tests/benchmarks/components/mdns/__init__.py | 5 + .../benchmarks/components/network/__init__.py | 5 + .../benchmarks/components/socket/__init__.py | 7 + 7 files changed, 209 insertions(+), 1 deletion(-) create mode 100644 tests/benchmarks/components/api/__init__.py create mode 100644 tests/benchmarks/components/api/bench_noise_encrypt.cpp create mode 100644 tests/benchmarks/components/mdns/__init__.py create mode 100644 tests/benchmarks/components/network/__init__.py create mode 100644 tests/benchmarks/components/socket/__init__.py diff --git a/script/setup_codspeed_lib.py b/script/setup_codspeed_lib.py index 959c89d05b..4f5d1bff24 100755 --- a/script/setup_codspeed_lib.py +++ b/script/setup_codspeed_lib.py @@ -205,7 +205,13 @@ def setup_codspeed_lib(output_dir: Path) -> None: if hooks_dist_c.exists(): _copy_if_missing(hooks_dist_c, lib_src / "instrument_hooks.c") - # 4. Write library.json + # 4. Copy instrument-hooks headers (core.h, callgrind.h, valgrind.h) next to + # measurement.hpp so they are found before any same-named headers from + # other libraries (e.g. libsodium's core.h). + for header in hooks_include.glob("*.h"): + _copy_if_missing(header, core_include / header.name) + + # 5. Write library.json version = _read_codspeed_version(output_dir / CORE_CMAKE) _write_library_json( benchmark_dir, core_include, hooks_include, version, project_root diff --git a/tests/benchmarks/components/api/__init__.py b/tests/benchmarks/components/api/__init__.py new file mode 100644 index 0000000000..0687c3f87f --- /dev/null +++ b/tests/benchmarks/components/api/__init__.py @@ -0,0 +1,7 @@ +from tests.testing_helpers import ComponentManifestOverride + + +def override_manifest(manifest: ComponentManifestOverride) -> None: + # api must run its to_code to define USE_API, USE_API_PLAINTEXT, + # and add the noise-c library dependency. + manifest.enable_codegen() diff --git a/tests/benchmarks/components/api/bench_noise_encrypt.cpp b/tests/benchmarks/components/api/bench_noise_encrypt.cpp new file mode 100644 index 0000000000..223e6ada0d --- /dev/null +++ b/tests/benchmarks/components/api/bench_noise_encrypt.cpp @@ -0,0 +1,177 @@ +#include "esphome/core/defines.h" +#ifdef USE_API_NOISE + +#include +#include +#include + +#include "noise/protocol.h" + +namespace esphome::api::benchmarks { + +static constexpr int kInnerIterations = 2000; + +// Helper to create and initialize a NoiseCipherState with ChaChaPoly. +// Returns nullptr on failure. +static NoiseCipherState *create_cipher() { + NoiseCipherState *cipher = nullptr; + int err = noise_cipherstate_new_by_id(&cipher, NOISE_CIPHER_CHACHAPOLY); + if (err != NOISE_ERROR_NONE || cipher == nullptr) + return nullptr; + + // Initialize with a dummy 32-byte key (same pattern as handshake split produces) + uint8_t key[32]; + memset(key, 0xAB, sizeof(key)); + err = noise_cipherstate_init_key(cipher, key, sizeof(key)); + if (err != NOISE_ERROR_NONE) { + noise_cipherstate_free(cipher); + return nullptr; + } + return cipher; +} + +// Benchmark helper matching the exact pattern from +// APINoiseFrameHelper::write_protobuf_messages: +// - noise_buffer_init + noise_buffer_set_inout (same as production) +// - No explicit set_nonce (production relies on internal nonce increment) +// - Error checking on encrypt return +static void noise_encrypt_bench(benchmark::State &state, size_t plaintext_size) { + NoiseCipherState *cipher = create_cipher(); + if (cipher == nullptr) { + state.SkipWithError("Failed to create cipher state"); + return; + } + + size_t mac_len = noise_cipherstate_get_mac_length(cipher); + size_t buf_capacity = plaintext_size + mac_len; + auto buffer = std::make_unique(buf_capacity); + memset(buffer.get(), 0x42, plaintext_size); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + // Match production: init buffer, set inout, encrypt + NoiseBuffer mbuf; + noise_buffer_init(mbuf); + noise_buffer_set_inout(mbuf, buffer.get(), plaintext_size, buf_capacity); + + int err = noise_cipherstate_encrypt(cipher, &mbuf); + if (err != NOISE_ERROR_NONE) { + state.SkipWithError("noise_cipherstate_encrypt failed"); + noise_cipherstate_free(cipher); + return; + } + } + benchmark::DoNotOptimize(buffer[0]); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); + + noise_cipherstate_free(cipher); +} + +// --- Encrypt a typical sensor state message (small payload ~14 bytes) --- +// This is the most common message encrypted on every sensor update. +// 4 bytes type+len header + ~10 bytes payload. + +static void NoiseEncrypt_SmallMessage(benchmark::State &state) { noise_encrypt_bench(state, 14); } +BENCHMARK(NoiseEncrypt_SmallMessage); + +// --- Encrypt a medium message (~128 bytes, typical for LightStateResponse) --- + +static void NoiseEncrypt_MediumMessage(benchmark::State &state) { noise_encrypt_bench(state, 128); } +BENCHMARK(NoiseEncrypt_MediumMessage); + +// --- Encrypt a large message (~1024 bytes, typical for DeviceInfoResponse) --- + +static void NoiseEncrypt_LargeMessage(benchmark::State &state) { noise_encrypt_bench(state, 1024); } +BENCHMARK(NoiseEncrypt_LargeMessage); + +// Benchmark helper matching the exact pattern from +// APINoiseFrameHelper::read_packet: +// - noise_buffer_init + noise_buffer_set_inout with capacity == size (decrypt shrinks) +// - Error checking on decrypt return +// +// Pre-encrypts kInnerIterations messages with sequential nonces before the +// timed loop. Each outer iteration re-inits the decrypt key to reset the +// nonce back to 0, then decrypts all pre-encrypted messages in sequence. +// The init_key cost is amortized over kInnerIterations decrypts. +static void noise_decrypt_bench(benchmark::State &state, size_t plaintext_size) { + NoiseCipherState *encrypt_cipher = create_cipher(); + NoiseCipherState *decrypt_cipher = create_cipher(); + if (encrypt_cipher == nullptr || decrypt_cipher == nullptr) { + state.SkipWithError("Failed to create cipher state"); + if (encrypt_cipher) + noise_cipherstate_free(encrypt_cipher); + if (decrypt_cipher) + noise_cipherstate_free(decrypt_cipher); + return; + } + + size_t mac_len = noise_cipherstate_get_mac_length(encrypt_cipher); + size_t encrypted_size = plaintext_size + mac_len; + + // Pre-encrypt kInnerIterations messages with sequential nonces (0..N-1). + auto ciphertexts = std::make_unique(encrypted_size * kInnerIterations); + for (int i = 0; i < kInnerIterations; i++) { + uint8_t *ct = ciphertexts.get() + i * encrypted_size; + memset(ct, 0x42, plaintext_size); + NoiseBuffer enc_buf; + noise_buffer_init(enc_buf); + noise_buffer_set_inout(enc_buf, ct, plaintext_size, encrypted_size); + int err = noise_cipherstate_encrypt(encrypt_cipher, &enc_buf); + if (err != NOISE_ERROR_NONE) { + state.SkipWithError("Pre-encrypt failed"); + noise_cipherstate_free(encrypt_cipher); + noise_cipherstate_free(decrypt_cipher); + return; + } + } + + // Working buffer — decrypt modifies in place + auto buffer = std::make_unique(encrypted_size); + static constexpr uint8_t KEY[32] = {0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, + 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, + 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB}; + + for (auto _ : state) { + // Reset nonce to 0 by re-initing the key (amortized over kInnerIterations) + noise_cipherstate_init_key(decrypt_cipher, KEY, sizeof(KEY)); + + for (int i = 0; i < kInnerIterations; i++) { + // Copy ciphertext into working buffer (decrypt modifies in place) + memcpy(buffer.get(), ciphertexts.get() + i * encrypted_size, encrypted_size); + + // Decrypt matching production pattern + NoiseBuffer mbuf; + noise_buffer_init(mbuf); + noise_buffer_set_inout(mbuf, buffer.get(), encrypted_size, encrypted_size); + + int err = noise_cipherstate_decrypt(decrypt_cipher, &mbuf); + if (err != NOISE_ERROR_NONE) { + state.SkipWithError("noise_cipherstate_decrypt failed"); + noise_cipherstate_free(encrypt_cipher); + noise_cipherstate_free(decrypt_cipher); + return; + } + } + benchmark::DoNotOptimize(buffer[0]); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); + + noise_cipherstate_free(encrypt_cipher); + noise_cipherstate_free(decrypt_cipher); +} + +// --- Decrypt benchmarks (matching read_packet path) --- + +static void NoiseDecrypt_SmallMessage(benchmark::State &state) { noise_decrypt_bench(state, 14); } +BENCHMARK(NoiseDecrypt_SmallMessage); + +static void NoiseDecrypt_MediumMessage(benchmark::State &state) { noise_decrypt_bench(state, 128); } +BENCHMARK(NoiseDecrypt_MediumMessage); + +static void NoiseDecrypt_LargeMessage(benchmark::State &state) { noise_decrypt_bench(state, 1024); } +BENCHMARK(NoiseDecrypt_LargeMessage); + +} // namespace esphome::api::benchmarks + +#endif // USE_API_NOISE diff --git a/tests/benchmarks/components/api/benchmark.yaml b/tests/benchmarks/components/api/benchmark.yaml index bfc24d7440..e57276ea66 100644 --- a/tests/benchmarks/components/api/benchmark.yaml +++ b/tests/benchmarks/components/api/benchmark.yaml @@ -108,6 +108,7 @@ esphome: area_id: area_20 api: + encryption: sensor: binary_sensor: light: diff --git a/tests/benchmarks/components/mdns/__init__.py b/tests/benchmarks/components/mdns/__init__.py new file mode 100644 index 0000000000..b08f67a095 --- /dev/null +++ b/tests/benchmarks/components/mdns/__init__.py @@ -0,0 +1,5 @@ +from tests.testing_helpers import ComponentManifestOverride + + +def override_manifest(manifest: ComponentManifestOverride) -> None: + manifest.enable_codegen() diff --git a/tests/benchmarks/components/network/__init__.py b/tests/benchmarks/components/network/__init__.py new file mode 100644 index 0000000000..b08f67a095 --- /dev/null +++ b/tests/benchmarks/components/network/__init__.py @@ -0,0 +1,5 @@ +from tests.testing_helpers import ComponentManifestOverride + + +def override_manifest(manifest: ComponentManifestOverride) -> None: + manifest.enable_codegen() diff --git a/tests/benchmarks/components/socket/__init__.py b/tests/benchmarks/components/socket/__init__.py new file mode 100644 index 0000000000..7a20f9f230 --- /dev/null +++ b/tests/benchmarks/components/socket/__init__.py @@ -0,0 +1,7 @@ +from tests.testing_helpers import ComponentManifestOverride + + +def override_manifest(manifest: ComponentManifestOverride) -> None: + # socket must run its to_code to define USE_SOCKET_IMPL_BSD_SOCKETS + # which is needed by the api frame helper benchmarks. + manifest.enable_codegen() From d203a46ef808f76c22018eec570bab9f29b999a8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 20 Mar 2026 18:17:37 -1000 Subject: [PATCH 031/166] [api] Enable HAVE_WEAK_SYMBOLS and HAVE_INLINE_ASM for libsodium (#15038) --- esphome/components/api/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index 4c3cf81927..84589d540d 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -455,6 +455,9 @@ async def to_code(config: ConfigType) -> None: cg.add_define("USE_API_PLAINTEXT") cg.add_define("USE_API_NOISE") cg.add_library("esphome/noise-c", "0.1.11") + # Enable optimized memzero/memcmp in libsodium instead of volatile byte loops + cg.add_build_flag("-DHAVE_WEAK_SYMBOLS=1") + cg.add_build_flag("-DHAVE_INLINE_ASM=1") else: cg.add_define("USE_API_PLAINTEXT") From 8dd69207ea09f8ca8c2d90d9437c7396ae0dc2aa Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 21 Mar 2026 10:24:56 +0000 Subject: [PATCH 032/166] Bump aioesphomeapi from 44.6.2 to 44.7.0 (#15052) 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 10e56c3b49..2e09e2ed99 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.19 esptool==5.2.0 click==8.3.1 esphome-dashboard==20260210.0 -aioesphomeapi==44.6.2 +aioesphomeapi==44.7.0 zeroconf==0.148.0 puremagic==1.30 ruamel.yaml==0.19.1 # dashboard_import From 2a6ec597b4ca81bd63f062307799c97c024f2726 Mon Sep 17 00:00:00 2001 From: Samuel Sieb Date: Sat, 21 Mar 2026 11:13:08 -0700 Subject: [PATCH 033/166] [analog_threshhold] add missing header (#15058) --- .../components/analog_threshold/analog_threshold_binary_sensor.h | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/components/analog_threshold/analog_threshold_binary_sensor.h b/esphome/components/analog_threshold/analog_threshold_binary_sensor.h index 9ea95d8570..dd70768105 100644 --- a/esphome/components/analog_threshold/analog_threshold_binary_sensor.h +++ b/esphome/components/analog_threshold/analog_threshold_binary_sensor.h @@ -1,5 +1,6 @@ #pragma once +#include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/components/binary_sensor/binary_sensor.h" #include "esphome/components/sensor/sensor.h" From 86ec218f75685454a3cc012cd632c350ba60ad5c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 21 Mar 2026 13:15:35 -1000 Subject: [PATCH 034/166] [benchmark] Add plaintext API frame write benchmarks (#15036) --- .../components/api/bench_plaintext_frame.cpp | 162 ++++++++++++++++++ 1 file changed, 162 insertions(+) create mode 100644 tests/benchmarks/components/api/bench_plaintext_frame.cpp diff --git a/tests/benchmarks/components/api/bench_plaintext_frame.cpp b/tests/benchmarks/components/api/bench_plaintext_frame.cpp new file mode 100644 index 0000000000..79bffaf953 --- /dev/null +++ b/tests/benchmarks/components/api/bench_plaintext_frame.cpp @@ -0,0 +1,162 @@ +#include "esphome/core/defines.h" +#ifdef USE_API_PLAINTEXT + +#include +#include +#include +#include +#include +#include + +#include "esphome/components/api/api_frame_helper_plaintext.h" +#include "esphome/components/api/api_pb2.h" +#include "esphome/components/api/api_buffer.h" + +namespace esphome::api::benchmarks { + +static constexpr int kInnerIterations = 2000; + +// Helper to drain accumulated data from the read side of a socket +// to prevent the write side from blocking. +static void drain_socket(int fd) { + char buf[65536]; + while (::read(fd, buf, sizeof(buf)) > 0) { + } +} + +// Helper to create a TCP loopback connection with an APIPlaintextFrameHelper +// on the write end. Returns the helper and the read-side fd. +// Uses real TCP sockets so TCP_NODELAY succeeds during init(). +static std::pair, int> create_plaintext_helper() { + // Create a TCP listener on loopback + int listen_fd = ::socket(AF_INET, SOCK_STREAM, 0); + int opt = 1; + ::setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); + + struct sockaddr_in addr {}; + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + addr.sin_port = 0; // OS-assigned port + ::bind(listen_fd, reinterpret_cast(&addr), sizeof(addr)); + ::listen(listen_fd, 1); + + // Get the assigned port + socklen_t addr_len = sizeof(addr); + ::getsockname(listen_fd, reinterpret_cast(&addr), &addr_len); + + // Connect from client side + int write_fd = ::socket(AF_INET, SOCK_STREAM, 0); + ::connect(write_fd, reinterpret_cast(&addr), sizeof(addr)); + + // Accept on server side (this is our read fd) + int read_fd = ::accept(listen_fd, nullptr, nullptr); + ::close(listen_fd); + + // Make both ends non-blocking + int flags = ::fcntl(write_fd, F_GETFL, 0); + ::fcntl(write_fd, F_SETFL, flags | O_NONBLOCK); + flags = ::fcntl(read_fd, F_GETFL, 0); + ::fcntl(read_fd, F_SETFL, flags | O_NONBLOCK); + + // Increase socket buffer sizes to reduce drain frequency + int bufsize = 1024 * 1024; + ::setsockopt(write_fd, SOL_SOCKET, SO_SNDBUF, &bufsize, sizeof(bufsize)); + ::setsockopt(read_fd, SOL_SOCKET, SO_RCVBUF, &bufsize, sizeof(bufsize)); + + auto sock = std::make_unique(write_fd); + auto helper = std::make_unique(std::move(sock)); + helper->init(); + + return {std::move(helper), read_fd}; +} + +// --- Write a single SensorStateResponse through plaintext framing --- +// Measures the full write path: header construction, varint encoding, +// iovec assembly, and socket write. + +static void PlaintextFrame_WriteSensorState(benchmark::State &state) { + auto [helper, read_fd] = create_plaintext_helper(); + uint8_t padding = helper->frame_header_padding(); + + // Pre-init buffer to typical TCP MSS size to avoid benchmarking + // heap allocation — in real use the buffer is reused across writes. + APIBuffer buffer; + buffer.reserve(1460); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + buffer.clear(); + SensorStateResponse msg; + msg.key = 0x12345678; + msg.state = 23.5f; + msg.missing_state = false; + + uint32_t size = msg.calculate_size(); + buffer.resize(padding + size); + ProtoWriteBuffer writer(&buffer, padding); + msg.encode(writer); + + helper->write_protobuf_packet(SensorStateResponse::MESSAGE_TYPE, writer); + + if ((i & 0xFF) == 0) + drain_socket(read_fd); + } + drain_socket(read_fd); + benchmark::DoNotOptimize(helper.get()); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); + + ::close(read_fd); +} +BENCHMARK(PlaintextFrame_WriteSensorState); + +// --- Write a batch of 5 SensorStateResponses in one call --- +// Measures batched write: multiple messages assembled into one writev. + +static void PlaintextFrame_WriteBatch5(benchmark::State &state) { + auto [helper, read_fd] = create_plaintext_helper(); + uint8_t padding = helper->frame_header_padding(); + uint8_t footer = helper->frame_footer_size(); + + // Pre-init buffer to typical TCP MSS size to avoid benchmarking + // heap allocation — in real use the buffer is reused across writes. + APIBuffer buffer; + buffer.reserve(1460); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + buffer.clear(); + MessageInfo messages[5] = {{0, 0, 0}, {0, 0, 0}, {0, 0, 0}, {0, 0, 0}, {0, 0, 0}}; + + for (int j = 0; j < 5; j++) { + uint16_t offset = buffer.size(); + SensorStateResponse msg; + msg.key = static_cast(j); + msg.state = 23.5f + static_cast(j); + msg.missing_state = false; + + uint32_t size = msg.calculate_size(); + buffer.resize(offset + padding + size + footer); + ProtoWriteBuffer writer(&buffer, offset + padding); + msg.encode(writer); + + messages[j] = MessageInfo(SensorStateResponse::MESSAGE_TYPE, offset, size); + } + + helper->write_protobuf_messages(ProtoWriteBuffer(&buffer, 0), std::span(messages, 5)); + + if ((i & 0xFF) == 0) + drain_socket(read_fd); + } + drain_socket(read_fd); + benchmark::DoNotOptimize(helper.get()); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); + + ::close(read_fd); +} +BENCHMARK(PlaintextFrame_WriteBatch5); + +} // namespace esphome::api::benchmarks + +#endif // USE_API_PLAINTEXT From dd82a91d8fa8b3922e7e4b8177df5875c1d56d04 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Sun, 22 Mar 2026 10:13:17 +1000 Subject: [PATCH 035/166] [lvgl] Don't animate page change when not requested (#15069) --- esphome/components/lvgl/defines.py | 2 +- esphome/components/lvgl/lvgl_esphome.cpp | 12 ++++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/esphome/components/lvgl/defines.py b/esphome/components/lvgl/defines.py index d6d7a5e161..0a53d88669 100644 --- a/esphome/components/lvgl/defines.py +++ b/esphome/components/lvgl/defines.py @@ -280,7 +280,7 @@ SWIPE_TRIGGERS = tuple( LV_ANIM = LvConstant( - "LV_SCR_LOAD_ANIM_", + "LV_SCREEN_LOAD_ANIM_", "NONE", "OVER_LEFT", "OVER_RIGHT", diff --git a/esphome/components/lvgl/lvgl_esphome.cpp b/esphome/components/lvgl/lvgl_esphome.cpp index b1618f77c4..fb5e595713 100644 --- a/esphome/components/lvgl/lvgl_esphome.cpp +++ b/esphome/components/lvgl/lvgl_esphome.cpp @@ -176,7 +176,11 @@ void LvglComponent::show_page(size_t index, lv_scr_load_anim_t anim, uint32_t ti if (index >= this->pages_.size()) return; this->current_page_ = index; - lv_scr_load_anim(this->pages_[this->current_page_]->obj, anim, time, 0, false); + if (anim == LV_SCREEN_LOAD_ANIM_NONE) { + lv_scr_load(this->pages_[this->current_page_]->obj); + } else { + lv_scr_load_anim(this->pages_[this->current_page_]->obj, anim, time, 0, false); + } } void LvglComponent::show_next_page(lv_scr_load_anim_t anim, uint32_t time) { @@ -262,8 +266,8 @@ void LvglComponent::flush_cb_(lv_display_t *disp_drv, const lv_area_t *area, uin if (!this->is_paused()) { auto now = millis(); this->draw_buffer_(area, reinterpret_cast(color_p)); - ESP_LOGV(TAG, "flush_cb, area=%d/%d, %d/%d took %dms", area->x1, area->y1, lv_area_get_width(area), - lv_area_get_height(area), (int) (millis() - now)); + ESP_LOGV(TAG, "flush_cb, area=%d/%d, %d/%d took %dms", (int) area->x1, (int) area->y1, + (int) lv_area_get_width(area), (int) lv_area_get_height(area), (int) (millis() - now)); } lv_display_flush_ready(disp_drv); } @@ -619,7 +623,7 @@ void LvglComponent::setup() { // Rotation will be handled by our drawing function, so reset the display rotation. for (auto *disp : this->displays_) disp->set_rotation(display::DISPLAY_ROTATION_0_DEGREES); - this->show_page(0, LV_SCR_LOAD_ANIM_NONE, 0); + this->show_page(0, LV_SCREEN_LOAD_ANIM_NONE, 0); lv_display_trigger_activity(this->disp_); } From 8224da3460819a7a41fb302018f9ee3265fd46a0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 21 Mar 2026 15:32:24 -1000 Subject: [PATCH 036/166] [core] Inline Component::get_component_log_str() (#15068) --- esphome/core/component.cpp | 3 --- esphome/core/component.h | 4 +++- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index 89ac0c7a2a..caaea89143 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -272,9 +272,6 @@ void Component::call() { break; } } -const LogString *Component::get_component_log_str() const { - return this->component_source_ == nullptr ? LOG_STR("") : this->component_source_; -} bool Component::should_warn_of_blocking(uint32_t blocking_time) { if (blocking_time > this->warn_if_blocking_over_) { // Prevent overflow when adding increment - if we're about to overflow, just max out diff --git a/esphome/core/component.h b/esphome/core/component.h index 119681f64c..46cd77b034 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -294,7 +294,9 @@ class Component { * * Returns LOG_STR("") if source not set */ - const LogString *get_component_log_str() const; + const LogString *get_component_log_str() const { + return this->component_source_ == nullptr ? LOG_STR("") : this->component_source_; + } bool should_warn_of_blocking(uint32_t blocking_time); From c48fd0738ba6ec1e88860758687c2fae06fae430 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 21 Mar 2026 15:33:42 -1000 Subject: [PATCH 037/166] [mqtt] Rate-limit component resends to prevent task WDT on reconnect (#15061) --- esphome/components/mqtt/mqtt_client.cpp | 17 ++++++++++++++--- esphome/components/mqtt/mqtt_component.h | 3 +++ 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/esphome/components/mqtt/mqtt_client.cpp b/esphome/components/mqtt/mqtt_client.cpp index 38daf8f8f6..ab665e2579 100644 --- a/esphome/components/mqtt/mqtt_client.cpp +++ b/esphome/components/mqtt/mqtt_client.cpp @@ -28,6 +28,10 @@ namespace esphome::mqtt { static const char *const TAG = "mqtt"; +// Maximum number of MQTT component resends per loop iteration. +// Limits work to avoid triggering the task watchdog on reconnect. +static constexpr uint8_t MAX_RESENDS_PER_LOOP = 8; + // Disconnect reason strings indexed by MQTTClientDisconnectReason enum (0-8) PROGMEM_STRING_TABLE(MQTTDisconnectReasonStrings, "TCP disconnected", "Unacceptable Protocol Version", "Identifier Rejected", "Server Unavailable", "Malformed Credentials", "Not Authorized", @@ -396,9 +400,16 @@ void MQTTClientComponent::loop() { this->resubscribe_subscriptions_(); // Process pending resends for all MQTT components centrally - // This is more efficient than each component polling in its own loop - for (MQTTComponent *component : this->children_) { - component->process_resend(); + // Limit work per loop iteration to avoid triggering task WDT on reconnect + { + uint8_t resend_count = 0; + for (MQTTComponent *component : this->children_) { + if (component->is_resend_pending()) { + component->process_resend(); + if (++resend_count >= MAX_RESENDS_PER_LOOP) + break; + } + } } } break; diff --git a/esphome/components/mqtt/mqtt_component.h b/esphome/components/mqtt/mqtt_component.h index 2403ef64ea..7983e04870 100644 --- a/esphome/components/mqtt/mqtt_component.h +++ b/esphome/components/mqtt/mqtt_component.h @@ -147,6 +147,9 @@ class MQTTComponent : public Component { /// Internal method for the MQTT client base to schedule a resend of the state on reconnect. void schedule_resend_state(); + /// Check if a resend is pending (called by MQTTClientComponent to rate-limit work) + bool is_resend_pending() const { return this->resend_state_; } + /// Process pending resend if needed (called by MQTTClientComponent) void process_resend(); From a0d552531238ce2f4d9ae95b6d717f4ab838ed11 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Sun, 22 Mar 2026 18:01:49 +1000 Subject: [PATCH 038/166] [lvgl] Meter fixes (#15073) --- esphome/components/lvgl/lvgl_esphome.cpp | 4 +++- esphome/components/lvgl/lvgl_esphome.h | 2 +- esphome/components/lvgl/widgets/meter.py | 21 +++++++++++---------- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/esphome/components/lvgl/lvgl_esphome.cpp b/esphome/components/lvgl/lvgl_esphome.cpp index fb5e595713..b3cb4d56ad 100644 --- a/esphome/components/lvgl/lvgl_esphome.cpp +++ b/esphome/components/lvgl/lvgl_esphome.cpp @@ -671,9 +671,10 @@ void LvglComponent::static_flush_cb(lv_display_t *disp_drv, const lv_area_t *are * @param e The event data * @param color_start The color to apply to the first tick * @param color_end The color to apply to the last tick + * @param width */ void lv_scale_draw_event_cb(lv_event_t *e, uint16_t range_start, uint16_t range_end, lv_color_t color_start, - lv_color_t color_end, bool local) { + lv_color_t color_end, int width, bool local) { auto *scale = static_cast(lv_event_get_target(e)); lv_draw_task_t *task = lv_event_get_draw_task(e); @@ -691,6 +692,7 @@ void lv_scale_draw_event_cb(lv_event_t *e, uint16_t range_start, uint16_t range_ range = 1; auto ratio = (tick * 255) / range; line_dsc->color = lv_color_mix(color_end, color_start, ratio); + line_dsc->width += width; } } } diff --git a/esphome/components/lvgl/lvgl_esphome.h b/esphome/components/lvgl/lvgl_esphome.h index 4ce7296159..8e34f16c98 100644 --- a/esphome/components/lvgl/lvgl_esphome.h +++ b/esphome/components/lvgl/lvgl_esphome.h @@ -53,7 +53,7 @@ extern std::string lv_event_code_name_for(lv_event_t *event); lv_obj_t *lv_container_create(lv_obj_t *parent); #ifdef USE_LVGL_SCALE void lv_scale_draw_event_cb(lv_event_t *e, uint16_t range_start, uint16_t range_end, lv_color_t color_start, - lv_color_t color_end, bool local); + lv_color_t color_end, int width, bool local); #endif #if LV_COLOR_DEPTH == 16 static const display::ColorBitness LV_BITNESS = display::ColorBitness::COLOR_BITNESS_565; diff --git a/esphome/components/lvgl/widgets/meter.py b/esphome/components/lvgl/widgets/meter.py index 63cc645f22..6a7559c42c 100644 --- a/esphome/components/lvgl/widgets/meter.py +++ b/esphome/components/lvgl/widgets/meter.py @@ -177,7 +177,7 @@ INDICATOR_ARC_SCHEMA = cv.Schema( cv.Optional(CONF_VALUE): lv_float, cv.Optional(CONF_START_VALUE): lv_float, cv.Optional(CONF_END_VALUE): lv_float, - cv.Optional(CONF_OPA): opacity, + cv.Optional(CONF_OPA, default=1.0): opacity, } ).add_extra(cv.has_at_most_one_key(CONF_VALUE, CONF_START_VALUE)) @@ -247,7 +247,7 @@ SCALE_SCHEMA = cv.Schema( cv.Optional(CONF_RANGE_FROM, default=0.0): lv_int, cv.Optional(CONF_RANGE_TO, default=100.0): lv_int, cv.Optional(CONF_ANGLE_RANGE, default=270): lv_angle_degrees, - cv.Optional(CONF_ROTATION, default=0): lv_angle_degrees, + cv.Optional(CONF_ROTATION): lv_angle_degrees, cv.Optional(CONF_INDICATORS): cv.ensure_list(INDICATOR_SCHEMA), cv.Optional(CONF_DRAW_TICKS_ON_TOP, default=True): bool, } @@ -329,7 +329,7 @@ class MeterType(WidgetType): ) def get_uses(self): - return CONF_SCALE, CONF_LINE + return CONF_SCALE, CONF_LINE, CONF_IMAGE def validate(self, value): return cv.has_at_most_one_key(CONF_INDICATOR, CONF_PIVOT)(value) @@ -366,16 +366,17 @@ class MeterType(WidgetType): lv.scale_set_range(scale_var, range_from, range_to) angle_range = await lv_angle_degrees.process(scale_conf[CONF_ANGLE_RANGE]) - rotation = await lv_angle_degrees.process(scale_conf[CONF_ROTATION]) + if (rotation := scale_conf.get(CONF_ROTATION)) is not None: + rotation = await lv_angle_degrees.process(rotation) + else: + rotation = 90 + (360 - angle_range) // 2 + # Set angle range lv.scale_set_angle_range( scale_var, angle_range, ) - - # Set rotation if specified - if rotation: - lv.scale_set_rotation(scale_var, rotation) + lv.scale_set_rotation(scale_var, rotation) # Handle indicators as sections for indicator in scale_conf.get(CONF_INDICATORS, ()): @@ -393,10 +394,9 @@ class MeterType(WidgetType): props = { "arc_width": v[CONF_WIDTH], "arc_color": v[CONF_COLOR], + "arc_opa": v[CONF_OPA], "arc_rounded": v.get("arc_rounded", False), } - if (opa := v.get(CONF_OPA)) is not None: - props["arc_opa"] = opa if CONF_R_MOD in v: get_warnings().add( "The 'r_mod' indicator property is not supported in LVGL 9.x and will be ignored." @@ -424,6 +424,7 @@ class MeterType(WidgetType): end_value, color_start, color_end, + v[CONF_WIDTH], local, ) lv_obj.add_event_cb( From 5e68282519caf8601bf2192923b1975d85580044 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Mar 2026 10:14:52 -1000 Subject: [PATCH 039/166] [light] Fix constant_brightness broken by gamma LUT refactor (#15048) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/light/light_color_values.h | 10 + esphome/components/light/light_state.cpp | 47 ++++- .../fixtures/light_constant_brightness.yaml | 57 ++++++ .../test_light_constant_brightness.py | 188 ++++++++++++++++++ 4 files changed, 296 insertions(+), 6 deletions(-) create mode 100644 tests/integration/fixtures/light_constant_brightness.yaml create mode 100644 tests/integration/test_light_constant_brightness.py diff --git a/esphome/components/light/light_color_values.h b/esphome/components/light/light_color_values.h index 3a9ca8c8c2..a2c2dbca46 100644 --- a/esphome/components/light/light_color_values.h +++ b/esphome/components/light/light_color_values.h @@ -154,6 +154,16 @@ class LightColorValues { } /// Convert these light color values to an CWWW representation with the given parameters. + /// + /// Note on gamma and constant_brightness: This method operates on the raw/internal channel + /// values stored in this object. For cold_white_ and warm_white_ specifically, these + /// may already be gamma-uncorrected when derived from a color_temperature value. + /// For constant_brightness=false, additional gamma for the output can be applied after + /// this method since gamma commutes with simple multiplication. For constant_brightness=true, + /// the caller (LightState::current_values_as_cwww) must apply gamma to the individual + /// channel values BEFORE the balancing formula, because the nonlinear max/sum ratio does + /// not commute with gamma. See LightState::current_values_as_cwww() for the correct + /// implementation. void as_cwww(float *cold_white, float *warm_white, bool constant_brightness = false) const { if (this->color_mode_ & ColorCapability::COLD_WARM_WHITE) { const float cw_level = this->cold_white_; diff --git a/esphome/components/light/light_state.cpp b/esphome/components/light/light_state.cpp index 161092532a..1b736d84f6 100644 --- a/esphome/components/light/light_state.cpp +++ b/esphome/components/light/light_state.cpp @@ -223,12 +223,11 @@ void LightState::current_values_as_rgbw(float *red, float *green, float *blue, f } void LightState::current_values_as_rgbww(float *red, float *green, float *blue, float *cold_white, float *warm_white, bool constant_brightness) { - this->current_values.as_rgbww(red, green, blue, cold_white, warm_white, constant_brightness); + this->current_values.as_rgb(red, green, blue); *red = this->gamma_correct_lut(*red); *green = this->gamma_correct_lut(*green); *blue = this->gamma_correct_lut(*blue); - *cold_white = this->gamma_correct_lut(*cold_white); - *warm_white = this->gamma_correct_lut(*warm_white); + this->current_values_as_cwww(cold_white, warm_white, constant_brightness); } void LightState::current_values_as_rgbct(float *red, float *green, float *blue, float *color_temperature, float *white_brightness) { @@ -241,9 +240,45 @@ void LightState::current_values_as_rgbct(float *red, float *green, float *blue, *white_brightness = this->gamma_correct_lut(*white_brightness); } void LightState::current_values_as_cwww(float *cold_white, float *warm_white, bool constant_brightness) { - this->current_values.as_cwww(cold_white, warm_white, constant_brightness); - *cold_white = this->gamma_correct_lut(*cold_white); - *warm_white = this->gamma_correct_lut(*warm_white); + if (!constant_brightness) { + // Without constant_brightness, gamma commutes with simple multiplication: + // gamma(white_level * cw) = gamma(white_level) * gamma(cw) + // (since gamma(a*b) = (a*b)^g = a^g * b^g = gamma(a) * gamma(b)) + // so applying gamma after is mathematically equivalent and simpler. + this->current_values.as_cwww(cold_white, warm_white, false); + *cold_white = this->gamma_correct_lut(*cold_white); + *warm_white = this->gamma_correct_lut(*warm_white); + return; + } + + // For constant_brightness mode, gamma MUST be applied to the individual + // channel values BEFORE the balancing formula (max/sum ratio), not after. + // + // Why: The cold_white_ and warm_white_ values stored in LightColorValues + // are gamma-uncorrected (see transform_parameters_() which applies + // gamma_uncorrect to the linear CW/WW fractions derived from color + // temperature). Applying gamma_correct here recovers the original linear + // fractions, which the constant_brightness formula then uses to distribute + // power evenly. The max/sum formula ensures cold+warm PWM output sums to + // a constant, keeping total power (and perceived brightness) the same + // across all color temperatures. + // + // Applying gamma AFTER the formula would be incorrect because gamma is + // nonlinear: gamma(a/b) != gamma(a)/gamma(b), so the carefully balanced + // ratio would be distorted, causing a severe brightness dip at mid-range + // color temperatures. + const auto &v = this->current_values; + if (!(v.get_color_mode() & ColorCapability::COLD_WARM_WHITE)) { + *cold_white = *warm_white = 0; + return; + } + + const float cw_level = this->gamma_correct_lut(v.get_cold_white()); + const float ww_level = this->gamma_correct_lut(v.get_warm_white()); + const float white_level = this->gamma_correct_lut(v.get_state() * v.get_brightness()); + const float sum = cw_level > 0 || ww_level > 0 ? cw_level + ww_level : 1; // Don't divide by zero. + *cold_white = white_level * std::max(cw_level, ww_level) * cw_level / sum; + *warm_white = white_level * std::max(cw_level, ww_level) * ww_level / sum; } void LightState::current_values_as_ct(float *color_temperature, float *white_brightness) { auto traits = this->get_traits(); diff --git a/tests/integration/fixtures/light_constant_brightness.yaml b/tests/integration/fixtures/light_constant_brightness.yaml new file mode 100644 index 0000000000..4357a16d58 --- /dev/null +++ b/tests/integration/fixtures/light_constant_brightness.yaml @@ -0,0 +1,57 @@ +esphome: + name: light-cb-test +host: +api: # Port will be automatically injected +logger: + level: DEBUG + +output: + - platform: template + id: cb_cold_white_output + type: float + write_action: + - logger.log: + format: "CB_CW_OUTPUT:%.6f" + args: [state] + - platform: template + id: cb_warm_white_output + type: float + write_action: + - logger.log: + format: "CB_WW_OUTPUT:%.6f" + args: [state] + - platform: template + id: ncb_cold_white_output + type: float + write_action: + - logger.log: + format: "NCB_CW_OUTPUT:%.6f" + args: [state] + - platform: template + id: ncb_warm_white_output + type: float + write_action: + - logger.log: + format: "NCB_WW_OUTPUT:%.6f" + args: [state] + +light: + - platform: cwww + name: "Test CB Light" + id: test_cb_light + cold_white: cb_cold_white_output + warm_white: cb_warm_white_output + cold_white_color_temperature: 6536 K + warm_white_color_temperature: 2000 K + constant_brightness: true + gamma_correct: 2.8 + + - platform: cwww + name: "Test NCB Light" + id: test_ncb_light + cold_white: ncb_cold_white_output + warm_white: ncb_warm_white_output + cold_white_color_temperature: 6536 K + warm_white_color_temperature: 2000 K + constant_brightness: false + gamma_correct: 2.8 diff --git a/tests/integration/test_light_constant_brightness.py b/tests/integration/test_light_constant_brightness.py new file mode 100644 index 0000000000..622dc0e065 --- /dev/null +++ b/tests/integration/test_light_constant_brightness.py @@ -0,0 +1,188 @@ +"""Integration test for constant_brightness with gamma correction. + +Tests both constant_brightness: true and false cwww lights with gamma +correction in a single compilation to verify: +- constant_brightness: true maintains constant total CW+WW power output +- constant_brightness: false correctly varies total power across color temps + +This is a regression test for https://github.com/esphome/esphome/issues/15040 +where the gamma LUT refactor (#14123) broke constant_brightness by applying +gamma after the balancing formula instead of before it. +""" + +from __future__ import annotations + +import asyncio +import re +from typing import Any + +from aioesphomeapi import EntityState, LightInfo, LightState +import pytest + +from .state_utils import InitialStateHelper +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_light_constant_brightness( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test constant_brightness true and false behavior with gamma correction.""" + # Track output values for both lights from log lines + cb_cw_pattern = re.compile(r"(? None: + for pattern, key in [ + (cb_cw_pattern, "cb_cw"), + (cb_ww_pattern, "cb_ww"), + (ncb_cw_pattern, "ncb_cw"), + (ncb_ww_pattern, "ncb_ww"), + ]: + match = pattern.search(line) + if match: + latest[key] = float(match.group(1)) + + loop = asyncio.get_running_loop() + + async with ( + run_compiled(yaml_config, line_callback=on_log_line), + api_client_connected() as client, + ): + entities, _ = await client.list_entities_services() + lights = [e for e in entities if isinstance(e, LightInfo)] + cb_light = next(e for e in lights if e.object_id.endswith("cb_light")) + ncb_light = next(e for e in lights if e.object_id.endswith("ncb_light")) + + # Use InitialStateHelper to wait for initial state broadcast + initial_state_helper = InitialStateHelper(entities) + + # Track state changes per light key + state_futures: dict[int, asyncio.Future[EntityState]] = {} + + def on_state(state: EntityState) -> None: + if isinstance(state, LightState) and state.key in state_futures: + future = state_futures[state.key] + if not future.done(): + future.set_result(state) + + client.subscribe_states(initial_state_helper.on_state_wrapper(on_state)) + + try: + await initial_state_helper.wait_for_initial_states() + except TimeoutError: + pytest.fail("Timeout waiting for initial states") + + async def send_and_wait( + light_key: int, timeout: float = 5.0, **kwargs: Any + ) -> LightState: + """Send a light command and wait for the state response.""" + state_futures[light_key] = loop.create_future() + client.light_command(key=light_key, **kwargs) + try: + return await asyncio.wait_for(state_futures[light_key], timeout=timeout) + except TimeoutError: + pytest.fail(f"Timeout waiting for light state after command: {kwargs}") + + # --- Test constant_brightness: true --- + + # Turn on CB light at full brightness + await send_and_wait( + cb_light.key, + state=True, + brightness=1.0, + color_temperature=153.0, + transition_length=0, + ) + + test_mireds = [ + 153.0, # Pure cold white + 200.0, # Mostly cold + 280.0, # Mixed + 326.5, # Midpoint + 400.0, # Mostly warm + 500.0, # Pure warm white + ] + + cb_totals: list[tuple[float, float, float]] = [] + for mireds in test_mireds: + await send_and_wait( + cb_light.key, color_temperature=mireds, transition_length=0 + ) + cb_totals.append((mireds, latest["cb_cw"], latest["cb_ww"])) + + # All totals should be approximately equal (constant brightness) + reference_total = next((cw + ww for _, cw, ww in cb_totals if cw + ww > 0), 0) + assert reference_total > 0, ( + f"Reference total power is zero, CB light outputs not working. " + f"Values: {cb_totals}" + ) + + for mireds, cw, ww in cb_totals: + total = cw + ww + assert total == pytest.approx(reference_total, rel=0.05), ( + f"constant_brightness: Total power at {mireds} mireds " + f"({total:.4f}) differs from reference ({reference_total:.4f}) " + f"by more than 5%. CW={cw:.4f}, WW={ww:.4f}. " + f"All values: {cb_totals}" + ) + + # --- Test constant_brightness: false --- + + # Turn on NCB light at full brightness + await send_and_wait( + ncb_light.key, + state=True, + brightness=1.0, + color_temperature=153.0, + transition_length=0, + ) + + ncb_totals: list[tuple[float, float, float]] = [] + for mireds in test_mireds: + await send_and_wait( + ncb_light.key, color_temperature=mireds, transition_length=0 + ) + ncb_totals.append((mireds, latest["ncb_cw"], latest["ncb_ww"])) + + extreme_cw = ncb_totals[0] # 153 mireds - pure cold + extreme_ww = ncb_totals[-1] # 500 mireds - pure warm + midpoint = ncb_totals[3] # 326.5 mireds - midpoint + + # At pure cold white, WW should be ~0 + assert extreme_cw[2] == pytest.approx(0.0, abs=0.01), ( + f"Pure cold white should have WW~0, got WW={extreme_cw[2]:.4f}" + ) + # At pure warm white, CW should be ~0 + assert extreme_ww[1] == pytest.approx(0.0, abs=0.01), ( + f"Pure warm white should have CW~0, got CW={extreme_ww[1]:.4f}" + ) + + # At midpoint, both channels should be non-zero + assert midpoint[1] > 0.05, f"Midpoint CW should be >0.05, got {midpoint[1]:.4f}" + assert midpoint[2] > 0.05, f"Midpoint WW should be >0.05, got {midpoint[2]:.4f}" + + # Total power at midpoint should be higher than at the extremes + midpoint_total = midpoint[1] + midpoint[2] + extreme_cw_total = extreme_cw[1] + extreme_cw[2] + extreme_ww_total = extreme_ww[1] + extreme_ww[2] + + assert midpoint_total > extreme_cw_total, ( + f"Midpoint total ({midpoint_total:.4f}) should be > pure CW total " + f"({extreme_cw_total:.4f}). All values: {ncb_totals}" + ) + assert midpoint_total > extreme_ww_total, ( + f"Midpoint total ({midpoint_total:.4f}) should be > pure WW total " + f"({extreme_ww_total:.4f}). All values: {ncb_totals}" + ) From ca0523b86ca11cf7eac5bb86c1ff762279371aff Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Mar 2026 10:16:46 -1000 Subject: [PATCH 040/166] [sht4x] Fix heater causing measurement jitter (#15030) --- esphome/components/sht4x/sht4x.cpp | 40 ++++++++++++++++-------------- esphome/components/sht4x/sht4x.h | 3 ++- tests/components/sht4x/common.yaml | 4 +++ 3 files changed, 28 insertions(+), 19 deletions(-) diff --git a/esphome/components/sht4x/sht4x.cpp b/esphome/components/sht4x/sht4x.cpp index bf23e42e66..43c2436a56 100644 --- a/esphome/components/sht4x/sht4x.cpp +++ b/esphome/components/sht4x/sht4x.cpp @@ -9,14 +9,12 @@ static const char *const TAG = "sht4x"; static const uint8_t MEASURECOMMANDS[] = {0xFD, 0xF6, 0xE0}; static const uint8_t SERIAL_NUMBER_COMMAND = 0x89; -void SHT4XComponent::start_heater_() { - uint8_t cmd[] = {this->heater_command_}; - - ESP_LOGD(TAG, "Heater turning on"); - if (this->write(cmd, 1) != i2c::ERROR_OK) { - this->status_set_error(LOG_STR("Failed to turn on heater")); - } -} +// Conversion constants from SHT4x datasheet +static constexpr float TEMPERATURE_OFFSET = -45.0f; +static constexpr float TEMPERATURE_SPAN = 175.0f; +static constexpr float HUMIDITY_OFFSET = -6.0f; +static constexpr float HUMIDITY_SPAN = 125.0f; +static constexpr float RAW_MAX = 65535.0f; void SHT4XComponent::read_serial_number_() { uint16_t buffer[2]; @@ -39,8 +37,8 @@ void SHT4XComponent::setup() { this->read_serial_number_(); if (std::isfinite(this->duty_cycle_) && this->duty_cycle_ > 0.0f) { - uint32_t heater_interval = static_cast(static_cast(this->heater_time_) / this->duty_cycle_); - ESP_LOGD(TAG, "Heater interval: %" PRIu32, heater_interval); + this->heater_interval_ = static_cast(static_cast(this->heater_time_) / this->duty_cycle_); + ESP_LOGD(TAG, "Heater interval: %" PRIu32, this->heater_interval_); if (this->heater_power_ == SHT4X_HEATERPOWER_HIGH) { if (this->heater_time_ == SHT4X_HEATERTIME_LONG) { @@ -62,8 +60,6 @@ void SHT4XComponent::setup() { } } ESP_LOGD(TAG, "Heater command: %x", this->heater_command_); - - this->set_interval(heater_interval, [this]() { this->start_heater_(); }); } } @@ -106,19 +102,27 @@ void SHT4XComponent::update() { // Evaluate and publish measurements if (this->temp_sensor_ != nullptr) { // Temp is contained in the first result word - float sensor_value_temp = buffer[0]; - float temp = -45 + 175 * sensor_value_temp / 65535; - + float temp = TEMPERATURE_OFFSET + TEMPERATURE_SPAN * static_cast(buffer[0]) / RAW_MAX; this->temp_sensor_->publish_state(temp); } if (this->humidity_sensor_ != nullptr) { // Relative humidity is in the second result word - float sensor_value_rh = buffer[1]; - float rh = -6 + 125 * sensor_value_rh / 65535; - + float rh = HUMIDITY_OFFSET + HUMIDITY_SPAN * static_cast(buffer[1]) / RAW_MAX; this->humidity_sensor_->publish_state(rh); } + + // Fire heater after measurement to maximize cooldown time before the next reading. + // The heater command produces a measurement that we don't need (datasheet 4.9). + if (this->heater_interval_ > 0) { + uint32_t now = millis(); + if (now - this->last_heater_millis_ >= this->heater_interval_) { + ESP_LOGD(TAG, "Heater turning on"); + if (this->write_command(this->heater_command_)) { + this->last_heater_millis_ = now; + } + } + } }); } diff --git a/esphome/components/sht4x/sht4x.h b/esphome/components/sht4x/sht4x.h index aec0f3d7f8..51f473fe3f 100644 --- a/esphome/components/sht4x/sht4x.h +++ b/esphome/components/sht4x/sht4x.h @@ -35,9 +35,10 @@ class SHT4XComponent : public PollingComponent, public sensirion_common::Sensiri SHT4XHEATERTIME heater_time_; float duty_cycle_; - void start_heater_(); void read_serial_number_(); uint8_t heater_command_; + uint32_t heater_interval_{0}; + uint32_t last_heater_millis_{0}; uint32_t serial_number_; sensor::Sensor *temp_sensor_{nullptr}; diff --git a/tests/components/sht4x/common.yaml b/tests/components/sht4x/common.yaml index 50d5ad8ca4..bec192d6db 100644 --- a/tests/components/sht4x/common.yaml +++ b/tests/components/sht4x/common.yaml @@ -6,4 +6,8 @@ sensor: humidity: name: SHT4X Humidity address: 0x44 + precision: High + heater_max_duty: 0.02 + heater_power: High + heater_time: Long update_interval: 15s From ba4be2a904b18d0509b9768aa5076bb9e218b8fa Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Mar 2026 10:17:59 -1000 Subject: [PATCH 041/166] [uart] Fix RTL87xx compilation failure due to SUCCESS macro collision (#15054) --- esphome/components/uart/uart_component.h | 5 +++++ tests/components/uart/test.rtl87xx-ard.yaml | 14 ++++++++++++++ 2 files changed, 19 insertions(+) create mode 100644 tests/components/uart/test.rtl87xx-ard.yaml diff --git a/esphome/components/uart/uart_component.h b/esphome/components/uart/uart_component.h index ee2b006039..00a4c78878 100644 --- a/esphome/components/uart/uart_component.h +++ b/esphome/components/uart/uart_component.h @@ -30,12 +30,17 @@ enum UARTDirection { const LogString *parity_to_str(UARTParityOptions parity); /// Result of a flush() call. +// Some vendor SDKs (e.g., Realtek) define SUCCESS as a macro. +// Save and restore around the enum to avoid collisions with our scoped enum value. +#pragma push_macro("SUCCESS") +#undef SUCCESS enum class FlushResult { SUCCESS, ///< Confirmed: all bytes left the TX FIFO. TIMEOUT, ///< Confirmed: timed out before TX completed. FAILED, ///< Confirmed: driver or hardware error. ASSUMED_SUCCESS, ///< Platform cannot report result; success is assumed. }; +#pragma pop_macro("SUCCESS") class UARTComponent { public: diff --git a/tests/components/uart/test.rtl87xx-ard.yaml b/tests/components/uart/test.rtl87xx-ard.yaml new file mode 100644 index 0000000000..414bf1f14d --- /dev/null +++ b/tests/components/uart/test.rtl87xx-ard.yaml @@ -0,0 +1,14 @@ +uart: + - id: uart_id + tx_pin: PA23 + rx_pin: PA18 + baud_rate: 9600 + data_bits: 8 + parity: NONE + stop_bits: 1 + +switch: + - platform: uart + name: "UART Switch" + uart_id: uart_id + data: [0x01, 0x02, 0x03] From 6a77b8b1f457fb5d2be9e8863b82e0328c8b4b29 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Mar 2026 10:19:28 -1000 Subject: [PATCH 042/166] [light] Fix gamma LUT quantizing small brightness to zero (#15060) --- esphome/components/light/__init__.py | 28 +++-- tests/unit_tests/components/light/__init__.py | 0 .../components/light/test_gamma_table.py | 117 ++++++++++++++++++ 3 files changed, 138 insertions(+), 7 deletions(-) create mode 100644 tests/unit_tests/components/light/__init__.py create mode 100644 tests/unit_tests/components/light/test_gamma_table.py diff --git a/esphome/components/light/__init__.py b/esphome/components/light/__init__.py index 4403281116..64452e4282 100644 --- a/esphome/components/light/__init__.py +++ b/esphome/components/light/__init__.py @@ -81,18 +81,32 @@ def _get_data() -> LightData: return CORE.data[DOMAIN] +def generate_gamma_table(gamma_correct: float) -> list[HexInt]: + """Generate a 256-entry uint16 gamma lookup table. + + For gamma > 0, non-zero indices are clamped to a minimum of 1 to preserve + the invariant that non-zero input always produces non-zero output. Without + this, small brightness values (e.g. 1%) get quantized to exactly 0.0, + which breaks zero_means_zero logic in FloatOutput. + """ + if gamma_correct > 0: + return [ + HexInt( + max(1, min(65535, int(round((i / 255.0) ** gamma_correct * 65535)))) + if i > 0 + else HexInt(0) + ) + for i in range(256) + ] + return [HexInt(int(round(i / 255.0 * 65535))) for i in range(256)] + + def _get_or_create_gamma_table(gamma_correct): data = _get_data() if gamma_correct in data.gamma_tables: return data.gamma_tables[gamma_correct] - if gamma_correct > 0: - forward = [ - HexInt(min(65535, int(round((i / 255.0) ** gamma_correct * 65535)))) - for i in range(256) - ] - else: - forward = [HexInt(int(round(i / 255.0 * 65535))) for i in range(256)] + forward = generate_gamma_table(gamma_correct) gamma_str = f"{gamma_correct}".replace(".", "_") fwd_id = ID(f"gamma_{gamma_str}_fwd", is_declaration=True, type=cg.uint16) diff --git a/tests/unit_tests/components/light/__init__.py b/tests/unit_tests/components/light/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/unit_tests/components/light/test_gamma_table.py b/tests/unit_tests/components/light/test_gamma_table.py new file mode 100644 index 0000000000..a302a355dc --- /dev/null +++ b/tests/unit_tests/components/light/test_gamma_table.py @@ -0,0 +1,117 @@ +"""Tests for the gamma LUT table generation.""" + +import pytest + +from esphome.components.light import generate_gamma_table + + +def _simulate_gamma_correct_lut(table: list[int], value: float) -> float: + """Simulate the C++ gamma_correct_lut interpolation from light_state.cpp.""" + if value <= 0.0: + return 0.0 + if value >= 1.0: + return 1.0 + scaled = value * 255.0 + idx = int(scaled) + if idx >= 255: + return table[255] / 65535.0 + frac = scaled - idx + a = float(table[idx]) + b = float(table[idx + 1]) + return (a + frac * (b - a)) / 65535.0 + + +def test_table_length() -> None: + """Table must always have exactly 256 entries.""" + table = generate_gamma_table(2.8) + assert len(table) == 256 + + +def test_index_zero_is_zero() -> None: + """Index 0 must be 0 so true off remains off.""" + for gamma in (1.0, 2.0, 2.2, 2.8, 3.0): + table = generate_gamma_table(gamma) + assert table[0] == 0, f"gamma={gamma}" + + +def test_index_255_is_max() -> None: + """Index 255 must be 65535 (full on).""" + for gamma in (1.0, 2.0, 2.2, 2.8, 3.0): + table = generate_gamma_table(gamma) + assert table[255] == 65535, f"gamma={gamma}" + + +@pytest.mark.parametrize("gamma", [1.0, 2.0, 2.2, 2.8, 3.0]) +def test_nonzero_indices_are_nonzero(gamma: float) -> None: + """All indices > 0 must produce non-zero values. + + This prevents zero_means_zero breakage: non-zero input must always + produce non-zero output so FloatOutput applies min_power scaling. + """ + table = generate_gamma_table(gamma) + for i in range(1, 256): + assert table[i] >= 1, f"gamma={gamma}, index {i}: got {table[i]}" + + +@pytest.mark.parametrize("gamma", [1.0, 2.0, 2.2, 2.8, 3.0]) +def test_table_monotonically_nondecreasing(gamma: float) -> None: + """The gamma table must be monotonically non-decreasing.""" + table = generate_gamma_table(gamma) + for i in range(1, 256): + assert table[i] >= table[i - 1], ( + f"gamma={gamma}: table[{i}]={table[i]} < table[{i - 1}]={table[i - 1]}" + ) + + +def test_linear_gamma() -> None: + """With gamma=0 (linear), table should be evenly spaced.""" + table = generate_gamma_table(0) + assert table[0] == 0 + assert table[128] == round(128 / 255.0 * 65535) + assert table[255] == 65535 + + +@pytest.mark.parametrize("brightness", [0.01, 0.005, 0.001, 1 / 255]) +def test_small_brightness_nonzero_after_lut(brightness: float) -> None: + """Small but non-zero brightness must produce non-zero output through the LUT. + + Regression test for #15055: with zero_means_zero=true, a gamma-corrected + value of exactly 0.0 causes FloatOutput to skip min_power scaling, turning + the LED off instead of to minimum brightness. + """ + table = generate_gamma_table(2.8) + result = _simulate_gamma_correct_lut(table, brightness) + assert result > 0.0, ( + f"brightness={brightness}: gamma LUT returned 0.0, would break zero_means_zero" + ) + + +@pytest.mark.parametrize("gamma", [1.0, 2.0, 2.2, 2.8, 3.0]) +def test_small_brightness_nonzero_all_gammas(gamma: float) -> None: + """1% brightness must be non-zero for all common gamma values.""" + table = generate_gamma_table(gamma) + result = _simulate_gamma_correct_lut(table, 0.01) + assert result > 0.0, f"gamma={gamma}: 1% brightness returned 0.0" + + +def test_lut_zero_returns_zero() -> None: + """LUT with input 0.0 must return 0.0.""" + table = generate_gamma_table(2.8) + assert _simulate_gamma_correct_lut(table, 0.0) == 0.0 + + +def test_lut_one_returns_one() -> None: + """LUT with input 1.0 must return 1.0.""" + table = generate_gamma_table(2.8) + assert _simulate_gamma_correct_lut(table, 1.0) == 1.0 + + +def test_lut_output_monotonically_nondecreasing() -> None: + """LUT output must be monotonically non-decreasing across the full range.""" + table = generate_gamma_table(2.8) + prev = 0.0 + for i in range(1001): + value = i / 1000.0 + result = _simulate_gamma_correct_lut(table, value) + assert result >= prev, f"value={value}: result {result} < previous {prev}" + prev = result From 12b10d8b89721d4e58ccef224eec4c7c9f37ea6c Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Sun, 22 Mar 2026 16:19:46 -0400 Subject: [PATCH 043/166] [ultrasonic] Fix ISR edge detection with debounce and trigger filtering (#15014) --- .../ultrasonic/ultrasonic_sensor.cpp | 29 +++++++++---------- .../components/ultrasonic/ultrasonic_sensor.h | 3 +- 2 files changed, 14 insertions(+), 18 deletions(-) diff --git a/esphome/components/ultrasonic/ultrasonic_sensor.cpp b/esphome/components/ultrasonic/ultrasonic_sensor.cpp index d3f7e69444..a354b198b4 100644 --- a/esphome/components/ultrasonic/ultrasonic_sensor.cpp +++ b/esphome/components/ultrasonic/ultrasonic_sensor.cpp @@ -6,11 +6,17 @@ namespace esphome::ultrasonic { static const char *const TAG = "ultrasonic.sensor"; +static constexpr uint32_t DEBOUNCE_US = 50; // Ignore edges within 50us of each other (noise filtering) +static constexpr uint32_t START_DELAY_US = 100; // Ignore edges within 100us of trigger (filters bleed-through) static constexpr uint32_t START_TIMEOUT_US = 40000; // Maximum time to wait for echo pulse to start void IRAM_ATTR UltrasonicSensorStore::gpio_intr(UltrasonicSensorStore *arg) { uint32_t now = micros(); - if (arg->echo_pin_isr.digital_read()) { + // Ignore edges after measurement complete or too soon after trigger pulse + if (arg->echo_end || (now - arg->measurement_start_us) <= START_DELAY_US) { + return; + } + if (!arg->echo_start || (now - arg->echo_start_us) <= DEBOUNCE_US) { arg->echo_start_us = now; arg->echo_start = true; } else { @@ -21,15 +27,14 @@ void IRAM_ATTR UltrasonicSensorStore::gpio_intr(UltrasonicSensorStore *arg) { void IRAM_ATTR UltrasonicSensorComponent::send_trigger_pulse_() { InterruptLock lock; - this->store_.echo_start_us = 0; - this->store_.echo_end_us = 0; this->store_.echo_start = false; this->store_.echo_end = false; + this->store_.measurement_start_us = micros(); this->trigger_pin_isr_.digital_write(true); delayMicroseconds(this->pulse_time_us_); this->trigger_pin_isr_.digital_write(false); this->measurement_pending_ = true; - this->measurement_start_us_ = micros(); + this->measurement_start_us_ = this->store_.measurement_start_us; } void UltrasonicSensorComponent::setup() { @@ -37,7 +42,6 @@ void UltrasonicSensorComponent::setup() { this->trigger_pin_->digital_write(false); this->trigger_pin_isr_ = this->trigger_pin_->to_isr(); this->echo_pin_->setup(); - this->store_.echo_pin_isr = this->echo_pin_->to_isr(); this->echo_pin_->attach_interrupt(UltrasonicSensorStore::gpio_intr, &this->store_, gpio::INTERRUPT_ANY_EDGE); } @@ -77,17 +81,10 @@ void UltrasonicSensorComponent::loop() { } if (this->store_.echo_end) { - float result; - if (this->store_.echo_start) { - uint32_t pulse_duration = this->store_.echo_end_us - this->store_.echo_start_us; - ESP_LOGV(TAG, "pulse start took %" PRIu32 "us, echo took %" PRIu32 "us", - this->store_.echo_start_us - this->measurement_start_us_, pulse_duration); - result = UltrasonicSensorComponent::us_to_m(pulse_duration); - ESP_LOGD(TAG, "'%s' - Got distance: %.3f m", this->name_.c_str(), result); - } else { - ESP_LOGW(TAG, "'%s' - pulse end before pulse start, does the echo pin need to be inverted?", this->name_.c_str()); - result = NAN; - } + uint32_t pulse_duration = this->store_.echo_end_us - this->store_.echo_start_us; + ESP_LOGV(TAG, "Echo took %" PRIu32 "us", pulse_duration); + float result = UltrasonicSensorComponent::us_to_m(pulse_duration); + ESP_LOGD(TAG, "'%s' - Got distance: %.3f m", this->name_.c_str(), result); this->publish_state(result); this->measurement_pending_ = false; return; diff --git a/esphome/components/ultrasonic/ultrasonic_sensor.h b/esphome/components/ultrasonic/ultrasonic_sensor.h index 541f7d2b70..7d333a1b24 100644 --- a/esphome/components/ultrasonic/ultrasonic_sensor.h +++ b/esphome/components/ultrasonic/ultrasonic_sensor.h @@ -11,8 +11,7 @@ namespace esphome::ultrasonic { struct UltrasonicSensorStore { static void gpio_intr(UltrasonicSensorStore *arg); - ISRInternalGPIOPin echo_pin_isr; - volatile uint32_t wait_start_us{0}; + volatile uint32_t measurement_start_us{0}; volatile uint32_t echo_start_us{0}; volatile uint32_t echo_end_us{0}; volatile bool echo_start{false}; From c917b8ce06b80f1db580d84c2512cc981dc7e687 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Mar 2026 10:20:28 -1000 Subject: [PATCH 044/166] [logger] Fix race condition in task log buffer initialization (#15071) 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/logger/__init__.py | 39 ++++++++++++++++----------- esphome/components/logger/logger.cpp | 20 ++++++-------- esphome/components/logger/logger.h | 5 ++-- 3 files changed, 35 insertions(+), 29 deletions(-) diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index a5601e6a8f..3da81e12a0 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -331,11 +331,27 @@ async def to_code(config: ConfigType) -> None: CORE.data.setdefault(CONF_LOGGER, {})[CONF_LEVEL] = level tx_buffer_size = config[CONF_TX_BUFFER_SIZE] cg.add_define("ESPHOME_LOGGER_TX_BUFFER_SIZE", tx_buffer_size) - log = cg.new_Pvariable( - config[CONF_ID], - baud_rate, - ) - if CORE.is_esp32: + # Determine task log buffer size and define USE_ESPHOME_TASK_LOG_BUFFER early + # so the constructor can allocate the buffer immediately, preventing a race + # where another task logs before the buffer is initialized. + task_log_buffer_size = 0 + if CORE.is_esp32 or CORE.is_libretiny or CORE.is_nrf52: + task_log_buffer_size = config[CONF_TASK_LOG_BUFFER_SIZE] + elif CORE.is_host: + task_log_buffer_size = 64 # Fixed 64 slots for host + if task_log_buffer_size > 0: + cg.add_define("USE_ESPHOME_TASK_LOG_BUFFER") + log = cg.new_Pvariable( + config[CONF_ID], + baud_rate, + task_log_buffer_size, + ) + else: + log = cg.new_Pvariable( + config[CONF_ID], + baud_rate, + ) + if CORE.is_esp32 or CORE.is_host: cg.add(log.create_pthread_key()) # set_uart_selection() must be called before pre_setup() because # pre_setup() switches on uart_ to decide which hardware to initialize @@ -364,17 +380,10 @@ async def _late_logger_init(config: ConfigType) -> None: log = await cg.get_variable(config[CONF_ID]) level = config[CONF_LEVEL] baud_rate: int = config[CONF_BAUD_RATE] - if CORE.is_esp32 or CORE.is_libretiny or CORE.is_nrf52: - task_log_buffer_size = config[CONF_TASK_LOG_BUFFER_SIZE] + if CORE.using_zephyr: + task_log_buffer_size = config.get(CONF_TASK_LOG_BUFFER_SIZE, 0) if task_log_buffer_size > 0: - cg.add_define("USE_ESPHOME_TASK_LOG_BUFFER") - cg.add(log.init_log_buffer(task_log_buffer_size)) - if CORE.using_zephyr: - zephyr_add_prj_conf("MPSC_PBUF", True) - elif CORE.is_host: - cg.add(log.create_pthread_key()) - cg.add_define("USE_ESPHOME_TASK_LOG_BUFFER") - cg.add(log.init_log_buffer(64)) # Fixed 64 slots for host + zephyr_add_prj_conf("MPSC_PBUF", True) # Enable runtime tag levels if logs are configured or explicitly enabled logs_config = config[CONF_LOGS] diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index 497809cd2e..ceacded775 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -152,29 +152,25 @@ inline uint8_t Logger::level_for(const char *tag) { return this->current_level_; } +#ifdef USE_ESPHOME_TASK_LOG_BUFFER +Logger::Logger(uint32_t baud_rate, size_t task_log_buffer_size) : baud_rate_(baud_rate) { +#else Logger::Logger(uint32_t baud_rate) : baud_rate_(baud_rate) { +#endif #if defined(USE_ESP32) || defined(USE_LIBRETINY) this->main_task_ = xTaskGetCurrentTaskHandle(); #elif defined(USE_ZEPHYR) this->main_task_ = k_current_get(); #elif defined(USE_HOST) - this->main_thread_ = pthread_self(); +this->main_thread_ = pthread_self(); #endif -} #ifdef USE_ESPHOME_TASK_LOG_BUFFER -void Logger::init_log_buffer(size_t total_buffer_size) { - // Host uses slot count instead of byte size // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) - allocated once, never freed - this->log_buffer_ = new logger::TaskLogBuffer(total_buffer_size); - -#if !(defined(USE_ZEPHYR) && defined(USE_LOGGER_UART_SELECTION_USB_CDC)) - // Start with loop disabled when using task buffer - // The loop will be enabled automatically when messages arrive - // Zephyr with USB CDC needs loop active to poll port readiness via cdc_loop_() - this->disable_loop_when_buffer_empty_(); + this->log_buffer_ = new logger::TaskLogBuffer(task_log_buffer_size); + // Note: we don't disable loop here because the component isn't registered with App yet. + // The loop self-disables on its first iteration when it finds no messages to process. #endif } -#endif #if defined(USE_ESPHOME_TASK_LOG_BUFFER) || (defined(USE_ZEPHYR) && defined(USE_LOGGER_UART_SELECTION_USB_CDC)) void Logger::loop() { diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index 8c38cadcbc..c81b8e4e94 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -143,9 +143,10 @@ enum UARTSelection : uint8_t { */ class Logger final : public Component { public: - explicit Logger(uint32_t baud_rate); #ifdef USE_ESPHOME_TASK_LOG_BUFFER - void init_log_buffer(size_t total_buffer_size); + explicit Logger(uint32_t baud_rate, size_t task_log_buffer_size); +#else + explicit Logger(uint32_t baud_rate); #endif #if defined(USE_ESPHOME_TASK_LOG_BUFFER) || (defined(USE_ZEPHYR) && defined(USE_LOGGER_UART_SELECTION_USB_CDC)) void loop() override; From 2b6d63fd09a7c4d40385114bd9222037397804da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jason=20K=C3=B6lker?= Date: Sun, 22 Mar 2026 20:21:08 +0000 Subject: [PATCH 045/166] [pmsx003] Keep active-mode reads aligned (#14832) --- esphome/components/pmsx003/pmsx003.cpp | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/esphome/components/pmsx003/pmsx003.cpp b/esphome/components/pmsx003/pmsx003.cpp index 114ecf435e..6275ff60c2 100644 --- a/esphome/components/pmsx003/pmsx003.cpp +++ b/esphome/components/pmsx003/pmsx003.cpp @@ -95,10 +95,6 @@ void PMSX003Component::loop() { // Just go ahead and read stuff break; } - } else if (now - this->last_update_ < this->update_interval_) { - // Otherwise just leave the sensor powered up and come back when we hit the update - // time - return; } if (now - this->last_transmission_ >= 500) { @@ -114,10 +110,11 @@ void PMSX003Component::loop() { this->read_byte(&this->data_[this->data_index_]); auto check = this->check_byte_(); if (!check.has_value()) { - // finished - this->parse_data_(); + if (this->update_interval_ > STABILISING_MS || now - this->last_update_ >= this->update_interval_) { + this->parse_data_(); + this->last_update_ = now; + } this->data_index_ = 0; - this->last_update_ = now; } else if (!*check) { // wrong data this->data_index_ = 0; @@ -138,7 +135,7 @@ optional PMSX003Component::check_byte_() { return true; } - ESP_LOGW(TAG, "Start character %u mismatch: 0x%02X != 0x%02X", index + 1, byte, START_CHARACTER_1); + ESP_LOGW(TAG, "Start character %u mismatch: 0x%02X != 0x%02X", index + 1, byte, start_char); return false; } From 705d548435d648eda29e044c43d538034df7f98d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 18 Mar 2026 09:39:26 -1000 Subject: [PATCH 046/166] Bump aioesphomeapi from 44.5.2 to 44.6.0 (#14927) 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 da95dd5a13..f8f60f1932 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.19 esptool==5.2.0 click==8.3.1 esphome-dashboard==20260210.0 -aioesphomeapi==44.5.2 +aioesphomeapi==44.6.0 zeroconf==0.148.0 puremagic==1.30 ruamel.yaml==0.19.1 # dashboard_import From 99d968f80a5057ec653e9fba8a55f2aa9ec5ee14 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Mar 2026 14:06:41 -1000 Subject: [PATCH 047/166] [http_request] Prevent double update task launch (#14910) --- .../components/http_request/update/http_request_update.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/esphome/components/http_request/update/http_request_update.cpp b/esphome/components/http_request/update/http_request_update.cpp index a15dc61675..1c52a28105 100644 --- a/esphome/components/http_request/update/http_request_update.cpp +++ b/esphome/components/http_request/update/http_request_update.cpp @@ -74,6 +74,10 @@ void HttpRequestUpdate::update() { } this->cancel_interval(INITIAL_CHECK_INTERVAL_ID); #ifdef USE_ESP32 + if (this->update_task_handle_ != nullptr) { + ESP_LOGW(TAG, "Update check already in progress"); + return; + } xTaskCreate(HttpRequestUpdate::update_task, "update_task", 8192, (void *) this, 1, &this->update_task_handle_); #else this->update_task(this); @@ -204,6 +208,9 @@ defer: // both success and error paths to avoid multiple std::function instantiations. // Lambda captures only 2 pointers (8 bytes) — fits in std::function SBO on supported toolchains. this_update->defer([this_update, result]() { +#ifdef USE_ESP32 + this_update->update_task_handle_ = nullptr; +#endif if (result->error_str != nullptr) { this_update->status_set_error(result->error_str); delete result; From 8af0991590f27f870d3544e0b2bd6798380d9564 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 18 Mar 2026 20:10:42 -0400 Subject: [PATCH 048/166] [ble_client] Fix RSSI sensor reporting same value for all clients (#14939) Co-authored-by: Claude Opus 4.6 (1M context) --- esphome/components/ble_client/sensor/ble_rssi_sensor.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/components/ble_client/sensor/ble_rssi_sensor.cpp b/esphome/components/ble_client/sensor/ble_rssi_sensor.cpp index dc032a7a98..715298e592 100644 --- a/esphome/components/ble_client/sensor/ble_rssi_sensor.cpp +++ b/esphome/components/ble_client/sensor/ble_rssi_sensor.cpp @@ -47,6 +47,8 @@ void BLEClientRSSISensor::gap_event_handler(esp_gap_ble_cb_event_t event, esp_bl switch (event) { // server response on RSSI request: case ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT: + if (!this->parent()->check_addr(param->read_rssi_cmpl.remote_addr)) + return; if (param->read_rssi_cmpl.status == ESP_BT_STATUS_SUCCESS) { int8_t rssi = param->read_rssi_cmpl.rssi; ESP_LOGI(TAG, "ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT RSSI: %d", rssi); From 729d3d4bc22718d0116f3dd1d47eea7d12362ef9 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 18 Mar 2026 21:23:47 -0400 Subject: [PATCH 049/166] [openthread] Guard InstanceLock against uninitialized semaphore (#14940) Co-authored-by: Claude Opus 4.6 (1M context) --- esphome/components/openthread/openthread.h | 4 ++++ .../components/openthread/openthread_esp.cpp | 22 ++++++++++++++++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/esphome/components/openthread/openthread.h b/esphome/components/openthread/openthread.h index 75d8fe11fd..bd10774fcf 100644 --- a/esphome/components/openthread/openthread.h +++ b/esphome/components/openthread/openthread.h @@ -11,6 +11,7 @@ #include #include +#include #include #include @@ -28,6 +29,8 @@ class OpenThreadComponent : public Component { float get_setup_priority() const override { return setup_priority::WIFI; } bool is_connected() const { return this->connected_; } + /// Returns true once esp_openthread_init() has completed and the OT lock is usable. + bool is_lock_initialized() const { return this->lock_initialized_; } network::IPAddresses get_ip_addresses(); std::optional get_omr_address(); void ot_main(); @@ -51,6 +54,7 @@ class OpenThreadComponent : public Component { uint32_t poll_period_{0}; #endif std::optional output_power_{}; + std::atomic lock_initialized_{false}; bool teardown_started_{false}; bool teardown_complete_{false}; bool connected_{false}; diff --git a/esphome/components/openthread/openthread_esp.cpp b/esphome/components/openthread/openthread_esp.cpp index 9cc9223b52..27712bd86a 100644 --- a/esphome/components/openthread/openthread_esp.cpp +++ b/esphome/components/openthread/openthread_esp.cpp @@ -8,6 +8,7 @@ #include "esp_openthread_lock.h" #include "esp_task_wdt.h" +#include "esphome/core/hal.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" @@ -81,6 +82,9 @@ void OpenThreadComponent::ot_main() { // Initialize the OpenThread stack // otLoggingSetLevel(OT_LOG_LEVEL_DEBG); ESP_ERROR_CHECK(esp_openthread_init(&config)); + // Mark lock as initialized so InstanceLock callers know it's safe to acquire. + // Must be set after esp_openthread_init() which creates the internal semaphore. + this->lock_initialized_ = true; // Fetch OT instance once to avoid repeated call into OT stack otInstance *instance = esp_openthread_get_instance(); @@ -180,7 +184,8 @@ void OpenThreadComponent::ot_main() { esp_openthread_launch_mainloop(); - // Clean up + // Clean up - reset lock flag before deinit destroys the semaphore + this->lock_initialized_ = false; esp_openthread_deinit(); esp_openthread_netif_glue_deinit(); esp_netif_destroy(openthread_netif); @@ -210,6 +215,9 @@ network::IPAddresses OpenThreadComponent::get_ip_addresses() { otInstance *OpenThreadComponent::get_openthread_instance_() { return esp_openthread_get_instance(); } std::optional InstanceLock::try_acquire(int delay) { + if (!global_openthread_component->is_lock_initialized()) { + return {}; + } if (esp_openthread_lock_acquire(delay)) { return InstanceLock(); } @@ -217,6 +225,18 @@ std::optional InstanceLock::try_acquire(int delay) { } InstanceLock InstanceLock::acquire() { + // Wait for the lock to be created by ot_main() before attempting to acquire it. + // esp_openthread_lock_acquire() will assert-crash if called before esp_openthread_init(). + constexpr uint32_t lock_init_timeout_ms = 10000; + uint32_t start = millis(); + while (!global_openthread_component->is_lock_initialized()) { + if (millis() - start > lock_init_timeout_ms) { + ESP_LOGE(TAG, "OpenThread lock not initialized after %" PRIu32 "ms, aborting", lock_init_timeout_ms); + abort(); + } + delay(10); + esp_task_wdt_reset(); + } while (!esp_openthread_lock_acquire(100)) { esp_task_wdt_reset(); } From 5cbe9362569e17447c661e2cf5ad9a528b2a7ef7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 18 Mar 2026 23:43:50 -1000 Subject: [PATCH 050/166] Bump aioesphomeapi from 44.6.0 to 44.6.1 (#14954) 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 f8f60f1932..9e2f4efe3e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.19 esptool==5.2.0 click==8.3.1 esphome-dashboard==20260210.0 -aioesphomeapi==44.6.0 +aioesphomeapi==44.6.1 zeroconf==0.148.0 puremagic==1.30 ruamel.yaml==0.19.1 # dashboard_import From 08cab43548fb7546690cdabd07cda2f7a0c0e74a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 19 Mar 2026 08:44:35 -1000 Subject: [PATCH 051/166] [time] Fix lookup of top-level IANA timezone keys like UTC and GMT (#14952) --- esphome/components/time/__init__.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/esphome/components/time/__init__.py b/esphome/components/time/__init__.py index 7ffa408db9..9821046a73 100644 --- a/esphome/components/time/__init__.py +++ b/esphome/components/time/__init__.py @@ -59,15 +59,20 @@ _DST_RULE_TYPE_MAP = { def _load_tzdata(iana_key: str) -> bytes | None: # From https://tzdata.readthedocs.io/en/latest/#examples + if not iana_key: + return None try: package_loc, resource = iana_key.rsplit("/", 1) except ValueError: - return None - package = "tzdata.zoneinfo." + package_loc.replace("/", ".") + # Handle top-level timezone entries like "UTC", "GMT" + package = "tzdata.zoneinfo" + resource = iana_key + else: + package = "tzdata.zoneinfo." + package_loc.replace("/", ".") try: return (resources.files(package) / resource).read_bytes() - except (FileNotFoundError, ModuleNotFoundError): + except (FileNotFoundError, ModuleNotFoundError, IsADirectoryError): return None From daf3502e1527dde0a919f39898ca9cc61849dc8b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 19 Mar 2026 14:11:49 -1000 Subject: [PATCH 052/166] [logger] Fix ESP8266 crash with VERY_VERBOSE log level (#14980) --- esphome/components/logger/__init__.py | 29 ++++++++++++++++++--------- esphome/core/config.py | 4 +++- esphome/coroutine.py | 10 +++++++-- esphome/writer.py | 3 +++ 4 files changed, 34 insertions(+), 12 deletions(-) diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index 675f9a2ca4..a5601e6a8f 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -56,6 +56,7 @@ from esphome.const import ( PlatformFramework, ) from esphome.core import CORE, CoroPriority, Lambda, coroutine_with_priority +from esphome.types import ConfigType CODEOWNERS = ["@esphome/core"] logger_ns = cg.esphome_ns.namespace("logger") @@ -323,12 +324,11 @@ CONFIG_SCHEMA = cv.All( ) -@coroutine_with_priority(CoroPriority.DIAGNOSTICS) -async def to_code(config): - baud_rate = config[CONF_BAUD_RATE] +@coroutine_with_priority(CoroPriority.EARLY_INIT) +async def to_code(config: ConfigType) -> None: + baud_rate: int = config[CONF_BAUD_RATE] level = config[CONF_LEVEL] CORE.data.setdefault(CONF_LOGGER, {})[CONF_LEVEL] = level - initial_level = LOG_LEVELS[config.get(CONF_INITIAL_LEVEL, level)] tx_buffer_size = config[CONF_TX_BUFFER_SIZE] cg.add_define("ESPHOME_LOGGER_TX_BUFFER_SIZE", tx_buffer_size) log = cg.new_Pvariable( @@ -347,10 +347,23 @@ async def to_code(config): HARDWARE_UART_TO_UART_SELECTION[config[CONF_HARDWARE_UART]] ) ) - # pre_setup() must be called before init_log_buffer() because - # init_log_buffer() calls disable_loop() which may log at VV level, - # and global_logger must be set before any logging occurs. + # pre_setup() sets global_logger and must run before any other code + # that may call ESP_LOG* (e.g. setup_preferences contains ESP_LOGVV). cg.add(log.pre_setup()) + initial_level = LOG_LEVELS[config.get(CONF_INITIAL_LEVEL, level)] + cg.add(log.set_log_level(initial_level)) + + # Schedule the rest of logger setup at DIAGNOSTICS priority, after + # Application is constructed (CORE priority) but before most components. + CORE.add_job(_late_logger_init, config) + + +@coroutine_with_priority(CoroPriority.DIAGNOSTICS) +async def _late_logger_init(config: ConfigType) -> None: + """Finish logger setup after Application is constructed.""" + log = await cg.get_variable(config[CONF_ID]) + level = config[CONF_LEVEL] + baud_rate: int = config[CONF_BAUD_RATE] if CORE.is_esp32 or CORE.is_libretiny or CORE.is_nrf52: task_log_buffer_size = config[CONF_TASK_LOG_BUFFER_SIZE] if task_log_buffer_size > 0: @@ -363,8 +376,6 @@ async def to_code(config): cg.add_define("USE_ESPHOME_TASK_LOG_BUFFER") cg.add(log.init_log_buffer(64)) # Fixed 64 slots for host - cg.add(log.set_log_level(initial_level)) - # Enable runtime tag levels if logs are configured or explicitly enabled logs_config = config[CONF_LOGS] if logs_config or config[CONF_RUNTIME_TAG_LEVELS]: diff --git a/esphome/core/config.py b/esphome/core/config.py index d4a839cb79..bdfb1cc417 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -587,7 +587,9 @@ async def _add_looping_components() -> None: @coroutine_with_priority(CoroPriority.CORE) async def to_code(config: ConfigType) -> None: - cg.add_global(cg.global_ns.namespace("esphome").using) + # using namespace esphome is hardcoded in writer.py to guarantee it + # precedes all variable declarations regardless of coroutine priority. + # These can be used by user lambdas, put them to default scope cg.add_global(cg.RawExpression("using std::isnan")) cg.add_global(cg.RawExpression("using std::min")) diff --git a/esphome/coroutine.py b/esphome/coroutine.py index f5d512e510..3ce94cc979 100644 --- a/esphome/coroutine.py +++ b/esphome/coroutine.py @@ -63,7 +63,13 @@ class CoroPriority(enum.IntEnum): resolution during code generation. """ - # Platform initialization - must run first + # Early init - runs before platform init and before Application exists. + # Currently used only to connect logging so ESP_LOG* calls work + # immediately in all subsequent phases. + # Examples: logger (1100) + EARLY_INIT = 1100 + + # Platform initialization # Examples: esp32, esp8266, rp2040 PLATFORM = 1000 @@ -83,7 +89,7 @@ class CoroPriority(enum.IntEnum): CORE = 100 # Diagnostic and debugging systems - # Examples: logger (90) + # Examples: debug component (90) DIAGNOSTICS = 90 # Status and monitoring systems diff --git a/esphome/writer.py b/esphome/writer.py index fd4c811fb3..69a35d00e3 100644 --- a/esphome/writer.py +++ b/esphome/writer.py @@ -381,7 +381,10 @@ def write_cpp(code_s): code_format = CPP_BASE_FORMAT copy_src_tree() + # using namespace esphome must precede all variable declarations since + # codegen types assume this namespace is in scope (esphome_ns = global_ns). global_s = '#include "esphome.h"\n' + global_s += "using namespace esphome;\n" global_s += CORE.cpp_global_section full_file = f"{code_format[0] + CPP_INCLUDE_BEGIN}\n{global_s}{CPP_INCLUDE_END}" From 08187a01b1de0897a9e42f7bf9b4eb5e577c5275 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Fri, 20 Mar 2026 11:39:10 +1000 Subject: [PATCH 053/166] [sdl] Fix get_width()/height() when rotation used (#14950) --- esphome/components/sdl/sdl_esphome.cpp | 37 ++++++++++++++++++++++++++ esphome/components/sdl/sdl_esphome.h | 4 +-- 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/esphome/components/sdl/sdl_esphome.cpp b/esphome/components/sdl/sdl_esphome.cpp index f235e4e68c..74ca2ce39a 100644 --- a/esphome/components/sdl/sdl_esphome.cpp +++ b/esphome/components/sdl/sdl_esphome.cpp @@ -5,6 +5,30 @@ namespace esphome { namespace sdl { +int Sdl::get_width() { + switch (this->rotation_) { + case display::DISPLAY_ROTATION_90_DEGREES: + case display::DISPLAY_ROTATION_270_DEGREES: + return this->get_height_internal(); + case display::DISPLAY_ROTATION_0_DEGREES: + case display::DISPLAY_ROTATION_180_DEGREES: + default: + return this->get_width_internal(); + } +} + +int Sdl::get_height() { + switch (this->rotation_) { + case display::DISPLAY_ROTATION_0_DEGREES: + case display::DISPLAY_ROTATION_180_DEGREES: + return this->get_height_internal(); + case display::DISPLAY_ROTATION_90_DEGREES: + case display::DISPLAY_ROTATION_270_DEGREES: + default: + return this->get_width_internal(); + } +} + void Sdl::setup() { SDL_Init(SDL_INIT_VIDEO); this->window_ = SDL_CreateWindow(App.get_name().c_str(), this->pos_x_, this->pos_y_, this->width_, this->height_, @@ -49,6 +73,19 @@ void Sdl::draw_pixel_at(int x, int y, Color color) { if (!this->get_clipping().inside(x, y)) return; + if (this->rotation_ == display::DISPLAY_ROTATION_180_DEGREES) { + x = this->width_ - x - 1; + y = this->height_ - y - 1; + } else if (this->rotation_ == display::DISPLAY_ROTATION_90_DEGREES) { + auto tmp = x; + x = this->width_ - y - 1; + y = tmp; + } else if (this->rotation_ == display::DISPLAY_ROTATION_270_DEGREES) { + auto tmp = y; + y = this->height_ - x - 1; + x = tmp; + } + SDL_Rect rect{x, y, 1, 1}; auto data = (display::ColorUtil::color_to_565(color, display::COLOR_ORDER_RGB)); SDL_UpdateTexture(this->texture_, &rect, &data, 2); diff --git a/esphome/components/sdl/sdl_esphome.h b/esphome/components/sdl/sdl_esphome.h index bf5fde1428..968434a04f 100644 --- a/esphome/components/sdl/sdl_esphome.h +++ b/esphome/components/sdl/sdl_esphome.h @@ -33,8 +33,8 @@ class Sdl : public display::Display { this->pos_x_ = pos_x; this->pos_y_ = pos_y; } - int get_width() override { return this->width_; } - int get_height() override { return this->height_; } + int get_width() override; + int get_height() override; float get_setup_priority() const override { return setup_priority::HARDWARE; } void dump_config() override { LOG_DISPLAY("", "SDL", this); } void add_key_listener(int32_t keycode, std::function &&callback) { From d4f7cb984cdc2510a676f65bc5ca7f70f5eba903 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 19 Mar 2026 20:56:51 -1000 Subject: [PATCH 054/166] [uart] Fix UART0 default pin IOMUX loopback on ESP32 (#14978) --- .../uart/uart_component_esp_idf.cpp | 33 ++++++++++++++++--- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/esphome/components/uart/uart_component_esp_idf.cpp b/esphome/components/uart/uart_component_esp_idf.cpp index 47ddf1a38d..8168e49805 100644 --- a/esphome/components/uart/uart_component_esp_idf.cpp +++ b/esphome/components/uart/uart_component_esp_idf.cpp @@ -7,7 +7,9 @@ #include "esphome/core/log.h" #include "esphome/core/gpio.h" #include "driver/gpio.h" +#include "esp_private/gpio.h" #include "soc/gpio_num.h" +#include "soc/uart_pins.h" #ifdef USE_UART_WAKE_LOOP_ON_RX #include "esphome/core/application.h" @@ -21,6 +23,20 @@ namespace esphome::uart { static const char *const TAG = "uart.idf"; +/// Check if a pin number matches one of the default UART0 GPIO pins. +/// These pins may have residual IOMUX state from the ROM bootloader that +/// must be cleared before UART reconfiguration. +/// +/// ESP-IDF's uart_set_pin() has an asymmetry: when routing TX via GPIO matrix, +/// it calls gpio_func_sel(PIN_FUNC_GPIO) to clear IOMUX, but for RX it only +/// calls gpio_input_enable() which does NOT clear the IOMUX function select. +/// If a default UART0 TX pin (configured as TX via IOMUX during boot) is later +/// reassigned as RX via GPIO matrix, the old IOMUX TX function remains active, +/// causing TX data to loop back into RX on the same pin. +static constexpr bool is_default_uart0_pin(int8_t pin_num) { + return pin_num == U0TXD_GPIO_NUM || pin_num == U0RXD_GPIO_NUM; +} + uart_config_t IDFUARTComponent::get_config_() { uart_parity_t parity = UART_PARITY_DISABLE; if (this->parity_ == UART_CONFIG_PARITY_EVEN) { @@ -131,6 +147,19 @@ void IDFUARTComponent::load_settings(bool dump_config) { return; } + int8_t tx = this->tx_pin_ != nullptr ? this->tx_pin_->get_pin() : -1; + int8_t rx = this->rx_pin_ != nullptr ? this->rx_pin_->get_pin() : -1; + int8_t flow_control = this->flow_control_pin_ != nullptr ? this->flow_control_pin_->get_pin() : -1; + + // Clear residual IOMUX function on UART0 default pins left by the ROM bootloader. + // See is_default_uart0_pin() comment for details on the ESP-IDF uart_set_pin() bug. + if (is_default_uart0_pin(tx)) { + gpio_func_sel(static_cast(tx), PIN_FUNC_GPIO); + } + if (is_default_uart0_pin(rx)) { + gpio_func_sel(static_cast(rx), PIN_FUNC_GPIO); + } + auto setup_pin_if_needed = [](InternalGPIOPin *pin) { if (!pin) { return; @@ -146,10 +175,6 @@ void IDFUARTComponent::load_settings(bool dump_config) { setup_pin_if_needed(this->tx_pin_); } - int8_t tx = this->tx_pin_ != nullptr ? this->tx_pin_->get_pin() : -1; - int8_t rx = this->rx_pin_ != nullptr ? this->rx_pin_->get_pin() : -1; - int8_t flow_control = this->flow_control_pin_ != nullptr ? this->flow_control_pin_->get_pin() : -1; - uint32_t invert = 0; if (this->tx_pin_ != nullptr && this->tx_pin_->is_inverted()) { invert |= UART_SIGNAL_TXD_INV; From 0297260a5773d38a2a65ade6cf35333f9aff09f4 Mon Sep 17 00:00:00 2001 From: Keith Roehrenbeck Date: Fri, 20 Mar 2026 15:11:57 -0500 Subject: [PATCH 055/166] [ld2450] Fix zone target counts including untracked ghost targets (#15026) --- esphome/components/ld2450/ld2450.cpp | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/esphome/components/ld2450/ld2450.cpp b/esphome/components/ld2450/ld2450.cpp index f9701cbdf6..1946831ca8 100644 --- a/esphome/components/ld2450/ld2450.cpp +++ b/esphome/components/ld2450/ld2450.cpp @@ -534,10 +534,11 @@ void LD2450Component::handle_periodic_data_() { } #endif - // Store target info for zone target count - this->target_info_[index].x = tx; - this->target_info_[index].y = ty; - this->target_info_[index].is_moving = is_moving; + // Store target info for zone target count. Zero out untracked targets (td==0) + // so stale coordinates don't produce ghost counts in count_targets_in_zone_(). + this->target_info_[index].x = (td > 0) ? tx : 0; + this->target_info_[index].y = (td > 0) ? ty : 0; + this->target_info_[index].is_moving = (td > 0) && is_moving; } // End loop thru targets From 28e8250b69e7ae9662e166a6bcf69085f5e3d48f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 20 Mar 2026 11:10:40 -1000 Subject: [PATCH 056/166] Bump aioesphomeapi from 44.6.1 to 44.6.2 (#15027) 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 9e2f4efe3e..10e56c3b49 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.19 esptool==5.2.0 click==8.3.1 esphome-dashboard==20260210.0 -aioesphomeapi==44.6.1 +aioesphomeapi==44.6.2 zeroconf==0.148.0 puremagic==1.30 ruamel.yaml==0.19.1 # dashboard_import From 12eed0d38407c77c70179b2dcbb9c969c06a79c3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 20 Mar 2026 12:06:23 -1000 Subject: [PATCH 057/166] [api] Increase noise handshake timeout to 60s for slow WiFi environments (#15022) --- esphome/components/api/api_connection.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index dea3ba5460..d97ef97762 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -64,7 +64,11 @@ static constexpr uint32_t KEEPALIVE_DISCONNECT_TIMEOUT = (KEEPALIVE_TIMEOUT_MS * // A stalled handshake from a buggy client or network glitch holds a connection // slot, which can prevent legitimate clients from reconnecting. Also hardens // against the less likely case of intentional connection slot exhaustion. -static constexpr uint32_t HANDSHAKE_TIMEOUT_MS = 15000; +// +// 60s is intentionally high: on ESP8266 with power_save_mode: LIGHT and weak +// WiFi (-70 dBm+), TCP retransmissions push real-world handshake times to +// 28-30s. See https://github.com/esphome/esphome/issues/14999 +static constexpr uint32_t HANDSHAKE_TIMEOUT_MS = 60000; static constexpr auto ESPHOME_VERSION_REF = StringRef::from_lit(ESPHOME_VERSION); From 3fe84eadef1e5448f6bae7c5f88ac74154e9f6d1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 20 Mar 2026 12:13:49 -1000 Subject: [PATCH 058/166] [wifi] Fix ESP8266 power_save_mode mapping (LIGHT/HIGH were swapped) (#15029) --- esphome/components/wifi/wifi_component_esp8266.cpp | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index 0bf7934878..5514f1c6be 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -92,13 +92,23 @@ bool WiFiComponent::wifi_mode_(optional sta, optional ap) { return ret; } bool WiFiComponent::wifi_apply_power_save_() { + // ESP8266 sleep types have confusing names — LIGHT_SLEEP_T is the MORE aggressive mode. + // SDK enum: NONE_SLEEP_T=0, LIGHT_SLEEP_T=1, MODEM_SLEEP_T=2 + // https://github.com/esp8266/Arduino/blob/3.1.2/tools/sdk/include/user_interface.h#L447-L451 + // Arduino ESP32 compat confirms: WIFI_PS_MIN_MODEM=MODEM_SLEEP, WIFI_PS_MAX_MODEM=LIGHT_SLEEP + // https://github.com/esp8266/Arduino/blob/3.1.2/libraries/ESP8266WiFi/src/ESP8266WiFiType.h#L53-L55 sleep_type_t power_save; switch (this->power_save_) { case WIFI_POWER_SAVE_LIGHT: - power_save = LIGHT_SLEEP_T; + // MODEM_SLEEP_T: only the WiFi modem sleeps between DTIM beacons, CPU stays active. + // Matches ESP32's WIFI_PS_MIN_MODEM. + power_save = MODEM_SLEEP_T; break; case WIFI_POWER_SAVE_HIGH: - power_save = MODEM_SLEEP_T; + // LIGHT_SLEEP_T: both WiFi modem AND CPU suspend between DTIM beacons. + // Most aggressive — prevents TCP processing during sleep. Matches ESP32's WIFI_PS_MAX_MODEM. + // See https://github.com/esphome/esphome/issues/14999 + power_save = LIGHT_SLEEP_T; break; case WIFI_POWER_SAVE_NONE: default: From 0bf6e1e8398a3793d5b28b50de20459ec183c132 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 20 Mar 2026 19:53:02 -0400 Subject: [PATCH 059/166] [esp32_touch] Fix initial state never published when sensor untouched (#15032) --- esphome/components/esp32_touch/esp32_touch.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/esphome/components/esp32_touch/esp32_touch.cpp b/esphome/components/esp32_touch/esp32_touch.cpp index e7124ce92f..0d331b29d6 100644 --- a/esphome/components/esp32_touch/esp32_touch.cpp +++ b/esphome/components/esp32_touch/esp32_touch.cpp @@ -360,11 +360,16 @@ void ESP32TouchComponent::loop() { } // Publish initial OFF state for sensors that haven't received events yet + bool all_initial_published = true; for (auto *child : this->children_) { this->publish_initial_state_if_needed_(child, now); + if (!child->initial_state_published_) { + all_initial_published = false; + } } - if (!this->setup_mode_) { + // Only disable loop once all initial states are published + if (!this->setup_mode_ && all_initial_published) { this->disable_loop(); } } From 98b4e1ea15c8badd9e701e225dc59a5de12a3824 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 20 Mar 2026 13:57:51 -1000 Subject: [PATCH 060/166] [web_server] Increase httpd task stack size to prevent stack overflow (#14997) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/web_server_idf/web_server_idf.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/esphome/components/web_server_idf/web_server_idf.cpp b/esphome/components/web_server_idf/web_server_idf.cpp index f18570965b..45ee711b1e 100644 --- a/esphome/components/web_server_idf/web_server_idf.cpp +++ b/esphome/components/web_server_idf/web_server_idf.cpp @@ -120,7 +120,10 @@ void AsyncWebServer::begin() { if (this->server_) { this->end(); } + // Default httpd stack is defined by ESP-IDF. Increase to accommodate SerializationBuffer's + // 640-byte stack buffer used by web_server JSON request handlers. httpd_config_t config = HTTPD_DEFAULT_CONFIG(); + config.stack_size = config.stack_size + 256; config.server_port = this->port_; config.uri_match_fn = [](const char * /*unused*/, const char * /*unused*/, size_t /*unused*/) { return true; }; // Always enable LRU purging to handle socket exhaustion gracefully. From 06cc5a29a733c1078ead4bdf7f6dfc46c5bbe57d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 20 Mar 2026 13:58:02 -1000 Subject: [PATCH 061/166] [core] Add copy() method to StringRef for std::string compatibility (#15028) --- esphome/core/string_ref.h | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/esphome/core/string_ref.h b/esphome/core/string_ref.h index 6047202753..34ba2474b2 100644 --- a/esphome/core/string_ref.h +++ b/esphome/core/string_ref.h @@ -76,6 +76,15 @@ class StringRef { constexpr bool empty() const { return len_ == 0; } constexpr const_reference operator[](size_type pos) const { return *(base_ + pos); } + /// Copy characters to destination buffer (std::string::copy-like, but returns 0 instead of throwing on out-of-range) + size_type copy(char *dest, size_type count, size_type pos = 0) const { + if (pos >= len_) + return 0; + size_type actual = (count > len_ - pos) ? len_ - pos : count; + std::memcpy(dest, base_ + pos, actual); + return actual; + } + std::string str() const { return std::string(base_, len_); } const uint8_t *byte() const { return reinterpret_cast(base_); } From 42da28185456200efbf886da2e4cdad2e670bc97 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 20 Mar 2026 13:58:14 -1000 Subject: [PATCH 062/166] [time] Fix timezone_offset() and recalc_timestamp_local() always returning UTC (#14996) --- esphome/core/time.cpp | 16 ++++++---------- esphome/core/time.h | 2 ++ 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/esphome/core/time.cpp b/esphome/core/time.cpp index 73ba0a9be7..6add82e7d1 100644 --- a/esphome/core/time.cpp +++ b/esphome/core/time.cpp @@ -283,19 +283,15 @@ void ESPTime::recalc_timestamp_local() { bool dst_valid = time::is_in_dst(utc_if_dst, tz); bool std_valid = !time::is_in_dst(utc_if_std, tz); - if (dst_valid && std_valid) { - // Ambiguous time (repeated hour during fall-back) - prefer standard time - this->timestamp = utc_if_std; - } else if (dst_valid) { + if (dst_valid && !std_valid) { // Only DST interpretation is valid this->timestamp = utc_if_dst; - } else if (std_valid) { - // Only standard interpretation is valid - this->timestamp = utc_if_std; } else { - // Invalid time (skipped hour during spring-forward) - // libc normalizes forward: 02:30 CST -> 08:30 UTC -> 03:30 CDT - // Using std offset achieves this since the UTC result falls during DST + // All other cases use standard offset: + // - Both valid (ambiguous fall-back repeated hour): prefer standard time + // - Only standard valid: straightforward + // - Neither valid (spring-forward skipped hour): std offset normalizes + // forward to match libc mktime(), e.g. 02:30 CST -> 03:30 CDT this->timestamp = utc_if_std; } #else diff --git a/esphome/core/time.h b/esphome/core/time.h index 874f0db4b4..1716c51ffd 100644 --- a/esphome/core/time.h +++ b/esphome/core/time.h @@ -1,5 +1,7 @@ #pragma once +#include "esphome/core/defines.h" + #include #include #include From 77264de3f6a17c2f5f0f1bb62bee0f75febab28f Mon Sep 17 00:00:00 2001 From: Samuel Sieb Date: Sat, 21 Mar 2026 11:13:08 -0700 Subject: [PATCH 063/166] [analog_threshhold] add missing header (#15058) --- .../components/analog_threshold/analog_threshold_binary_sensor.h | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/components/analog_threshold/analog_threshold_binary_sensor.h b/esphome/components/analog_threshold/analog_threshold_binary_sensor.h index 9ea95d8570..dd70768105 100644 --- a/esphome/components/analog_threshold/analog_threshold_binary_sensor.h +++ b/esphome/components/analog_threshold/analog_threshold_binary_sensor.h @@ -1,5 +1,6 @@ #pragma once +#include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/components/binary_sensor/binary_sensor.h" #include "esphome/components/sensor/sensor.h" From 2352c732de79b76bb277dc6e1927f73105de12ba Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 21 Mar 2026 15:33:42 -1000 Subject: [PATCH 064/166] [mqtt] Rate-limit component resends to prevent task WDT on reconnect (#15061) --- esphome/components/mqtt/mqtt_client.cpp | 17 ++++++++++++++--- esphome/components/mqtt/mqtt_component.h | 3 +++ 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/esphome/components/mqtt/mqtt_client.cpp b/esphome/components/mqtt/mqtt_client.cpp index 38daf8f8f6..ab665e2579 100644 --- a/esphome/components/mqtt/mqtt_client.cpp +++ b/esphome/components/mqtt/mqtt_client.cpp @@ -28,6 +28,10 @@ namespace esphome::mqtt { static const char *const TAG = "mqtt"; +// Maximum number of MQTT component resends per loop iteration. +// Limits work to avoid triggering the task watchdog on reconnect. +static constexpr uint8_t MAX_RESENDS_PER_LOOP = 8; + // Disconnect reason strings indexed by MQTTClientDisconnectReason enum (0-8) PROGMEM_STRING_TABLE(MQTTDisconnectReasonStrings, "TCP disconnected", "Unacceptable Protocol Version", "Identifier Rejected", "Server Unavailable", "Malformed Credentials", "Not Authorized", @@ -396,9 +400,16 @@ void MQTTClientComponent::loop() { this->resubscribe_subscriptions_(); // Process pending resends for all MQTT components centrally - // This is more efficient than each component polling in its own loop - for (MQTTComponent *component : this->children_) { - component->process_resend(); + // Limit work per loop iteration to avoid triggering task WDT on reconnect + { + uint8_t resend_count = 0; + for (MQTTComponent *component : this->children_) { + if (component->is_resend_pending()) { + component->process_resend(); + if (++resend_count >= MAX_RESENDS_PER_LOOP) + break; + } + } } } break; diff --git a/esphome/components/mqtt/mqtt_component.h b/esphome/components/mqtt/mqtt_component.h index 2403ef64ea..7983e04870 100644 --- a/esphome/components/mqtt/mqtt_component.h +++ b/esphome/components/mqtt/mqtt_component.h @@ -147,6 +147,9 @@ class MQTTComponent : public Component { /// Internal method for the MQTT client base to schedule a resend of the state on reconnect. void schedule_resend_state(); + /// Check if a resend is pending (called by MQTTClientComponent to rate-limit work) + bool is_resend_pending() const { return this->resend_state_; } + /// Process pending resend if needed (called by MQTTClientComponent) void process_resend(); From b5880df93c6830003b46d106181a26516a4fac07 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Mar 2026 10:14:52 -1000 Subject: [PATCH 065/166] [light] Fix constant_brightness broken by gamma LUT refactor (#15048) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/light/light_color_values.h | 10 + esphome/components/light/light_state.cpp | 47 ++++- .../fixtures/light_constant_brightness.yaml | 57 ++++++ .../test_light_constant_brightness.py | 188 ++++++++++++++++++ 4 files changed, 296 insertions(+), 6 deletions(-) create mode 100644 tests/integration/fixtures/light_constant_brightness.yaml create mode 100644 tests/integration/test_light_constant_brightness.py diff --git a/esphome/components/light/light_color_values.h b/esphome/components/light/light_color_values.h index 3a9ca8c8c2..a2c2dbca46 100644 --- a/esphome/components/light/light_color_values.h +++ b/esphome/components/light/light_color_values.h @@ -154,6 +154,16 @@ class LightColorValues { } /// Convert these light color values to an CWWW representation with the given parameters. + /// + /// Note on gamma and constant_brightness: This method operates on the raw/internal channel + /// values stored in this object. For cold_white_ and warm_white_ specifically, these + /// may already be gamma-uncorrected when derived from a color_temperature value. + /// For constant_brightness=false, additional gamma for the output can be applied after + /// this method since gamma commutes with simple multiplication. For constant_brightness=true, + /// the caller (LightState::current_values_as_cwww) must apply gamma to the individual + /// channel values BEFORE the balancing formula, because the nonlinear max/sum ratio does + /// not commute with gamma. See LightState::current_values_as_cwww() for the correct + /// implementation. void as_cwww(float *cold_white, float *warm_white, bool constant_brightness = false) const { if (this->color_mode_ & ColorCapability::COLD_WARM_WHITE) { const float cw_level = this->cold_white_; diff --git a/esphome/components/light/light_state.cpp b/esphome/components/light/light_state.cpp index 161092532a..1b736d84f6 100644 --- a/esphome/components/light/light_state.cpp +++ b/esphome/components/light/light_state.cpp @@ -223,12 +223,11 @@ void LightState::current_values_as_rgbw(float *red, float *green, float *blue, f } void LightState::current_values_as_rgbww(float *red, float *green, float *blue, float *cold_white, float *warm_white, bool constant_brightness) { - this->current_values.as_rgbww(red, green, blue, cold_white, warm_white, constant_brightness); + this->current_values.as_rgb(red, green, blue); *red = this->gamma_correct_lut(*red); *green = this->gamma_correct_lut(*green); *blue = this->gamma_correct_lut(*blue); - *cold_white = this->gamma_correct_lut(*cold_white); - *warm_white = this->gamma_correct_lut(*warm_white); + this->current_values_as_cwww(cold_white, warm_white, constant_brightness); } void LightState::current_values_as_rgbct(float *red, float *green, float *blue, float *color_temperature, float *white_brightness) { @@ -241,9 +240,45 @@ void LightState::current_values_as_rgbct(float *red, float *green, float *blue, *white_brightness = this->gamma_correct_lut(*white_brightness); } void LightState::current_values_as_cwww(float *cold_white, float *warm_white, bool constant_brightness) { - this->current_values.as_cwww(cold_white, warm_white, constant_brightness); - *cold_white = this->gamma_correct_lut(*cold_white); - *warm_white = this->gamma_correct_lut(*warm_white); + if (!constant_brightness) { + // Without constant_brightness, gamma commutes with simple multiplication: + // gamma(white_level * cw) = gamma(white_level) * gamma(cw) + // (since gamma(a*b) = (a*b)^g = a^g * b^g = gamma(a) * gamma(b)) + // so applying gamma after is mathematically equivalent and simpler. + this->current_values.as_cwww(cold_white, warm_white, false); + *cold_white = this->gamma_correct_lut(*cold_white); + *warm_white = this->gamma_correct_lut(*warm_white); + return; + } + + // For constant_brightness mode, gamma MUST be applied to the individual + // channel values BEFORE the balancing formula (max/sum ratio), not after. + // + // Why: The cold_white_ and warm_white_ values stored in LightColorValues + // are gamma-uncorrected (see transform_parameters_() which applies + // gamma_uncorrect to the linear CW/WW fractions derived from color + // temperature). Applying gamma_correct here recovers the original linear + // fractions, which the constant_brightness formula then uses to distribute + // power evenly. The max/sum formula ensures cold+warm PWM output sums to + // a constant, keeping total power (and perceived brightness) the same + // across all color temperatures. + // + // Applying gamma AFTER the formula would be incorrect because gamma is + // nonlinear: gamma(a/b) != gamma(a)/gamma(b), so the carefully balanced + // ratio would be distorted, causing a severe brightness dip at mid-range + // color temperatures. + const auto &v = this->current_values; + if (!(v.get_color_mode() & ColorCapability::COLD_WARM_WHITE)) { + *cold_white = *warm_white = 0; + return; + } + + const float cw_level = this->gamma_correct_lut(v.get_cold_white()); + const float ww_level = this->gamma_correct_lut(v.get_warm_white()); + const float white_level = this->gamma_correct_lut(v.get_state() * v.get_brightness()); + const float sum = cw_level > 0 || ww_level > 0 ? cw_level + ww_level : 1; // Don't divide by zero. + *cold_white = white_level * std::max(cw_level, ww_level) * cw_level / sum; + *warm_white = white_level * std::max(cw_level, ww_level) * ww_level / sum; } void LightState::current_values_as_ct(float *color_temperature, float *white_brightness) { auto traits = this->get_traits(); diff --git a/tests/integration/fixtures/light_constant_brightness.yaml b/tests/integration/fixtures/light_constant_brightness.yaml new file mode 100644 index 0000000000..4357a16d58 --- /dev/null +++ b/tests/integration/fixtures/light_constant_brightness.yaml @@ -0,0 +1,57 @@ +esphome: + name: light-cb-test +host: +api: # Port will be automatically injected +logger: + level: DEBUG + +output: + - platform: template + id: cb_cold_white_output + type: float + write_action: + - logger.log: + format: "CB_CW_OUTPUT:%.6f" + args: [state] + - platform: template + id: cb_warm_white_output + type: float + write_action: + - logger.log: + format: "CB_WW_OUTPUT:%.6f" + args: [state] + - platform: template + id: ncb_cold_white_output + type: float + write_action: + - logger.log: + format: "NCB_CW_OUTPUT:%.6f" + args: [state] + - platform: template + id: ncb_warm_white_output + type: float + write_action: + - logger.log: + format: "NCB_WW_OUTPUT:%.6f" + args: [state] + +light: + - platform: cwww + name: "Test CB Light" + id: test_cb_light + cold_white: cb_cold_white_output + warm_white: cb_warm_white_output + cold_white_color_temperature: 6536 K + warm_white_color_temperature: 2000 K + constant_brightness: true + gamma_correct: 2.8 + + - platform: cwww + name: "Test NCB Light" + id: test_ncb_light + cold_white: ncb_cold_white_output + warm_white: ncb_warm_white_output + cold_white_color_temperature: 6536 K + warm_white_color_temperature: 2000 K + constant_brightness: false + gamma_correct: 2.8 diff --git a/tests/integration/test_light_constant_brightness.py b/tests/integration/test_light_constant_brightness.py new file mode 100644 index 0000000000..622dc0e065 --- /dev/null +++ b/tests/integration/test_light_constant_brightness.py @@ -0,0 +1,188 @@ +"""Integration test for constant_brightness with gamma correction. + +Tests both constant_brightness: true and false cwww lights with gamma +correction in a single compilation to verify: +- constant_brightness: true maintains constant total CW+WW power output +- constant_brightness: false correctly varies total power across color temps + +This is a regression test for https://github.com/esphome/esphome/issues/15040 +where the gamma LUT refactor (#14123) broke constant_brightness by applying +gamma after the balancing formula instead of before it. +""" + +from __future__ import annotations + +import asyncio +import re +from typing import Any + +from aioesphomeapi import EntityState, LightInfo, LightState +import pytest + +from .state_utils import InitialStateHelper +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_light_constant_brightness( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test constant_brightness true and false behavior with gamma correction.""" + # Track output values for both lights from log lines + cb_cw_pattern = re.compile(r"(? None: + for pattern, key in [ + (cb_cw_pattern, "cb_cw"), + (cb_ww_pattern, "cb_ww"), + (ncb_cw_pattern, "ncb_cw"), + (ncb_ww_pattern, "ncb_ww"), + ]: + match = pattern.search(line) + if match: + latest[key] = float(match.group(1)) + + loop = asyncio.get_running_loop() + + async with ( + run_compiled(yaml_config, line_callback=on_log_line), + api_client_connected() as client, + ): + entities, _ = await client.list_entities_services() + lights = [e for e in entities if isinstance(e, LightInfo)] + cb_light = next(e for e in lights if e.object_id.endswith("cb_light")) + ncb_light = next(e for e in lights if e.object_id.endswith("ncb_light")) + + # Use InitialStateHelper to wait for initial state broadcast + initial_state_helper = InitialStateHelper(entities) + + # Track state changes per light key + state_futures: dict[int, asyncio.Future[EntityState]] = {} + + def on_state(state: EntityState) -> None: + if isinstance(state, LightState) and state.key in state_futures: + future = state_futures[state.key] + if not future.done(): + future.set_result(state) + + client.subscribe_states(initial_state_helper.on_state_wrapper(on_state)) + + try: + await initial_state_helper.wait_for_initial_states() + except TimeoutError: + pytest.fail("Timeout waiting for initial states") + + async def send_and_wait( + light_key: int, timeout: float = 5.0, **kwargs: Any + ) -> LightState: + """Send a light command and wait for the state response.""" + state_futures[light_key] = loop.create_future() + client.light_command(key=light_key, **kwargs) + try: + return await asyncio.wait_for(state_futures[light_key], timeout=timeout) + except TimeoutError: + pytest.fail(f"Timeout waiting for light state after command: {kwargs}") + + # --- Test constant_brightness: true --- + + # Turn on CB light at full brightness + await send_and_wait( + cb_light.key, + state=True, + brightness=1.0, + color_temperature=153.0, + transition_length=0, + ) + + test_mireds = [ + 153.0, # Pure cold white + 200.0, # Mostly cold + 280.0, # Mixed + 326.5, # Midpoint + 400.0, # Mostly warm + 500.0, # Pure warm white + ] + + cb_totals: list[tuple[float, float, float]] = [] + for mireds in test_mireds: + await send_and_wait( + cb_light.key, color_temperature=mireds, transition_length=0 + ) + cb_totals.append((mireds, latest["cb_cw"], latest["cb_ww"])) + + # All totals should be approximately equal (constant brightness) + reference_total = next((cw + ww for _, cw, ww in cb_totals if cw + ww > 0), 0) + assert reference_total > 0, ( + f"Reference total power is zero, CB light outputs not working. " + f"Values: {cb_totals}" + ) + + for mireds, cw, ww in cb_totals: + total = cw + ww + assert total == pytest.approx(reference_total, rel=0.05), ( + f"constant_brightness: Total power at {mireds} mireds " + f"({total:.4f}) differs from reference ({reference_total:.4f}) " + f"by more than 5%. CW={cw:.4f}, WW={ww:.4f}. " + f"All values: {cb_totals}" + ) + + # --- Test constant_brightness: false --- + + # Turn on NCB light at full brightness + await send_and_wait( + ncb_light.key, + state=True, + brightness=1.0, + color_temperature=153.0, + transition_length=0, + ) + + ncb_totals: list[tuple[float, float, float]] = [] + for mireds in test_mireds: + await send_and_wait( + ncb_light.key, color_temperature=mireds, transition_length=0 + ) + ncb_totals.append((mireds, latest["ncb_cw"], latest["ncb_ww"])) + + extreme_cw = ncb_totals[0] # 153 mireds - pure cold + extreme_ww = ncb_totals[-1] # 500 mireds - pure warm + midpoint = ncb_totals[3] # 326.5 mireds - midpoint + + # At pure cold white, WW should be ~0 + assert extreme_cw[2] == pytest.approx(0.0, abs=0.01), ( + f"Pure cold white should have WW~0, got WW={extreme_cw[2]:.4f}" + ) + # At pure warm white, CW should be ~0 + assert extreme_ww[1] == pytest.approx(0.0, abs=0.01), ( + f"Pure warm white should have CW~0, got CW={extreme_ww[1]:.4f}" + ) + + # At midpoint, both channels should be non-zero + assert midpoint[1] > 0.05, f"Midpoint CW should be >0.05, got {midpoint[1]:.4f}" + assert midpoint[2] > 0.05, f"Midpoint WW should be >0.05, got {midpoint[2]:.4f}" + + # Total power at midpoint should be higher than at the extremes + midpoint_total = midpoint[1] + midpoint[2] + extreme_cw_total = extreme_cw[1] + extreme_cw[2] + extreme_ww_total = extreme_ww[1] + extreme_ww[2] + + assert midpoint_total > extreme_cw_total, ( + f"Midpoint total ({midpoint_total:.4f}) should be > pure CW total " + f"({extreme_cw_total:.4f}). All values: {ncb_totals}" + ) + assert midpoint_total > extreme_ww_total, ( + f"Midpoint total ({midpoint_total:.4f}) should be > pure WW total " + f"({extreme_ww_total:.4f}). All values: {ncb_totals}" + ) From 9abc112f7606870ae868954f07547769e0040b83 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Mar 2026 10:16:46 -1000 Subject: [PATCH 066/166] [sht4x] Fix heater causing measurement jitter (#15030) --- esphome/components/sht4x/sht4x.cpp | 40 ++++++++++++++++-------------- esphome/components/sht4x/sht4x.h | 3 ++- tests/components/sht4x/common.yaml | 4 +++ 3 files changed, 28 insertions(+), 19 deletions(-) diff --git a/esphome/components/sht4x/sht4x.cpp b/esphome/components/sht4x/sht4x.cpp index 42be326202..43c2436a56 100644 --- a/esphome/components/sht4x/sht4x.cpp +++ b/esphome/components/sht4x/sht4x.cpp @@ -9,14 +9,12 @@ static const char *const TAG = "sht4x"; static const uint8_t MEASURECOMMANDS[] = {0xFD, 0xF6, 0xE0}; static const uint8_t SERIAL_NUMBER_COMMAND = 0x89; -void SHT4XComponent::start_heater_() { - uint8_t cmd[] = {this->heater_command_}; - - ESP_LOGD(TAG, "Heater turning on"); - if (this->write(cmd, 1) != i2c::ERROR_OK) { - this->status_set_error(LOG_STR("Failed to turn on heater")); - } -} +// Conversion constants from SHT4x datasheet +static constexpr float TEMPERATURE_OFFSET = -45.0f; +static constexpr float TEMPERATURE_SPAN = 175.0f; +static constexpr float HUMIDITY_OFFSET = -6.0f; +static constexpr float HUMIDITY_SPAN = 125.0f; +static constexpr float RAW_MAX = 65535.0f; void SHT4XComponent::read_serial_number_() { uint16_t buffer[2]; @@ -39,8 +37,8 @@ void SHT4XComponent::setup() { this->read_serial_number_(); if (std::isfinite(this->duty_cycle_) && this->duty_cycle_ > 0.0f) { - uint32_t heater_interval = static_cast(static_cast(this->heater_time_) / this->duty_cycle_); - ESP_LOGD(TAG, "Heater interval: %" PRIu32, heater_interval); + this->heater_interval_ = static_cast(static_cast(this->heater_time_) / this->duty_cycle_); + ESP_LOGD(TAG, "Heater interval: %" PRIu32, this->heater_interval_); if (this->heater_power_ == SHT4X_HEATERPOWER_HIGH) { if (this->heater_time_ == SHT4X_HEATERTIME_LONG) { @@ -62,8 +60,6 @@ void SHT4XComponent::setup() { } } ESP_LOGD(TAG, "Heater command: %x", this->heater_command_); - - this->set_interval(heater_interval, std::bind(&SHT4XComponent::start_heater_, this)); } } @@ -106,19 +102,27 @@ void SHT4XComponent::update() { // Evaluate and publish measurements if (this->temp_sensor_ != nullptr) { // Temp is contained in the first result word - float sensor_value_temp = buffer[0]; - float temp = -45 + 175 * sensor_value_temp / 65535; - + float temp = TEMPERATURE_OFFSET + TEMPERATURE_SPAN * static_cast(buffer[0]) / RAW_MAX; this->temp_sensor_->publish_state(temp); } if (this->humidity_sensor_ != nullptr) { // Relative humidity is in the second result word - float sensor_value_rh = buffer[1]; - float rh = -6 + 125 * sensor_value_rh / 65535; - + float rh = HUMIDITY_OFFSET + HUMIDITY_SPAN * static_cast(buffer[1]) / RAW_MAX; this->humidity_sensor_->publish_state(rh); } + + // Fire heater after measurement to maximize cooldown time before the next reading. + // The heater command produces a measurement that we don't need (datasheet 4.9). + if (this->heater_interval_ > 0) { + uint32_t now = millis(); + if (now - this->last_heater_millis_ >= this->heater_interval_) { + ESP_LOGD(TAG, "Heater turning on"); + if (this->write_command(this->heater_command_)) { + this->last_heater_millis_ = now; + } + } + } }); } diff --git a/esphome/components/sht4x/sht4x.h b/esphome/components/sht4x/sht4x.h index aec0f3d7f8..51f473fe3f 100644 --- a/esphome/components/sht4x/sht4x.h +++ b/esphome/components/sht4x/sht4x.h @@ -35,9 +35,10 @@ class SHT4XComponent : public PollingComponent, public sensirion_common::Sensiri SHT4XHEATERTIME heater_time_; float duty_cycle_; - void start_heater_(); void read_serial_number_(); uint8_t heater_command_; + uint32_t heater_interval_{0}; + uint32_t last_heater_millis_{0}; uint32_t serial_number_; sensor::Sensor *temp_sensor_{nullptr}; diff --git a/tests/components/sht4x/common.yaml b/tests/components/sht4x/common.yaml index 50d5ad8ca4..bec192d6db 100644 --- a/tests/components/sht4x/common.yaml +++ b/tests/components/sht4x/common.yaml @@ -6,4 +6,8 @@ sensor: humidity: name: SHT4X Humidity address: 0x44 + precision: High + heater_max_duty: 0.02 + heater_power: High + heater_time: Long update_interval: 15s From 67ab2e143c8c549bcdbc1fe0f05c2e488075b661 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Mar 2026 10:17:59 -1000 Subject: [PATCH 067/166] [uart] Fix RTL87xx compilation failure due to SUCCESS macro collision (#15054) --- esphome/components/uart/uart_component.h | 5 +++++ tests/components/uart/test.rtl87xx-ard.yaml | 14 ++++++++++++++ 2 files changed, 19 insertions(+) create mode 100644 tests/components/uart/test.rtl87xx-ard.yaml diff --git a/esphome/components/uart/uart_component.h b/esphome/components/uart/uart_component.h index 853de719fe..7b61501d44 100644 --- a/esphome/components/uart/uart_component.h +++ b/esphome/components/uart/uart_component.h @@ -30,12 +30,17 @@ enum UARTDirection { const LogString *parity_to_str(UARTParityOptions parity); /// Result of a flush() call. +// Some vendor SDKs (e.g., Realtek) define SUCCESS as a macro. +// Save and restore around the enum to avoid collisions with our scoped enum value. +#pragma push_macro("SUCCESS") +#undef SUCCESS enum class FlushResult { SUCCESS, ///< Confirmed: all bytes left the TX FIFO. TIMEOUT, ///< Confirmed: timed out before TX completed. FAILED, ///< Confirmed: driver or hardware error. ASSUMED_SUCCESS, ///< Platform cannot report result; success is assumed. }; +#pragma pop_macro("SUCCESS") class UARTComponent { public: diff --git a/tests/components/uart/test.rtl87xx-ard.yaml b/tests/components/uart/test.rtl87xx-ard.yaml new file mode 100644 index 0000000000..414bf1f14d --- /dev/null +++ b/tests/components/uart/test.rtl87xx-ard.yaml @@ -0,0 +1,14 @@ +uart: + - id: uart_id + tx_pin: PA23 + rx_pin: PA18 + baud_rate: 9600 + data_bits: 8 + parity: NONE + stop_bits: 1 + +switch: + - platform: uart + name: "UART Switch" + uart_id: uart_id + data: [0x01, 0x02, 0x03] From de3292c828c2116713dafddf86e725e6f2b69fc4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Mar 2026 10:19:28 -1000 Subject: [PATCH 068/166] [light] Fix gamma LUT quantizing small brightness to zero (#15060) --- esphome/components/light/__init__.py | 28 +++-- tests/unit_tests/components/light/__init__.py | 0 .../components/light/test_gamma_table.py | 117 ++++++++++++++++++ 3 files changed, 138 insertions(+), 7 deletions(-) create mode 100644 tests/unit_tests/components/light/__init__.py create mode 100644 tests/unit_tests/components/light/test_gamma_table.py diff --git a/esphome/components/light/__init__.py b/esphome/components/light/__init__.py index 4403281116..64452e4282 100644 --- a/esphome/components/light/__init__.py +++ b/esphome/components/light/__init__.py @@ -81,18 +81,32 @@ def _get_data() -> LightData: return CORE.data[DOMAIN] +def generate_gamma_table(gamma_correct: float) -> list[HexInt]: + """Generate a 256-entry uint16 gamma lookup table. + + For gamma > 0, non-zero indices are clamped to a minimum of 1 to preserve + the invariant that non-zero input always produces non-zero output. Without + this, small brightness values (e.g. 1%) get quantized to exactly 0.0, + which breaks zero_means_zero logic in FloatOutput. + """ + if gamma_correct > 0: + return [ + HexInt( + max(1, min(65535, int(round((i / 255.0) ** gamma_correct * 65535)))) + if i > 0 + else HexInt(0) + ) + for i in range(256) + ] + return [HexInt(int(round(i / 255.0 * 65535))) for i in range(256)] + + def _get_or_create_gamma_table(gamma_correct): data = _get_data() if gamma_correct in data.gamma_tables: return data.gamma_tables[gamma_correct] - if gamma_correct > 0: - forward = [ - HexInt(min(65535, int(round((i / 255.0) ** gamma_correct * 65535)))) - for i in range(256) - ] - else: - forward = [HexInt(int(round(i / 255.0 * 65535))) for i in range(256)] + forward = generate_gamma_table(gamma_correct) gamma_str = f"{gamma_correct}".replace(".", "_") fwd_id = ID(f"gamma_{gamma_str}_fwd", is_declaration=True, type=cg.uint16) diff --git a/tests/unit_tests/components/light/__init__.py b/tests/unit_tests/components/light/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/unit_tests/components/light/test_gamma_table.py b/tests/unit_tests/components/light/test_gamma_table.py new file mode 100644 index 0000000000..a302a355dc --- /dev/null +++ b/tests/unit_tests/components/light/test_gamma_table.py @@ -0,0 +1,117 @@ +"""Tests for the gamma LUT table generation.""" + +import pytest + +from esphome.components.light import generate_gamma_table + + +def _simulate_gamma_correct_lut(table: list[int], value: float) -> float: + """Simulate the C++ gamma_correct_lut interpolation from light_state.cpp.""" + if value <= 0.0: + return 0.0 + if value >= 1.0: + return 1.0 + scaled = value * 255.0 + idx = int(scaled) + if idx >= 255: + return table[255] / 65535.0 + frac = scaled - idx + a = float(table[idx]) + b = float(table[idx + 1]) + return (a + frac * (b - a)) / 65535.0 + + +def test_table_length() -> None: + """Table must always have exactly 256 entries.""" + table = generate_gamma_table(2.8) + assert len(table) == 256 + + +def test_index_zero_is_zero() -> None: + """Index 0 must be 0 so true off remains off.""" + for gamma in (1.0, 2.0, 2.2, 2.8, 3.0): + table = generate_gamma_table(gamma) + assert table[0] == 0, f"gamma={gamma}" + + +def test_index_255_is_max() -> None: + """Index 255 must be 65535 (full on).""" + for gamma in (1.0, 2.0, 2.2, 2.8, 3.0): + table = generate_gamma_table(gamma) + assert table[255] == 65535, f"gamma={gamma}" + + +@pytest.mark.parametrize("gamma", [1.0, 2.0, 2.2, 2.8, 3.0]) +def test_nonzero_indices_are_nonzero(gamma: float) -> None: + """All indices > 0 must produce non-zero values. + + This prevents zero_means_zero breakage: non-zero input must always + produce non-zero output so FloatOutput applies min_power scaling. + """ + table = generate_gamma_table(gamma) + for i in range(1, 256): + assert table[i] >= 1, f"gamma={gamma}, index {i}: got {table[i]}" + + +@pytest.mark.parametrize("gamma", [1.0, 2.0, 2.2, 2.8, 3.0]) +def test_table_monotonically_nondecreasing(gamma: float) -> None: + """The gamma table must be monotonically non-decreasing.""" + table = generate_gamma_table(gamma) + for i in range(1, 256): + assert table[i] >= table[i - 1], ( + f"gamma={gamma}: table[{i}]={table[i]} < table[{i - 1}]={table[i - 1]}" + ) + + +def test_linear_gamma() -> None: + """With gamma=0 (linear), table should be evenly spaced.""" + table = generate_gamma_table(0) + assert table[0] == 0 + assert table[128] == round(128 / 255.0 * 65535) + assert table[255] == 65535 + + +@pytest.mark.parametrize("brightness", [0.01, 0.005, 0.001, 1 / 255]) +def test_small_brightness_nonzero_after_lut(brightness: float) -> None: + """Small but non-zero brightness must produce non-zero output through the LUT. + + Regression test for #15055: with zero_means_zero=true, a gamma-corrected + value of exactly 0.0 causes FloatOutput to skip min_power scaling, turning + the LED off instead of to minimum brightness. + """ + table = generate_gamma_table(2.8) + result = _simulate_gamma_correct_lut(table, brightness) + assert result > 0.0, ( + f"brightness={brightness}: gamma LUT returned 0.0, would break zero_means_zero" + ) + + +@pytest.mark.parametrize("gamma", [1.0, 2.0, 2.2, 2.8, 3.0]) +def test_small_brightness_nonzero_all_gammas(gamma: float) -> None: + """1% brightness must be non-zero for all common gamma values.""" + table = generate_gamma_table(gamma) + result = _simulate_gamma_correct_lut(table, 0.01) + assert result > 0.0, f"gamma={gamma}: 1% brightness returned 0.0" + + +def test_lut_zero_returns_zero() -> None: + """LUT with input 0.0 must return 0.0.""" + table = generate_gamma_table(2.8) + assert _simulate_gamma_correct_lut(table, 0.0) == 0.0 + + +def test_lut_one_returns_one() -> None: + """LUT with input 1.0 must return 1.0.""" + table = generate_gamma_table(2.8) + assert _simulate_gamma_correct_lut(table, 1.0) == 1.0 + + +def test_lut_output_monotonically_nondecreasing() -> None: + """LUT output must be monotonically non-decreasing across the full range.""" + table = generate_gamma_table(2.8) + prev = 0.0 + for i in range(1001): + value = i / 1000.0 + result = _simulate_gamma_correct_lut(table, value) + assert result >= prev, f"value={value}: result {result} < previous {prev}" + prev = result From bbfe324dd6da2ffddcc98fe67d9655bccb854f6d Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Sun, 22 Mar 2026 16:19:46 -0400 Subject: [PATCH 069/166] [ultrasonic] Fix ISR edge detection with debounce and trigger filtering (#15014) --- .../ultrasonic/ultrasonic_sensor.cpp | 29 +++++++++---------- .../components/ultrasonic/ultrasonic_sensor.h | 3 +- 2 files changed, 14 insertions(+), 18 deletions(-) diff --git a/esphome/components/ultrasonic/ultrasonic_sensor.cpp b/esphome/components/ultrasonic/ultrasonic_sensor.cpp index d3f7e69444..a354b198b4 100644 --- a/esphome/components/ultrasonic/ultrasonic_sensor.cpp +++ b/esphome/components/ultrasonic/ultrasonic_sensor.cpp @@ -6,11 +6,17 @@ namespace esphome::ultrasonic { static const char *const TAG = "ultrasonic.sensor"; +static constexpr uint32_t DEBOUNCE_US = 50; // Ignore edges within 50us of each other (noise filtering) +static constexpr uint32_t START_DELAY_US = 100; // Ignore edges within 100us of trigger (filters bleed-through) static constexpr uint32_t START_TIMEOUT_US = 40000; // Maximum time to wait for echo pulse to start void IRAM_ATTR UltrasonicSensorStore::gpio_intr(UltrasonicSensorStore *arg) { uint32_t now = micros(); - if (arg->echo_pin_isr.digital_read()) { + // Ignore edges after measurement complete or too soon after trigger pulse + if (arg->echo_end || (now - arg->measurement_start_us) <= START_DELAY_US) { + return; + } + if (!arg->echo_start || (now - arg->echo_start_us) <= DEBOUNCE_US) { arg->echo_start_us = now; arg->echo_start = true; } else { @@ -21,15 +27,14 @@ void IRAM_ATTR UltrasonicSensorStore::gpio_intr(UltrasonicSensorStore *arg) { void IRAM_ATTR UltrasonicSensorComponent::send_trigger_pulse_() { InterruptLock lock; - this->store_.echo_start_us = 0; - this->store_.echo_end_us = 0; this->store_.echo_start = false; this->store_.echo_end = false; + this->store_.measurement_start_us = micros(); this->trigger_pin_isr_.digital_write(true); delayMicroseconds(this->pulse_time_us_); this->trigger_pin_isr_.digital_write(false); this->measurement_pending_ = true; - this->measurement_start_us_ = micros(); + this->measurement_start_us_ = this->store_.measurement_start_us; } void UltrasonicSensorComponent::setup() { @@ -37,7 +42,6 @@ void UltrasonicSensorComponent::setup() { this->trigger_pin_->digital_write(false); this->trigger_pin_isr_ = this->trigger_pin_->to_isr(); this->echo_pin_->setup(); - this->store_.echo_pin_isr = this->echo_pin_->to_isr(); this->echo_pin_->attach_interrupt(UltrasonicSensorStore::gpio_intr, &this->store_, gpio::INTERRUPT_ANY_EDGE); } @@ -77,17 +81,10 @@ void UltrasonicSensorComponent::loop() { } if (this->store_.echo_end) { - float result; - if (this->store_.echo_start) { - uint32_t pulse_duration = this->store_.echo_end_us - this->store_.echo_start_us; - ESP_LOGV(TAG, "pulse start took %" PRIu32 "us, echo took %" PRIu32 "us", - this->store_.echo_start_us - this->measurement_start_us_, pulse_duration); - result = UltrasonicSensorComponent::us_to_m(pulse_duration); - ESP_LOGD(TAG, "'%s' - Got distance: %.3f m", this->name_.c_str(), result); - } else { - ESP_LOGW(TAG, "'%s' - pulse end before pulse start, does the echo pin need to be inverted?", this->name_.c_str()); - result = NAN; - } + uint32_t pulse_duration = this->store_.echo_end_us - this->store_.echo_start_us; + ESP_LOGV(TAG, "Echo took %" PRIu32 "us", pulse_duration); + float result = UltrasonicSensorComponent::us_to_m(pulse_duration); + ESP_LOGD(TAG, "'%s' - Got distance: %.3f m", this->name_.c_str(), result); this->publish_state(result); this->measurement_pending_ = false; return; diff --git a/esphome/components/ultrasonic/ultrasonic_sensor.h b/esphome/components/ultrasonic/ultrasonic_sensor.h index 541f7d2b70..7d333a1b24 100644 --- a/esphome/components/ultrasonic/ultrasonic_sensor.h +++ b/esphome/components/ultrasonic/ultrasonic_sensor.h @@ -11,8 +11,7 @@ namespace esphome::ultrasonic { struct UltrasonicSensorStore { static void gpio_intr(UltrasonicSensorStore *arg); - ISRInternalGPIOPin echo_pin_isr; - volatile uint32_t wait_start_us{0}; + volatile uint32_t measurement_start_us{0}; volatile uint32_t echo_start_us{0}; volatile uint32_t echo_end_us{0}; volatile bool echo_start{false}; From 036be63f7b022023a7ab36f138dbd6e7df1dde84 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Mar 2026 10:20:28 -1000 Subject: [PATCH 070/166] [logger] Fix race condition in task log buffer initialization (#15071) 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/logger/__init__.py | 39 ++++++++++++++++----------- esphome/components/logger/logger.cpp | 20 ++++++-------- esphome/components/logger/logger.h | 5 ++-- 3 files changed, 35 insertions(+), 29 deletions(-) diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index a5601e6a8f..3da81e12a0 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -331,11 +331,27 @@ async def to_code(config: ConfigType) -> None: CORE.data.setdefault(CONF_LOGGER, {})[CONF_LEVEL] = level tx_buffer_size = config[CONF_TX_BUFFER_SIZE] cg.add_define("ESPHOME_LOGGER_TX_BUFFER_SIZE", tx_buffer_size) - log = cg.new_Pvariable( - config[CONF_ID], - baud_rate, - ) - if CORE.is_esp32: + # Determine task log buffer size and define USE_ESPHOME_TASK_LOG_BUFFER early + # so the constructor can allocate the buffer immediately, preventing a race + # where another task logs before the buffer is initialized. + task_log_buffer_size = 0 + if CORE.is_esp32 or CORE.is_libretiny or CORE.is_nrf52: + task_log_buffer_size = config[CONF_TASK_LOG_BUFFER_SIZE] + elif CORE.is_host: + task_log_buffer_size = 64 # Fixed 64 slots for host + if task_log_buffer_size > 0: + cg.add_define("USE_ESPHOME_TASK_LOG_BUFFER") + log = cg.new_Pvariable( + config[CONF_ID], + baud_rate, + task_log_buffer_size, + ) + else: + log = cg.new_Pvariable( + config[CONF_ID], + baud_rate, + ) + if CORE.is_esp32 or CORE.is_host: cg.add(log.create_pthread_key()) # set_uart_selection() must be called before pre_setup() because # pre_setup() switches on uart_ to decide which hardware to initialize @@ -364,17 +380,10 @@ async def _late_logger_init(config: ConfigType) -> None: log = await cg.get_variable(config[CONF_ID]) level = config[CONF_LEVEL] baud_rate: int = config[CONF_BAUD_RATE] - if CORE.is_esp32 or CORE.is_libretiny or CORE.is_nrf52: - task_log_buffer_size = config[CONF_TASK_LOG_BUFFER_SIZE] + if CORE.using_zephyr: + task_log_buffer_size = config.get(CONF_TASK_LOG_BUFFER_SIZE, 0) if task_log_buffer_size > 0: - cg.add_define("USE_ESPHOME_TASK_LOG_BUFFER") - cg.add(log.init_log_buffer(task_log_buffer_size)) - if CORE.using_zephyr: - zephyr_add_prj_conf("MPSC_PBUF", True) - elif CORE.is_host: - cg.add(log.create_pthread_key()) - cg.add_define("USE_ESPHOME_TASK_LOG_BUFFER") - cg.add(log.init_log_buffer(64)) # Fixed 64 slots for host + zephyr_add_prj_conf("MPSC_PBUF", True) # Enable runtime tag levels if logs are configured or explicitly enabled logs_config = config[CONF_LOGS] diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index 497809cd2e..ceacded775 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -152,29 +152,25 @@ inline uint8_t Logger::level_for(const char *tag) { return this->current_level_; } +#ifdef USE_ESPHOME_TASK_LOG_BUFFER +Logger::Logger(uint32_t baud_rate, size_t task_log_buffer_size) : baud_rate_(baud_rate) { +#else Logger::Logger(uint32_t baud_rate) : baud_rate_(baud_rate) { +#endif #if defined(USE_ESP32) || defined(USE_LIBRETINY) this->main_task_ = xTaskGetCurrentTaskHandle(); #elif defined(USE_ZEPHYR) this->main_task_ = k_current_get(); #elif defined(USE_HOST) - this->main_thread_ = pthread_self(); +this->main_thread_ = pthread_self(); #endif -} #ifdef USE_ESPHOME_TASK_LOG_BUFFER -void Logger::init_log_buffer(size_t total_buffer_size) { - // Host uses slot count instead of byte size // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) - allocated once, never freed - this->log_buffer_ = new logger::TaskLogBuffer(total_buffer_size); - -#if !(defined(USE_ZEPHYR) && defined(USE_LOGGER_UART_SELECTION_USB_CDC)) - // Start with loop disabled when using task buffer - // The loop will be enabled automatically when messages arrive - // Zephyr with USB CDC needs loop active to poll port readiness via cdc_loop_() - this->disable_loop_when_buffer_empty_(); + this->log_buffer_ = new logger::TaskLogBuffer(task_log_buffer_size); + // Note: we don't disable loop here because the component isn't registered with App yet. + // The loop self-disables on its first iteration when it finds no messages to process. #endif } -#endif #if defined(USE_ESPHOME_TASK_LOG_BUFFER) || (defined(USE_ZEPHYR) && defined(USE_LOGGER_UART_SELECTION_USB_CDC)) void Logger::loop() { diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index 263d12b444..fdb330c133 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -143,9 +143,10 @@ enum UARTSelection : uint8_t { */ class Logger final : public Component { public: - explicit Logger(uint32_t baud_rate); #ifdef USE_ESPHOME_TASK_LOG_BUFFER - void init_log_buffer(size_t total_buffer_size); + explicit Logger(uint32_t baud_rate, size_t task_log_buffer_size); +#else + explicit Logger(uint32_t baud_rate); #endif #if defined(USE_ESPHOME_TASK_LOG_BUFFER) || (defined(USE_ZEPHYR) && defined(USE_LOGGER_UART_SELECTION_USB_CDC)) void loop() override; From a3c483edf3838ba12fb83a927e9d4b1a744d9364 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jason=20K=C3=B6lker?= Date: Sun, 22 Mar 2026 20:21:08 +0000 Subject: [PATCH 071/166] [pmsx003] Keep active-mode reads aligned (#14832) --- esphome/components/pmsx003/pmsx003.cpp | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/esphome/components/pmsx003/pmsx003.cpp b/esphome/components/pmsx003/pmsx003.cpp index 114ecf435e..6275ff60c2 100644 --- a/esphome/components/pmsx003/pmsx003.cpp +++ b/esphome/components/pmsx003/pmsx003.cpp @@ -95,10 +95,6 @@ void PMSX003Component::loop() { // Just go ahead and read stuff break; } - } else if (now - this->last_update_ < this->update_interval_) { - // Otherwise just leave the sensor powered up and come back when we hit the update - // time - return; } if (now - this->last_transmission_ >= 500) { @@ -114,10 +110,11 @@ void PMSX003Component::loop() { this->read_byte(&this->data_[this->data_index_]); auto check = this->check_byte_(); if (!check.has_value()) { - // finished - this->parse_data_(); + if (this->update_interval_ > STABILISING_MS || now - this->last_update_ >= this->update_interval_) { + this->parse_data_(); + this->last_update_ = now; + } this->data_index_ = 0; - this->last_update_ = now; } else if (!*check) { // wrong data this->data_index_ = 0; @@ -138,7 +135,7 @@ optional PMSX003Component::check_byte_() { return true; } - ESP_LOGW(TAG, "Start character %u mismatch: 0x%02X != 0x%02X", index + 1, byte, START_CHARACTER_1); + ESP_LOGW(TAG, "Start character %u mismatch: 0x%02X != 0x%02X", index + 1, byte, start_char); return false; } From 320474b62d26fd4d8e6d55eba84b6c76e5bad48c Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 23 Mar 2026 09:28:58 +1300 Subject: [PATCH 072/166] Bump version to 2026.3.1 --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index d8a030536e..d86894435f 100644 --- a/Doxyfile +++ b/Doxyfile @@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = 2026.3.0 +PROJECT_NUMBER = 2026.3.1 # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a diff --git a/esphome/const.py b/esphome/const.py index 756ad69464..52ac7acd22 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2026.3.0" +__version__ = "2026.3.1" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From daafa8faa38fd8ba9cb450da18407961dbfde19a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Mar 2026 10:36:18 -1000 Subject: [PATCH 073/166] [wifi] Inline trivial WiFiAP and WiFiComponent accessors (#15075) --- esphome/components/wifi/wifi_component.cpp | 5 ----- esphome/components/wifi/wifi_component.h | 10 +++++----- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 346276692a..08065a7544 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -871,9 +871,6 @@ void WiFiComponent::loop() { WiFiComponent::WiFiComponent() { global_wifi_component = this; } -bool WiFiComponent::has_ap() const { return this->has_ap_; } -bool WiFiComponent::is_ap_active() const { return this->ap_started_; } -bool WiFiComponent::has_sta() const { return !this->sta_.empty(); } #ifdef USE_WIFI_11KV_SUPPORT void WiFiComponent::set_btm(bool btm) { this->btm_ = btm; } void WiFiComponent::set_rrm(bool rrm) { this->rrm_ = rrm; } @@ -2250,8 +2247,6 @@ bool WiFiAP::has_bssid() const { return this->bssid_ != bssid_t{}; } #ifdef USE_WIFI_WPA2_EAP const optional &WiFiAP::get_eap() const { return this->eap_; } #endif -uint8_t WiFiAP::get_channel() const { return this->channel_; } -bool WiFiAP::has_channel() const { return this->channel_ != 0; } #ifdef USE_WIFI_MANUAL_IP const optional &WiFiAP::get_manual_ip() const { return this->manual_ip_; } #endif diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 057f2c0661..bd202604d6 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -263,8 +263,8 @@ class WiFiAP { #ifdef USE_WIFI_WPA2_EAP const optional &get_eap() const; #endif // USE_WIFI_WPA2_EAP - uint8_t get_channel() const; - bool has_channel() const; + uint8_t get_channel() const { return this->channel_; } + bool has_channel() const { return this->channel_ != 0; } int8_t get_priority() const { return priority_; } #ifdef USE_WIFI_MANUAL_IP const optional &get_manual_ip() const; @@ -470,9 +470,9 @@ class WiFiComponent final : public Component { /// Reconnect WiFi if required. void loop() override; - bool has_sta() const; - bool has_ap() const; - bool is_ap_active() const; + bool has_sta() const { return !this->sta_.empty(); } + bool has_ap() const { return this->has_ap_; } + bool is_ap_active() const { return this->ap_started_; } #ifdef USE_WIFI_11KV_SUPPORT void set_btm(bool btm); From 593dbc9e67f4347dcb5642ffe60cec63edf97408 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Mar 2026 10:50:58 -1000 Subject: [PATCH 074/166] [logger] Fix unit test and benchmark Logger constructor calls (#15085) --- tests/benchmarks/components/main.cpp | 2 +- tests/components/main.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/benchmarks/components/main.cpp b/tests/benchmarks/components/main.cpp index 9bc0c31a15..901dc44c07 100644 --- a/tests/benchmarks/components/main.cpp +++ b/tests/benchmarks/components/main.cpp @@ -26,7 +26,7 @@ void setup() { // Log functions call global_logger->log_vprintf_() without a null check, // so we must set up a Logger before any test that triggers logging. - static esphome::logger::Logger test_logger(0); + static esphome::logger::Logger test_logger(0, 64); test_logger.set_log_level(ESPHOME_LOG_LEVEL); test_logger.pre_setup(); diff --git a/tests/components/main.cpp b/tests/components/main.cpp index 373fde7151..622b1f107b 100644 --- a/tests/components/main.cpp +++ b/tests/components/main.cpp @@ -22,7 +22,7 @@ void original_setup() { void setup() { // Log functions call global_logger->log_vprintf_() without a null check, // so we must set up a Logger before any test that triggers logging. - static esphome::logger::Logger test_logger(0); + static esphome::logger::Logger test_logger(0, 64); test_logger.set_log_level(ESPHOME_LOG_LEVEL); test_logger.pre_setup(); From 45c0e6ef7f3cf29ff1934cc8d94f8dcb9217df09 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Mar 2026 10:52:46 -1000 Subject: [PATCH 075/166] [logger] Fix unit test Logger constructor call (#15086) --- tests/components/main.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/main.cpp b/tests/components/main.cpp index 373fde7151..622b1f107b 100644 --- a/tests/components/main.cpp +++ b/tests/components/main.cpp @@ -22,7 +22,7 @@ void original_setup() { void setup() { // Log functions call global_logger->log_vprintf_() without a null check, // so we must set up a Logger before any test that triggers logging. - static esphome::logger::Logger test_logger(0); + static esphome::logger::Logger test_logger(0, 64); test_logger.set_log_level(ESPHOME_LOG_LEVEL); test_logger.pre_setup(); From 27f3a5f5f472ee3c80b5b8699e25a2f4d588b73c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Mar 2026 11:54:54 -1000 Subject: [PATCH 076/166] [sht4x] Add missing hal.h include for millis() on ESP-IDF (#15087) --- esphome/components/sht4x/sht4x.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/components/sht4x/sht4x.cpp b/esphome/components/sht4x/sht4x.cpp index 43c2436a56..b1dbde22a4 100644 --- a/esphome/components/sht4x/sht4x.cpp +++ b/esphome/components/sht4x/sht4x.cpp @@ -1,4 +1,5 @@ #include "sht4x.h" +#include "esphome/core/hal.h" #include "esphome/core/log.h" namespace esphome { From 6d16c57747773431da0484b0e10e10e5129fed39 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Mar 2026 11:54:54 -1000 Subject: [PATCH 077/166] [sht4x] Add missing hal.h include for millis() on ESP-IDF (#15087) --- esphome/components/sht4x/sht4x.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/components/sht4x/sht4x.cpp b/esphome/components/sht4x/sht4x.cpp index 43c2436a56..b1dbde22a4 100644 --- a/esphome/components/sht4x/sht4x.cpp +++ b/esphome/components/sht4x/sht4x.cpp @@ -1,4 +1,5 @@ #include "sht4x.h" +#include "esphome/core/hal.h" #include "esphome/core/log.h" namespace esphome { From 5cc4f6e85ab77fe914000fb41013f6fc5aadded2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Mar 2026 12:29:12 -1000 Subject: [PATCH 078/166] [logger] Add task_log_buffer_zephyr.cpp to platform source filter (#15081) --- esphome/components/logger/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index 3da81e12a0..4345e291a3 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -614,6 +614,7 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform( PlatformFramework.RTL87XX_ARDUINO, PlatformFramework.LN882X_ARDUINO, }, + "task_log_buffer_zephyr.cpp": {PlatformFramework.NRF52_ZEPHYR}, } ) From 4d09eb2cec628cd5ad082678f3fa5266ef46fe1d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Mar 2026 12:29:28 -1000 Subject: [PATCH 079/166] [tests] Fix flaky ld24xx integration tests by disabling API batching (#15050) --- tests/integration/fixtures/uart_mock_ld2410.yaml | 1 + tests/integration/fixtures/uart_mock_ld2410_engineering.yaml | 1 + tests/integration/fixtures/uart_mock_ld2412.yaml | 1 + tests/integration/fixtures/uart_mock_ld2412_engineering.yaml | 1 + .../fixtures/uart_mock_ld2412_engineering_truncated.yaml | 1 + tests/integration/fixtures/uart_mock_ld2420.yaml | 1 + tests/integration/fixtures/uart_mock_ld2420_simple.yaml | 1 + tests/integration/fixtures/uart_mock_ld2450.yaml | 1 + 8 files changed, 8 insertions(+) diff --git a/tests/integration/fixtures/uart_mock_ld2410.yaml b/tests/integration/fixtures/uart_mock_ld2410.yaml index 59838b0599..e7568c0e3f 100644 --- a/tests/integration/fixtures/uart_mock_ld2410.yaml +++ b/tests/integration/fixtures/uart_mock_ld2410.yaml @@ -3,6 +3,7 @@ esphome: host: api: + batch_delay: 0ms # Disable batching to receive all state updates logger: level: VERBOSE diff --git a/tests/integration/fixtures/uart_mock_ld2410_engineering.yaml b/tests/integration/fixtures/uart_mock_ld2410_engineering.yaml index 4625ae8511..5e62a4b8a8 100644 --- a/tests/integration/fixtures/uart_mock_ld2410_engineering.yaml +++ b/tests/integration/fixtures/uart_mock_ld2410_engineering.yaml @@ -3,6 +3,7 @@ esphome: host: api: + batch_delay: 0ms # Disable batching to receive all state updates logger: level: VERBOSE diff --git a/tests/integration/fixtures/uart_mock_ld2412.yaml b/tests/integration/fixtures/uart_mock_ld2412.yaml index 9cf9d6bb87..525ab0d5c4 100644 --- a/tests/integration/fixtures/uart_mock_ld2412.yaml +++ b/tests/integration/fixtures/uart_mock_ld2412.yaml @@ -3,6 +3,7 @@ esphome: host: api: + batch_delay: 0ms # Disable batching to receive all state updates logger: level: VERBOSE diff --git a/tests/integration/fixtures/uart_mock_ld2412_engineering.yaml b/tests/integration/fixtures/uart_mock_ld2412_engineering.yaml index a69e18888e..9f83e3226f 100644 --- a/tests/integration/fixtures/uart_mock_ld2412_engineering.yaml +++ b/tests/integration/fixtures/uart_mock_ld2412_engineering.yaml @@ -3,6 +3,7 @@ esphome: host: api: + batch_delay: 0ms # Disable batching to receive all state updates logger: level: VERBOSE diff --git a/tests/integration/fixtures/uart_mock_ld2412_engineering_truncated.yaml b/tests/integration/fixtures/uart_mock_ld2412_engineering_truncated.yaml index c0bd514762..fd1cd9fd33 100644 --- a/tests/integration/fixtures/uart_mock_ld2412_engineering_truncated.yaml +++ b/tests/integration/fixtures/uart_mock_ld2412_engineering_truncated.yaml @@ -3,6 +3,7 @@ esphome: host: api: + batch_delay: 0ms # Disable batching to receive all state updates logger: level: VERBOSE diff --git a/tests/integration/fixtures/uart_mock_ld2420.yaml b/tests/integration/fixtures/uart_mock_ld2420.yaml index 5380b81071..ee22f807d4 100644 --- a/tests/integration/fixtures/uart_mock_ld2420.yaml +++ b/tests/integration/fixtures/uart_mock_ld2420.yaml @@ -3,6 +3,7 @@ esphome: host: api: + batch_delay: 0ms # Disable batching to receive all state updates logger: level: VERBOSE diff --git a/tests/integration/fixtures/uart_mock_ld2420_simple.yaml b/tests/integration/fixtures/uart_mock_ld2420_simple.yaml index 2ceca5d35d..d3b6ad5d92 100644 --- a/tests/integration/fixtures/uart_mock_ld2420_simple.yaml +++ b/tests/integration/fixtures/uart_mock_ld2420_simple.yaml @@ -3,6 +3,7 @@ esphome: host: api: + batch_delay: 0ms # Disable batching to receive all state updates logger: level: VERBOSE diff --git a/tests/integration/fixtures/uart_mock_ld2450.yaml b/tests/integration/fixtures/uart_mock_ld2450.yaml index 269136da68..4140ff659c 100644 --- a/tests/integration/fixtures/uart_mock_ld2450.yaml +++ b/tests/integration/fixtures/uart_mock_ld2450.yaml @@ -3,6 +3,7 @@ esphome: host: api: + batch_delay: 0ms # Disable batching to receive all state updates logger: level: VERBOSE From 9152f77cdd3833d51026ee95c917ed868c234ba8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Mar 2026 12:31:48 -1000 Subject: [PATCH 080/166] [core] Reduce automation call chain stack depth (#15042) --- esphome/core/automation.h | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/esphome/core/automation.h b/esphome/core/automation.h index 7934fdbec9..ca4a2c8b6b 100644 --- a/esphome/core/automation.h +++ b/esphome/core/automation.h @@ -322,7 +322,9 @@ template class Automation; template class Trigger { public: /// Inform the parent automation that the event has triggered. - void trigger(const Ts &...x) { + // Force-inline: collapses the Trigger→Automation→ActionList forwarding + // chain into a single frame, reducing automation call stack depth. + inline void trigger(const Ts &...x) ESPHOME_ALWAYS_INLINE { if (this->automation_parent_ == nullptr) return; this->automation_parent_->trigger(x...); @@ -429,7 +431,9 @@ template class ActionList { this->add_action(action); } } - void play(const Ts &...x) { + // Force-inline: part of the Trigger→Automation→ActionList forwarding + // chain collapsed to reduce automation call stack depth. + inline void play(const Ts &...x) ESPHOME_ALWAYS_INLINE { if (this->actions_begin_ != nullptr) this->actions_begin_->play_complex(x...); } @@ -473,7 +477,9 @@ template class Automation { void stop() { this->actions_.stop(); } - void trigger(const Ts &...x) { this->actions_.play(x...); } + // Force-inline: part of the Trigger→Automation→ActionList forwarding + // chain collapsed to reduce automation call stack depth. + inline void trigger(const Ts &...x) ESPHOME_ALWAYS_INLINE { this->actions_.play(x...); } bool is_running() { return this->actions_.is_running(); } From 6caa9ee22770acee9ccbe2369001900dc772a7d2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Mar 2026 12:32:08 -1000 Subject: [PATCH 081/166] [logger] Move log level lookup tables to PROGMEM (#15003) --- esphome/components/logger/log_buffer.h | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/esphome/components/logger/log_buffer.h b/esphome/components/logger/log_buffer.h index 734cb14dc5..067ce04114 100644 --- a/esphome/components/logger/log_buffer.h +++ b/esphome/components/logger/log_buffer.h @@ -1,5 +1,6 @@ #pragma once +#include "esphome/core/hal.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" @@ -8,8 +9,8 @@ namespace esphome::logger { // Maximum header size: 35 bytes fixed + 32 bytes tag + 16 bytes thread name = 83 bytes (45 byte safety margin) static constexpr uint16_t MAX_HEADER_SIZE = 128; -// ANSI color code last digit (30-38 range, store only last digit to save RAM) -static constexpr char LOG_LEVEL_COLOR_DIGIT[] = { +// ANSI color code last digit (30-38 range, store only last digit to save RAM on ESP8266) +static const char LOG_LEVEL_COLOR_DIGIT[] PROGMEM = { '\0', // NONE '1', // ERROR (31 = red) '3', // WARNING (33 = yellow) @@ -20,7 +21,7 @@ static constexpr char LOG_LEVEL_COLOR_DIGIT[] = { '8', // VERY_VERBOSE (38 = white) }; -static constexpr char LOG_LEVEL_LETTER_CHARS[] = { +static const char LOG_LEVEL_LETTER_CHARS[] PROGMEM = { '\0', // NONE 'E', // ERROR 'W', // WARNING @@ -64,7 +65,7 @@ struct LogBuffer { *p++ = 'V'; // VERY_VERBOSE = "VV" *p++ = 'V'; } else { - *p++ = LOG_LEVEL_LETTER_CHARS[level]; + *p++ = static_cast(progmem_read_byte(reinterpret_cast(&LOG_LEVEL_LETTER_CHARS[level]))); } } *p++ = ']'; @@ -184,7 +185,7 @@ struct LogBuffer { *p++ = (level == 1) ? '1' : '0'; // Only ERROR is bold *p++ = ';'; *p++ = '3'; - *p++ = LOG_LEVEL_COLOR_DIGIT[level]; + *p++ = static_cast(progmem_read_byte(reinterpret_cast(&LOG_LEVEL_COLOR_DIGIT[level]))); *p++ = 'm'; } // Copy string without null terminator, updates pointer in place From 30f66be1dab8dc645fdd77293c0730521d03f4dd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Mar 2026 12:32:42 -1000 Subject: [PATCH 082/166] [esp32] Mention ignore_pin_validation_error in flash pin error message (#14998) --- esphome/components/esp32/gpio_esp32.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/esphome/components/esp32/gpio_esp32.py b/esphome/components/esp32/gpio_esp32.py index b3166cf822..fec257f90b 100644 --- a/esphome/components/esp32/gpio_esp32.py +++ b/esphome/components/esp32/gpio_esp32.py @@ -28,7 +28,11 @@ def esp32_validate_gpio_pin(value: int) -> int: raise cv.Invalid(f"Invalid pin number: {value} (must be 0-39)") if value in _ESP_SDIO_PINS: raise cv.Invalid( - f"This pin cannot be used on ESP32s and is already used by the flash interface (function: {_ESP_SDIO_PINS[value]})" + f"This pin cannot be used on ESP32s and is already used by the flash interface" + f" (function: {_ESP_SDIO_PINS[value]})." + f" If you are using an ESP32 module that uses a different flash pin" + f" configuration (e.g. ESP32-PICO-V3-02), you can set" + f" 'ignore_pin_validation_error: true' to bypass this check." ) if 9 <= value <= 10: _LOGGER.warning( From b2b61bea6a12993bf6bcb0c2160a4fd735fcfe0f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Mar 2026 12:33:06 -1000 Subject: [PATCH 083/166] [web_server_idf] Inline send() to reduce httpd task stack depth (#15045) --- .../components/web_server_idf/web_server_idf.cpp | 13 ------------- esphome/components/web_server_idf/web_server_idf.h | 13 +++++++++++-- 2 files changed, 11 insertions(+), 15 deletions(-) diff --git a/esphome/components/web_server_idf/web_server_idf.cpp b/esphome/components/web_server_idf/web_server_idf.cpp index fb0c17c854..38ccfccb76 100644 --- a/esphome/components/web_server_idf/web_server_idf.cpp +++ b/esphome/components/web_server_idf/web_server_idf.cpp @@ -262,19 +262,6 @@ StringRef AsyncWebServerRequest::url_to(std::span buffer) co return StringRef(buffer.data(), decoded_len); } -void AsyncWebServerRequest::send(AsyncWebServerResponse *response) { - httpd_resp_send(*this, response->get_content_data(), response->get_content_size()); -} - -void AsyncWebServerRequest::send(int code, const char *content_type, const char *content) { - this->init_response_(nullptr, code, content_type); - if (content) { - httpd_resp_send(*this, content, HTTPD_RESP_USE_STRLEN); - } else { - httpd_resp_send(*this, nullptr, 0); - } -} - void AsyncWebServerRequest::redirect(const std::string &url) { httpd_resp_set_status(*this, "302 Found"); httpd_resp_set_hdr(*this, "Location", url.c_str()); diff --git a/esphome/components/web_server_idf/web_server_idf.h b/esphome/components/web_server_idf/web_server_idf.h index 9d81d89ec7..81683e8d85 100644 --- a/esphome/components/web_server_idf/web_server_idf.h +++ b/esphome/components/web_server_idf/web_server_idf.h @@ -134,8 +134,17 @@ class AsyncWebServerRequest { void redirect(const std::string &url); - void send(AsyncWebServerResponse *response); - void send(int code, const char *content_type = nullptr, const char *content = nullptr); + inline void ESPHOME_ALWAYS_INLINE send(AsyncWebServerResponse *response) { + httpd_resp_send(*this, response->get_content_data(), response->get_content_size()); + } + inline void ESPHOME_ALWAYS_INLINE send(int code, const char *content_type = nullptr, const char *content = nullptr) { + this->init_response_(nullptr, code, content_type); + if (content) { + httpd_resp_send(*this, content, HTTPD_RESP_USE_STRLEN); + } else { + httpd_resp_send(*this, nullptr, 0); + } + } // NOLINTNEXTLINE(readability-identifier-naming) AsyncWebServerResponse *beginResponse(int code, const char *content_type) { auto *res = new AsyncWebServerResponseEmpty(this); // NOLINT(cppcoreguidelines-owning-memory) From aef987dccf26fbe06a9a54f870e59eb1daa7a176 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Mar 2026 12:37:46 -1000 Subject: [PATCH 084/166] [core] Fix Callback::create memcpy from function reference (#14995) --- esphome/core/helpers.h | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 43431299de..82c6b3833c 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -1762,7 +1762,10 @@ template struct Callback { // Safe under C++20 (P0593R6): byte copy into aligned storage implicitly // creates objects of implicit-lifetime types (trivially copyable qualifies). Callback cb; // fn and ctx are zero-initialized by default - __builtin_memcpy(&cb.ctx_, &callable, sizeof(DecayF)); + // Decay callable to a local variable first. When F is a function reference + // (e.g. void(&)(int)), &callable would point at machine code, not a pointer variable. + DecayF decayed = std::forward(callable); + __builtin_memcpy(&cb.ctx_, &decayed, sizeof(DecayF)); cb.fn_ = [](void *c, Ts... args) { alignas(DecayF) char buf[sizeof(DecayF)]; __builtin_memcpy(buf, &c, sizeof(DecayF)); From 84727b1f71e2b3a08f297941592ef6df64fd2ebd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Mar 2026 12:41:01 -1000 Subject: [PATCH 085/166] [esp32] Validate eFuse MAC reads and reject garbage MACs (#15049) --- esphome/components/esp32/helpers.cpp | 46 ++++++++++++++++++++++------ esphome/core/helpers.cpp | 11 ++++++- 2 files changed, 46 insertions(+), 11 deletions(-) diff --git a/esphome/components/esp32/helpers.cpp b/esphome/components/esp32/helpers.cpp index 654a35b473..afcec8bfc7 100644 --- a/esphome/components/esp32/helpers.cpp +++ b/esphome/components/esp32/helpers.cpp @@ -9,11 +9,14 @@ #include #include +#include "esphome/core/log.h" #include "esp_random.h" #include "esp_system.h" namespace esphome { +static const char *const TAG = "esp32"; + bool random_bytes(uint8_t *data, size_t len) { esp_fill_random(data, len); return true; @@ -63,22 +66,43 @@ LwIPLock::~LwIPLock() { #endif } +/// Read MAC and validate both the return code and content. +static bool read_valid_mac(uint8_t *mac, esp_err_t err) { return err == ESP_OK && mac_address_is_valid(mac); } + +static constexpr size_t MAC_ADDRESS_SIZE_BITS = MAC_ADDRESS_SIZE * 8; // 48 bits + void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter) #if defined(CONFIG_SOC_IEEE802154_SUPPORTED) // When CONFIG_SOC_IEEE802154_SUPPORTED is defined, esp_efuse_mac_get_default // returns the 802.15.4 EUI-64 address, so we read directly from eFuse instead. - if (has_custom_mac_address()) { - esp_efuse_read_field_blob(ESP_EFUSE_MAC_CUSTOM, mac, 48); - } else { - esp_efuse_read_field_blob(ESP_EFUSE_MAC_FACTORY, mac, 48); + // Both paths already read raw eFuse bytes, so there is no CRC-bypass fallback + // (unlike the non-IEEE802154 path where esp_efuse_mac_get_default does CRC checks). + if (has_custom_mac_address() && + read_valid_mac(mac, esp_efuse_read_field_blob(ESP_EFUSE_MAC_CUSTOM, mac, MAC_ADDRESS_SIZE_BITS))) { + return; + } + if (read_valid_mac(mac, esp_efuse_read_field_blob(ESP_EFUSE_MAC_FACTORY, mac, MAC_ADDRESS_SIZE_BITS))) { + return; } #else - if (has_custom_mac_address()) { - esp_efuse_mac_get_custom(mac); - } else { - esp_efuse_mac_get_default(mac); + if (has_custom_mac_address() && read_valid_mac(mac, esp_efuse_mac_get_custom(mac))) { + return; + } + if (read_valid_mac(mac, esp_efuse_mac_get_default(mac))) { + return; + } + // Default MAC read failed (e.g., eFuse CRC error) - try reading raw eFuse bytes + // directly, bypassing CRC validation. A MAC that passes mac_address_is_valid() + // (non-zero, non-broadcast, unicast) is almost certainly the real factory MAC + // with a corrupted CRC byte, which is far better than returning garbage or zeros. + if (read_valid_mac(mac, esp_efuse_read_field_blob(ESP_EFUSE_MAC_FACTORY, mac, MAC_ADDRESS_SIZE_BITS))) { + ESP_LOGW(TAG, "eFuse MAC CRC failed but raw bytes appear valid - using raw eFuse MAC"); + return; } #endif + // All methods failed - zero the MAC rather than returning garbage + ESP_LOGE(TAG, "Failed to read a valid MAC address from eFuse"); + memset(mac, 0, MAC_ADDRESS_SIZE); } void set_mac_address(uint8_t *mac) { esp_base_mac_addr_set(mac); } @@ -88,9 +112,11 @@ bool has_custom_mac_address() { uint8_t mac[6]; // do not use 'esp_efuse_mac_get_custom(mac)' because it drops an error in the logs whenever it fails #ifndef USE_ESP32_VARIANT_ESP32 - return (esp_efuse_read_field_blob(ESP_EFUSE_USER_DATA_MAC_CUSTOM, mac, 48) == ESP_OK) && mac_address_is_valid(mac); + return (esp_efuse_read_field_blob(ESP_EFUSE_USER_DATA_MAC_CUSTOM, mac, MAC_ADDRESS_SIZE_BITS) == ESP_OK) && + mac_address_is_valid(mac); #else - return (esp_efuse_read_field_blob(ESP_EFUSE_MAC_CUSTOM, mac, 48) == ESP_OK) && mac_address_is_valid(mac); + return (esp_efuse_read_field_blob(ESP_EFUSE_MAC_CUSTOM, mac, MAC_ADDRESS_SIZE_BITS) == ESP_OK) && + mac_address_is_valid(mac); #endif #else return false; diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index ee99c54196..1732fc72e8 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -863,7 +863,16 @@ bool mac_address_is_valid(const uint8_t *mac) { is_all_ones = false; } } - return !(is_all_zeros || is_all_ones); + if (is_all_zeros || is_all_ones) { + return false; + } + // Reject multicast MACs (bit 0 of first byte set) - device MACs must be unicast. + // This catches garbage data from corrupted eFuse custom MAC areas, which often + // has random values that would otherwise pass the all-zeros/all-ones check. + if (mac[0] & 0x01) { + return false; + } + return true; } void IRAM_ATTR HOT delay_microseconds_safe(uint32_t us) { From 2c06464f7baa843bf8a45d9bbe93c8314af708a2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Mar 2026 12:41:54 -1000 Subject: [PATCH 086/166] [packet_transport] Use FixedVector and parent pointer to enable inline Callback storage (#14946) --- esphome/components/packet_transport/__init__.py | 10 ++++++++-- .../packet_transport/packet_transport.cpp | 16 ++++++++++------ .../packet_transport/packet_transport.h | 15 +++++++++++---- .../binary_sensor/binary_sensor_test.cpp | 4 ++++ .../packet_transport/sensor/sensor_test.cpp | 6 ++++++ 5 files changed, 39 insertions(+), 12 deletions(-) diff --git a/esphome/components/packet_transport/__init__.py b/esphome/components/packet_transport/__init__.py index 1930e45e85..0b166bb65c 100644 --- a/esphome/components/packet_transport/__init__.py +++ b/esphome/components/packet_transport/__init__.py @@ -177,13 +177,19 @@ async def register_packet_transport(var, config): cg.add(var.set_provider_encryption(name, hash_encryption_key(encryption))) is_provider = False - for sens_conf in config.get(CONF_SENSORS, ()): + sensors = config.get(CONF_SENSORS, ()) + binary_sensors = config.get(CONF_BINARY_SENSORS, ()) + if sensors: + cg.add(var.set_sensor_count(len(sensors))) + if binary_sensors: + cg.add(var.set_binary_sensor_count(len(binary_sensors))) + for sens_conf in sensors: is_provider = True sens_id = sens_conf[CONF_ID] sensor = await cg.get_variable(sens_id) bcst_id = sens_conf.get(CONF_BROADCAST_ID, sens_id.id) cg.add(var.add_sensor(bcst_id, sensor)) - for sens_conf in config.get(CONF_BINARY_SENSORS, ()): + for sens_conf in binary_sensors: is_provider = True sens_id = sens_conf[CONF_ID] sensor = await cg.get_variable(sens_id) diff --git a/esphome/components/packet_transport/packet_transport.cpp b/esphome/components/packet_transport/packet_transport.cpp index 964037a02c..a2199977aa 100644 --- a/esphome/components/packet_transport/packet_transport.cpp +++ b/esphome/components/packet_transport/packet_transport.cpp @@ -221,16 +221,20 @@ void PacketTransport::setup() { } #ifdef USE_SENSOR for (auto &sensor : this->sensors_) { - sensor.sensor->add_on_state_callback([this, &sensor](float x) { - this->updated_ = true; + // [&sensor] is safe: sensor refers to a FixedVector element that never reallocates, + // so the reference remains valid for the component's lifetime. + sensor.sensor->add_on_state_callback([&sensor](float x) { + sensor.parent->updated_ = true; sensor.updated = true; }); } #endif #ifdef USE_BINARY_SENSOR for (auto &sensor : this->binary_sensors_) { - sensor.sensor->add_on_state_callback([this, &sensor](bool value) { - this->updated_ = true; + // [&sensor] is safe: sensor refers to a FixedVector element that never reallocates, + // so the reference remains valid for the component's lifetime. + sensor.sensor->add_on_state_callback([&sensor](bool value) { + sensor.parent->updated_ = true; sensor.updated = true; }); } @@ -548,11 +552,11 @@ void PacketTransport::dump_config() { " Ping-pong: %s", this->platform_name_, YESNO(this->is_encrypted_()), YESNO(this->ping_pong_enable_)); #ifdef USE_SENSOR - for (auto sensor : this->sensors_) + for (const auto &sensor : this->sensors_) ESP_LOGCONFIG(TAG, " Sensor: %s", sensor.id); #endif #ifdef USE_BINARY_SENSOR - for (auto sensor : this->binary_sensors_) + for (const auto &sensor : this->binary_sensors_) ESP_LOGCONFIG(TAG, " Binary Sensor: %s", sensor.id); #endif for (const auto &host : this->providers_) { diff --git a/esphome/components/packet_transport/packet_transport.h b/esphome/components/packet_transport/packet_transport.h index b3798738e2..836775bc85 100644 --- a/esphome/components/packet_transport/packet_transport.h +++ b/esphome/components/packet_transport/packet_transport.h @@ -1,6 +1,7 @@ #pragma once #include "esphome/core/component.h" +#include "esphome/core/helpers.h" #include "esphome/core/preferences.h" #ifdef USE_SENSOR #include "esphome/components/sensor/sensor.h" @@ -37,11 +38,14 @@ struct Provider { #endif }; +class PacketTransport; + #ifdef USE_SENSOR struct Sensor { sensor::Sensor *sensor; const char *id; bool updated; + PacketTransport *parent; }; #endif #ifdef USE_BINARY_SENSOR @@ -49,6 +53,7 @@ struct BinarySensor { binary_sensor::BinarySensor *sensor; const char *id; bool updated; + PacketTransport *parent; }; #endif @@ -60,8 +65,9 @@ class PacketTransport : public PollingComponent { void dump_config() override; #ifdef USE_SENSOR + void set_sensor_count(size_t count) { this->sensors_.init(count); } void add_sensor(const char *id, sensor::Sensor *sensor) { - Sensor st{sensor, id, true}; + Sensor st{sensor, id, true, this}; this->sensors_.push_back(st); } void add_remote_sensor(const char *hostname, const char *remote_id, sensor::Sensor *sensor) { @@ -70,8 +76,9 @@ class PacketTransport : public PollingComponent { } #endif #ifdef USE_BINARY_SENSOR + void set_binary_sensor_count(size_t count) { this->binary_sensors_.init(count); } void add_binary_sensor(const char *id, binary_sensor::BinarySensor *sensor) { - BinarySensor st{sensor, id, true}; + BinarySensor st{sensor, id, true, this}; this->binary_sensors_.push_back(st); } @@ -141,11 +148,11 @@ class PacketTransport : public PollingComponent { std::vector encryption_key_{}; #ifdef USE_SENSOR - std::vector sensors_{}; + FixedVector sensors_{}; string_map_t> remote_sensors_{}; #endif #ifdef USE_BINARY_SENSOR - std::vector binary_sensors_{}; + FixedVector binary_sensors_{}; string_map_t> remote_binary_sensors_{}; #endif diff --git a/tests/components/packet_transport/binary_sensor/binary_sensor_test.cpp b/tests/components/packet_transport/binary_sensor/binary_sensor_test.cpp index 36af087d2c..5ad25c2d7d 100644 --- a/tests/components/packet_transport/binary_sensor/binary_sensor_test.cpp +++ b/tests/components/packet_transport/binary_sensor/binary_sensor_test.cpp @@ -5,6 +5,7 @@ namespace esphome::packet_transport::testing { TEST(PacketTransportBinarySensorTest, AddBinarySensor) { TestablePacketTransport transport; binary_sensor::BinarySensor bs; + transport.set_binary_sensor_count(1); transport.add_binary_sensor("motion", &bs); ASSERT_EQ(transport.binary_sensors_.size(), 1u); EXPECT_STREQ(transport.binary_sensors_[0].id, "motion"); @@ -24,6 +25,7 @@ TEST(PacketTransportBinarySensorTest, UnencryptedBinarySensorRoundTrip) { encoder.init_for_test("sender"); binary_sensor::BinarySensor local_bs; local_bs.state = true; + encoder.set_binary_sensor_count(1); encoder.add_binary_sensor("motion", &local_bs); encoder.send_data_(true); @@ -46,11 +48,13 @@ TEST(PacketTransportBinarySensorTest, MultipleSensorsRoundTrip) { sensor::Sensor s1, s2; s1.state = 10.0f; s2.state = 20.0f; + encoder.set_sensor_count(2); encoder.add_sensor("s1", &s1); encoder.add_sensor("s2", &s2); binary_sensor::BinarySensor bs1; bs1.state = true; + encoder.set_binary_sensor_count(1); encoder.add_binary_sensor("bs1", &bs1); encoder.send_data_(true); diff --git a/tests/components/packet_transport/sensor/sensor_test.cpp b/tests/components/packet_transport/sensor/sensor_test.cpp index 2f681aee58..5d1cfb4bc2 100644 --- a/tests/components/packet_transport/sensor/sensor_test.cpp +++ b/tests/components/packet_transport/sensor/sensor_test.cpp @@ -5,6 +5,7 @@ namespace esphome::packet_transport::testing { TEST(PacketTransportSensorTest, AddSensor) { TestablePacketTransport transport; sensor::Sensor s; + transport.set_sensor_count(1); transport.add_sensor("temp", &s); ASSERT_EQ(transport.sensors_.size(), 1u); EXPECT_STREQ(transport.sensors_[0].id, "temp"); @@ -26,6 +27,7 @@ TEST(PacketTransportSensorTest, UnencryptedSensorRoundTrip) { encoder.init_for_test("sender"); sensor::Sensor local_sensor; local_sensor.state = 42.5f; + encoder.set_sensor_count(1); encoder.add_sensor("temp", &local_sensor); encoder.send_data_(true); @@ -53,6 +55,7 @@ TEST(PacketTransportSensorTest, EncryptedSensorRoundTrip) { encoder.set_encryption_key(key); sensor::Sensor local_sensor; local_sensor.state = 99.9f; + encoder.set_sensor_count(1); encoder.add_sensor("temp", &local_sensor); encoder.send_data_(true); @@ -77,6 +80,7 @@ TEST(PacketTransportSensorTest, SendDataOnlyUpdated) { sensor::Sensor s1, s2; s1.state = 1.0f; s2.state = 2.0f; + encoder.set_sensor_count(2); encoder.add_sensor("s1", &s1); encoder.add_sensor("s2", &s2); @@ -111,6 +115,7 @@ TEST(PacketTransportSensorTest, PingKeyIncludedInTransmittedPacket) { responder.set_encryption_key(key); sensor::Sensor local_sensor; local_sensor.state = 77.7f; + responder.set_sensor_count(1); responder.add_sensor("temp", &local_sensor); // Requester sends a MAGIC_PING that the responder processes @@ -148,6 +153,7 @@ TEST(PacketTransportSensorTest, MissingPingKeyBlocksSensorData) { responder.set_encryption_key(key); sensor::Sensor local_sensor; local_sensor.state = 77.7f; + responder.set_sensor_count(1); responder.add_sensor("temp", &local_sensor); responder.send_data_(true); ASSERT_EQ(responder.sent_packets.size(), 1u); From d0e705d948c20aa68eab915ffbd5c3699916bd03 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Mar 2026 12:46:28 -1000 Subject: [PATCH 087/166] [core] Inline Application::loop() to eliminate stack frame (#15041) --- esphome/components/esp32/core.cpp | 4 +- esphome/components/socket/socket.h | 2 +- esphome/core/application.cpp | 145 +-------------------------- esphome/core/application.h | 154 ++++++++++++++++++++++++++++- 4 files changed, 156 insertions(+), 149 deletions(-) diff --git a/esphome/components/esp32/core.cpp b/esphome/components/esp32/core.cpp index 83bd09b643..313818e601 100644 --- a/esphome/components/esp32/core.cpp +++ b/esphome/components/esp32/core.cpp @@ -2,6 +2,7 @@ #include "esphome/core/defines.h" #include "crash_handler.h" +#include "esphome/core/application.h" #include "esphome/core/hal.h" #include "esphome/core/helpers.h" #include "preferences.h" @@ -15,7 +16,6 @@ #include void setup(); // NOLINT(readability-redundant-declaration) -void loop(); // NOLINT(readability-redundant-declaration) // Weak stub for initArduino - overridden when the Arduino component is present extern "C" __attribute__((weak)) void initArduino() {} @@ -65,7 +65,7 @@ TaskHandle_t loop_task_handle = nullptr; // NOLINT(cppcoreguidelines-avoid-non- void loop_task(void *pv_params) { setup(); while (true) { - loop(); + App.loop(); } } diff --git a/esphome/components/socket/socket.h b/esphome/components/socket/socket.h index a21bd64730..226a669e31 100644 --- a/esphome/components/socket/socket.h +++ b/esphome/components/socket/socket.h @@ -125,7 +125,7 @@ size_t format_sockaddr_to(const struct sockaddr *addr_ptr, socklen_t len, std::s /// On ESP8266, uses esp_delay() with a callback that checks socket activity. /// On RP2040, uses __wfe() (Wait For Event) to truly sleep until an interrupt /// (for example, CYW43 GPIO or a timer alarm) fires and wakes the CPU. -void socket_delay(uint32_t ms); +void socket_delay(uint32_t ms); // NOLINT(readability-redundant-declaration) /// Signal socket/IO activity and wake the main loop early. /// On ESP8266: sets flag + esp_schedule(). diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index 08df385475..c020a8ed58 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -12,21 +12,11 @@ #endif #ifdef USE_LWIP_FAST_SELECT #include "esphome/core/lwip_fast_select.h" -#ifdef USE_ESP32 -#include -#include -#else -#include -#include -#endif #endif // USE_LWIP_FAST_SELECT #include "esphome/core/version.h" #include "esphome/core/hal.h" #include #include -#ifdef USE_RUNTIME_STATS -#include "esphome/components/runtime_stats/runtime_stats.h" -#endif #ifdef USE_STATUS_LED #include "esphome/components/status_led/status_led.h" @@ -163,66 +153,6 @@ void Application::setup() { this->schedule_dump_config(); } -void Application::loop() { - uint8_t new_app_state = 0; - - // Get the initial loop time at the start - uint32_t last_op_end_time = millis(); - - this->before_loop_tasks_(last_op_end_time); - - for (this->current_loop_index_ = 0; this->current_loop_index_ < this->looping_components_active_end_; - this->current_loop_index_++) { - Component *component = this->looping_components_[this->current_loop_index_]; - - // Update the cached time before each component runs - this->loop_component_start_time_ = last_op_end_time; - - { - this->set_current_component(component); - WarnIfComponentBlockingGuard guard{component, last_op_end_time}; - component->loop(); - // Use the finish method to get the current time as the end time - last_op_end_time = guard.finish(); - } - new_app_state |= component->get_component_state(); - this->app_state_ |= new_app_state; - this->feed_wdt(last_op_end_time); - } - - this->after_loop_tasks_(); - this->app_state_ = new_app_state; - -#ifdef USE_RUNTIME_STATS - // Process any pending runtime stats printing after all components have run - // This ensures stats printing doesn't affect component timing measurements - if (global_runtime_stats != nullptr) { - global_runtime_stats->process_pending_stats(last_op_end_time); - } -#endif - - // Use the last component's end time instead of calling millis() again - auto elapsed = last_op_end_time - this->last_loop_; - if (elapsed >= this->loop_interval_ || HighFrequencyLoopRequester::is_high_frequency()) { - // Even if we overran the loop interval, we still need to select() - // to know if any sockets have data ready - this->yield_with_select_(0); - } else { - uint32_t delay_time = this->loop_interval_ - elapsed; - uint32_t next_schedule = this->scheduler.next_schedule_in(last_op_end_time).value_or(delay_time); - // next_schedule is max 0.5*delay_time - // otherwise interval=0 schedules result in constant looping with almost no sleep - next_schedule = std::max(next_schedule, delay_time / 2); - delay_time = std::min(next_schedule, delay_time); - - this->yield_with_select_(delay_time); - } - this->last_loop_ = last_op_end_time; - - if (this->dump_config_at_ < this->components_.size()) { - this->process_dump_config_(); - } -} void Application::process_dump_config_() { if (this->dump_config_at_ == 0) { @@ -509,41 +439,6 @@ void Application::enable_pending_loops_() { } } -void Application::before_loop_tasks_(uint32_t loop_start_time) { -#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_LWIP_FAST_SELECT) - // Drain wake notifications first to clear socket for next wake - this->drain_wake_notifications_(); -#endif - - // Process scheduled tasks - this->scheduler.call(loop_start_time); - - // Feed the watchdog timer - this->feed_wdt(loop_start_time); - - // Process any pending enable_loop requests from ISRs - // This must be done before marking in_loop_ = true to avoid race conditions - if (this->has_pending_enable_loop_requests_) { - // Clear flag BEFORE processing to avoid race condition - // If ISR sets it during processing, we'll catch it next loop iteration - // This is safe because: - // 1. Each component has its own pending_enable_loop_ flag that we check - // 2. If we can't process a component (wrong state), enable_pending_loops_() - // will set this flag back to true - // 3. Any new ISR requests during processing will set the flag again - this->has_pending_enable_loop_requests_ = false; - this->enable_pending_loops_(); - } - - // Mark that we're in the loop for safe reentrant modifications - this->in_loop_ = true; -} - -void Application::after_loop_tasks_() { - // Clear the in_loop_ flag to indicate we're done processing components - this->in_loop_ = false; -} - #ifdef USE_LWIP_FAST_SELECT bool Application::register_socket(struct lwip_sock *sock) { // It modifies monitored_sockets_ without locking — must only be called from the main loop. @@ -625,36 +520,10 @@ void Application::unregister_socket_fd(int fd) { #endif +// Only the select() fallback path remains in the .cpp — all other paths are inlined in application.h +#if defined(USE_SOCKET_SELECT_SUPPORT) && !defined(USE_LWIP_FAST_SELECT) void Application::yield_with_select_(uint32_t delay_ms) { - // Delay while monitoring sockets. When delay_ms is 0, always yield() to ensure other tasks run. -#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_LWIP_FAST_SELECT) - // Fast path (ESP32/LibreTiny): reads rcvevent directly from cached lwip_sock pointers. - // Safe because this runs on the main loop which owns socket lifetime (create, read, close). - if (delay_ms == 0) [[unlikely]] { - yield(); - return; - } - - // Check if any socket already has pending data before sleeping. - // If a socket still has unread data (rcvevent > 0) but the task notification was already - // consumed, ulTaskNotifyTake would block until timeout — adding up to delay_ms latency. - // This scan preserves select() semantics: return immediately when any fd is ready. - for (struct lwip_sock *sock : this->monitored_sockets_) { - if (esphome_lwip_socket_has_data(sock)) { - yield(); - return; - } - } - - // Sleep with instant wake via FreeRTOS task notification. - // Woken by: callback wrapper (socket data arrives), wake_loop_threadsafe() (other tasks), or timeout. - // Without USE_WAKE_LOOP_THREADSAFE, only hooked socket callbacks wake the task — - // background tasks won't call wake, so this degrades to a pure timeout (same as old select path). - ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(delay_ms)); - -#elif defined(USE_SOCKET_SELECT_SUPPORT) // Fallback select() path (host platform and any future platforms without fast select). - // ESP32 and LibreTiny are excluded by the #if above — they use the fast path. if (!this->socket_fds_.empty()) [[likely]] { // Update fd_set if socket list has changed if (this->socket_fds_changed_) [[unlikely]] { @@ -701,16 +570,8 @@ void Application::yield_with_select_(uint32_t delay_ms) { } // No sockets registered or select() failed - use regular delay delay(delay_ms); -#elif (defined(USE_ESP8266) || defined(USE_RP2040)) && defined(USE_SOCKET_IMPL_LWIP_TCP) - // No select support but can wake on socket activity - // ESP8266: via esp_schedule() - // RP2040: via __sev()/__wfe() hardware sleep/wake - socket::socket_delay(delay_ms); -#else - // No select support, use regular delay - delay(delay_ms); -#endif } +#endif // defined(USE_SOCKET_SELECT_SUPPORT) && !defined(USE_LWIP_FAST_SELECT) // App storage — asm label shares the linker symbol with "extern Application App". // char[] is trivially destructible, so no __cxa_atexit or destructor chain is emitted. diff --git a/esphome/core/application.h b/esphome/core/application.h index 26abc15433..06ff30e81f 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -27,6 +27,13 @@ #ifdef USE_SOCKET_SELECT_SUPPORT #ifdef USE_LWIP_FAST_SELECT #include "esphome/core/lwip_fast_select.h" +#ifdef USE_ESP32 +#include +#include +#else +#include +#include +#endif #else #include #ifdef USE_WAKE_LOOP_THREADSAFE @@ -34,9 +41,13 @@ #endif #endif #endif // USE_SOCKET_SELECT_SUPPORT +#ifdef USE_RUNTIME_STATS +#include "esphome/components/runtime_stats/runtime_stats.h" +#endif #if (defined(USE_ESP8266) || defined(USE_RP2040)) && defined(USE_SOCKET_IMPL_LWIP_TCP) namespace esphome::socket { -void socket_wake(); // NOLINT(readability-redundant-declaration) +void socket_wake(); // NOLINT(readability-redundant-declaration) +void socket_delay(uint32_t ms); // NOLINT(readability-redundant-declaration) } // namespace esphome::socket #endif #ifdef USE_BINARY_SENSOR @@ -293,7 +304,7 @@ class Application { void setup(); /// Make a loop iteration. Call this in your loop() function. - void loop(); + inline void ESPHOME_ALWAYS_INLINE loop(); /// Get the name of this Application set by pre_setup(). const StringRef &get_name() const { return this->name_; } @@ -617,8 +628,8 @@ class Application { void enable_component_loop_(Component *component); void enable_pending_loops_(); void activate_looping_component_(uint16_t index); - void before_loop_tasks_(uint32_t loop_start_time); - void after_loop_tasks_(); + inline void ESPHOME_ALWAYS_INLINE before_loop_tasks_(uint32_t loop_start_time); + inline void ESPHOME_ALWAYS_INLINE after_loop_tasks_() { this->in_loop_ = false; } /// Process dump_config output one component per loop iteration. /// Extracted from loop() to keep cold startup/reconnect logging out of the hot path. @@ -628,7 +639,12 @@ class Application { void feed_wdt_arch_(); /// Perform a delay while also monitoring socket file descriptors for readiness +#if defined(USE_SOCKET_SELECT_SUPPORT) && !defined(USE_LWIP_FAST_SELECT) + // select() fallback path is too complex to inline (host platform) void yield_with_select_(uint32_t delay_ms); +#else + inline void ESPHOME_ALWAYS_INLINE yield_with_select_(uint32_t delay_ms); +#endif #if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_LWIP_FAST_SELECT) void setup_wake_loop_threadsafe_(); // Create wake notification socket @@ -814,4 +830,134 @@ inline void Application::drain_wake_notifications_() { } #endif // defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_LWIP_FAST_SELECT) +inline void ESPHOME_ALWAYS_INLINE Application::before_loop_tasks_(uint32_t loop_start_time) { +#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_LWIP_FAST_SELECT) + // Drain wake notifications first to clear socket for next wake + this->drain_wake_notifications_(); +#endif + + // Process scheduled tasks + this->scheduler.call(loop_start_time); + + // Feed the watchdog timer + this->feed_wdt(loop_start_time); + + // Process any pending enable_loop requests from ISRs + // This must be done before marking in_loop_ = true to avoid race conditions + if (this->has_pending_enable_loop_requests_) { + // Clear flag BEFORE processing to avoid race condition + // If ISR sets it during processing, we'll catch it next loop iteration + // This is safe because: + // 1. Each component has its own pending_enable_loop_ flag that we check + // 2. If we can't process a component (wrong state), enable_pending_loops_() + // will set this flag back to true + // 3. Any new ISR requests during processing will set the flag again + this->has_pending_enable_loop_requests_ = false; + this->enable_pending_loops_(); + } + + // Mark that we're in the loop for safe reentrant modifications + this->in_loop_ = true; +} + +inline void ESPHOME_ALWAYS_INLINE Application::loop() { + uint8_t new_app_state = 0; + + // Get the initial loop time at the start + uint32_t last_op_end_time = millis(); + + this->before_loop_tasks_(last_op_end_time); + + for (this->current_loop_index_ = 0; this->current_loop_index_ < this->looping_components_active_end_; + this->current_loop_index_++) { + Component *component = this->looping_components_[this->current_loop_index_]; + + // Update the cached time before each component runs + this->loop_component_start_time_ = last_op_end_time; + + { + this->set_current_component(component); + WarnIfComponentBlockingGuard guard{component, last_op_end_time}; + component->loop(); + // Use the finish method to get the current time as the end time + last_op_end_time = guard.finish(); + } + new_app_state |= component->get_component_state(); + this->app_state_ |= new_app_state; + this->feed_wdt(last_op_end_time); + } + + this->after_loop_tasks_(); + this->app_state_ = new_app_state; + +#ifdef USE_RUNTIME_STATS + // Process any pending runtime stats printing after all components have run + // This ensures stats printing doesn't affect component timing measurements + if (global_runtime_stats != nullptr) { + global_runtime_stats->process_pending_stats(last_op_end_time); + } +#endif + + // Use the last component's end time instead of calling millis() again + auto elapsed = last_op_end_time - this->last_loop_; + if (elapsed >= this->loop_interval_ || HighFrequencyLoopRequester::is_high_frequency()) { + // Even if we overran the loop interval, we still need to select() + // to know if any sockets have data ready + this->yield_with_select_(0); + } else { + uint32_t delay_time = this->loop_interval_ - elapsed; + uint32_t next_schedule = this->scheduler.next_schedule_in(last_op_end_time).value_or(delay_time); + // next_schedule is max 0.5*delay_time + // otherwise interval=0 schedules result in constant looping with almost no sleep + next_schedule = std::max(next_schedule, delay_time / 2); + delay_time = std::min(next_schedule, delay_time); + + this->yield_with_select_(delay_time); + } + this->last_loop_ = last_op_end_time; + + if (this->dump_config_at_ < this->components_.size()) { + this->process_dump_config_(); + } +} + +// Inline yield_with_select_ for all paths except the select() fallback +#if !defined(USE_SOCKET_SELECT_SUPPORT) || defined(USE_LWIP_FAST_SELECT) +inline void ESPHOME_ALWAYS_INLINE Application::yield_with_select_(uint32_t delay_ms) { +#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_LWIP_FAST_SELECT) + // Fast path (ESP32/LibreTiny): reads rcvevent directly from cached lwip_sock pointers. + // Safe because this runs on the main loop which owns socket lifetime (create, read, close). + if (delay_ms == 0) [[unlikely]] { + yield(); + return; + } + + // Check if any socket already has pending data before sleeping. + // If a socket still has unread data (rcvevent > 0) but the task notification was already + // consumed, ulTaskNotifyTake would block until timeout — adding up to delay_ms latency. + // This scan preserves select() semantics: return immediately when any fd is ready. + for (struct lwip_sock *sock : this->monitored_sockets_) { + if (esphome_lwip_socket_has_data(sock)) { + yield(); + return; + } + } + + // Sleep with instant wake via FreeRTOS task notification. + // Woken by: callback wrapper (socket data arrives), wake_loop_threadsafe() (other tasks), or timeout. + // Without USE_WAKE_LOOP_THREADSAFE, only hooked socket callbacks wake the task — + // background tasks won't call wake, so this degrades to a pure timeout (same as old select path). + ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(delay_ms)); +#elif (defined(USE_ESP8266) || defined(USE_RP2040)) && defined(USE_SOCKET_IMPL_LWIP_TCP) + // No select support but can wake on socket activity + // ESP8266: via esp_schedule() + // RP2040: via __sev()/__wfe() hardware sleep/wake + socket::socket_delay(delay_ms); +#else + // No select support, use regular delay + delay(delay_ms); +#endif +} +#endif // !defined(USE_SOCKET_SELECT_SUPPORT) || defined(USE_LWIP_FAST_SELECT) + } // namespace esphome From e85065b1c4211280a49b3b473905c33c83eae337 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Mar 2026 14:06:00 -1000 Subject: [PATCH 088/166] [logger] Fix dummy_main.cpp Logger constructor for clang-tidy (#15088) Co-authored-by: Claude Opus 4.6 (1M context) --- tests/dummy_main.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/dummy_main.cpp b/tests/dummy_main.cpp index 6fa0c08aa3..329286e2fa 100644 --- a/tests/dummy_main.cpp +++ b/tests/dummy_main.cpp @@ -15,7 +15,7 @@ void setup() { static char name[] = "livingroom"; static char friendly_name[] = "LivingRoom"; App.pre_setup(name, sizeof(name) - 1, friendly_name, sizeof(friendly_name) - 1); - auto *log = new logger::Logger(115200); // NOLINT + auto *log = new logger::Logger(115200, 512); // NOLINT log->pre_setup(); log->set_uart_selection(logger::UART_SELECTION_UART0); App.register_component_(log); From 83d02c602a911181ad0e0ef89ef50fde17ef0bd7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Mar 2026 14:06:00 -1000 Subject: [PATCH 089/166] [logger] Fix dummy_main.cpp Logger constructor for clang-tidy (#15088) Co-authored-by: Claude Opus 4.6 (1M context) --- tests/dummy_main.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/dummy_main.cpp b/tests/dummy_main.cpp index 6fa0c08aa3..329286e2fa 100644 --- a/tests/dummy_main.cpp +++ b/tests/dummy_main.cpp @@ -15,7 +15,7 @@ void setup() { static char name[] = "livingroom"; static char friendly_name[] = "LivingRoom"; App.pre_setup(name, sizeof(name) - 1, friendly_name, sizeof(friendly_name) - 1); - auto *log = new logger::Logger(115200); // NOLINT + auto *log = new logger::Logger(115200, 512); // NOLINT log->pre_setup(); log->set_uart_selection(logger::UART_SELECTION_UART0); App.register_component_(log); From cd05462e9fc4a295b523412c04fa17acb6c86171 Mon Sep 17 00:00:00 2001 From: Kamil Cukrowski Date: Mon, 23 Mar 2026 01:42:04 +0100 Subject: [PATCH 090/166] [core] Use placement new allocation for Pvariables (#15079) Co-authored-by: J. Nick Koston Co-authored-by: J. Nick Koston --- esphome/cpp_generator.py | 48 +++++++++++++++---- .../binary_sensor/test_binary_sensor.py | 2 +- tests/component_tests/button/test_button.py | 3 +- tests/component_tests/conftest.py | 2 +- .../deep_sleep/test_deep_sleep.py | 6 ++- tests/component_tests/globals/__init__.py | 0 .../globals/config/globals_test.yaml | 16 +++++++ tests/component_tests/globals/test_globals.py | 27 +++++++++++ .../gpio/test_gpio_binary_sensor.py | 3 +- tests/component_tests/image/test_init.py | 10 +++- tests/component_tests/logger/test_logger.py | 4 ++ .../mipi_dsi/test_mipi_dsi_config.py | 10 +++- tests/component_tests/status_led/__init__.py | 0 .../status_led/config/status_led_test.yaml | 8 ++++ .../status_led/test_status_led.py | 23 +++++++++ tests/component_tests/text/test_text.py | 3 +- .../text_sensor/test_text_sensor.py | 3 +- 17 files changed, 151 insertions(+), 17 deletions(-) create mode 100644 tests/component_tests/globals/__init__.py create mode 100644 tests/component_tests/globals/config/globals_test.yaml create mode 100644 tests/component_tests/globals/test_globals.py create mode 100644 tests/component_tests/status_led/__init__.py create mode 100644 tests/component_tests/status_led/config/status_led_test.yaml create mode 100644 tests/component_tests/status_led/test_status_led.py diff --git a/esphome/cpp_generator.py b/esphome/cpp_generator.py index 5457485d25..3ed5d0ba37 100644 --- a/esphome/cpp_generator.py +++ b/esphome/cpp_generator.py @@ -579,10 +579,41 @@ def Pvariable(id_: ID, rhs: SafeExpType, type_: "MockObj" = None) -> "MockObj": obj = MockObj(id_, "->") if type_ is not None: id_.type = type_ - decl = VariableDeclarationExpression(id_.type, "*", id_, static=True) - CORE.add_global(decl) - assignment = AssignmentExpression(None, None, id_, rhs) - CORE.add(assignment) + + if isinstance(rhs, MockObj) and rhs.is_new_expr: + # For 'new' allocations, use placement new into static storage + # to avoid heap fragmentation on embedded devices. + the_type = id_.type + storage_name = f"{id_.id}__pstorage" + + # Declare aligned byte array for the object storage + CORE.add_global( + RawStatement( + f"alignas({the_type}) static unsigned char {storage_name}[sizeof({the_type})];" + ) + ) + CORE.add_global( + AssignmentExpression( + f"static {the_type}", + "*const ", + id_, + MockObj(f"reinterpret_cast<{the_type} *>({storage_name})"), + ) + ) + # Extract args from the CallExpression and rebuild as placement new. + # Template args are already encoded in the_type (e.g. GlobalsComponent), + # so we only pass the constructor args, not template_args. + call_expr = rhs.base + assert isinstance(call_expr, CallExpression), ( + f"Expected CallExpression for placement new, got {type(call_expr)}" + ) + placement_new = CallExpression(f"new({id_.id}) {the_type}", *call_expr.args) + CORE.add(ExpressionStatement(placement_new)) + else: + decl = VariableDeclarationExpression(id_.type, "*", id_, static=True) + CORE.add_global(decl) + CORE.add(AssignmentExpression(None, None, id_, rhs)) + CORE.register_variable(id_, obj) return obj @@ -799,11 +830,12 @@ class MockObj(Expression): Mostly consists of magic methods that allow ESPHome's codegen syntax. """ - __slots__ = ("base", "op") + __slots__ = ("base", "op", "is_new_expr") - def __init__(self, base, op="."): + def __init__(self, base, op=".", is_new_expr=False) -> None: self.base = base self.op = op + self.is_new_expr = is_new_expr def __getattr__(self, attr: str) -> "MockObj": # prevent python dunder methods being replaced by mock objects @@ -818,7 +850,7 @@ class MockObj(Expression): def __call__(self, *args: SafeExpType) -> "MockObj": call = CallExpression(self.base, *args) - return MockObj(call, self.op) + return MockObj(call, self.op, is_new_expr=self.is_new_expr) def __str__(self): return str(self.base) @@ -832,7 +864,7 @@ class MockObj(Expression): @property def new(self) -> "MockObj": - return MockObj(f"new {self.base}", "->") + return MockObj(f"new {self.base}", "->", is_new_expr=True) def template(self, *args: SafeExpType) -> "MockObj": """Apply template parameters to this object.""" diff --git a/tests/component_tests/binary_sensor/test_binary_sensor.py b/tests/component_tests/binary_sensor/test_binary_sensor.py index 10d7f80834..4f41f2cc70 100644 --- a/tests/component_tests/binary_sensor/test_binary_sensor.py +++ b/tests/component_tests/binary_sensor/test_binary_sensor.py @@ -15,7 +15,7 @@ def test_binary_sensor_is_setup(generate_main): ) # Then - assert "new gpio::GPIOBinarySensor();" in main_cpp + assert "static gpio::GPIOBinarySensor *const" in main_cpp assert "App.register_binary_sensor" in main_cpp diff --git a/tests/component_tests/button/test_button.py b/tests/component_tests/button/test_button.py index a35994a682..544e748f91 100644 --- a/tests/component_tests/button/test_button.py +++ b/tests/component_tests/button/test_button.py @@ -13,7 +13,8 @@ def test_button_is_setup(generate_main): main_cpp = generate_main("tests/component_tests/button/test_button.yaml") # Then - assert "new wake_on_lan::WakeOnLanButton();" in main_cpp + assert "static wake_on_lan::WakeOnLanButton *const" in main_cpp + assert ") wake_on_lan::WakeOnLanButton();" in main_cpp assert "App.register_button" in main_cpp assert "App.register_component" in main_cpp diff --git a/tests/component_tests/conftest.py b/tests/component_tests/conftest.py index 0641e698e9..763628f57c 100644 --- a/tests/component_tests/conftest.py +++ b/tests/component_tests/conftest.py @@ -134,7 +134,7 @@ def generate_main() -> Generator[Callable[[str | Path], str]]: CORE.config_path = Path(path) CORE.config = read_config({}) generate_cpp_contents(CORE.config) - return CORE.cpp_main_section + return CORE.cpp_global_section + CORE.cpp_main_section yield generator diff --git a/tests/component_tests/deep_sleep/test_deep_sleep.py b/tests/component_tests/deep_sleep/test_deep_sleep.py index 41ddd72feb..212f61e44b 100644 --- a/tests/component_tests/deep_sleep/test_deep_sleep.py +++ b/tests/component_tests/deep_sleep/test_deep_sleep.py @@ -7,7 +7,11 @@ def test_deep_sleep_setup(generate_main): """ main_cpp = generate_main("tests/component_tests/deep_sleep/test_deep_sleep1.yaml") - assert "deepsleep = new deep_sleep::DeepSleepComponent();" in main_cpp + assert ( + "static deep_sleep::DeepSleepComponent *const deepsleep = reinterpret_cast(deepsleep__pstorage);" + in main_cpp + ) + assert "new(deepsleep) deep_sleep::DeepSleepComponent();" in main_cpp assert "App.register_component_(deepsleep);" in main_cpp diff --git a/tests/component_tests/globals/__init__.py b/tests/component_tests/globals/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/component_tests/globals/config/globals_test.yaml b/tests/component_tests/globals/config/globals_test.yaml new file mode 100644 index 0000000000..1d1a9edaa6 --- /dev/null +++ b/tests/component_tests/globals/config/globals_test.yaml @@ -0,0 +1,16 @@ +esphome: + name: test + +esp32: + board: esp32dev + +globals: + - id: my_global_int + type: int + initial_value: "42" + - id: my_global_float + type: float + initial_value: "1.5" + - id: my_global_bool + type: bool + initial_value: "true" diff --git a/tests/component_tests/globals/test_globals.py b/tests/component_tests/globals/test_globals.py new file mode 100644 index 0000000000..04fd6d5f7d --- /dev/null +++ b/tests/component_tests/globals/test_globals.py @@ -0,0 +1,27 @@ +"""Tests for the globals component.""" + +from __future__ import annotations + +from collections.abc import Callable +from pathlib import Path + + +def test_globals_placement_new_with_template_args( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], +) -> None: + """Test that globals uses placement new with template arguments preserved.""" + main_cpp = generate_main(component_config_path("globals_test.yaml")) + + # Globals uses Pvariable with Type.new(template_args, initial_value) + # which exercises the template_args preservation in placement new. + assert "static globals::GlobalsComponent *const my_global_int" in main_cpp + assert "sizeof(globals::GlobalsComponent)" in main_cpp + assert "new(my_global_int) globals::GlobalsComponent" in main_cpp + + # Verify initial value is passed as constructor arg + assert "42" in main_cpp + + # Check other globals are also generated + assert "sizeof(globals::GlobalsComponent)" in main_cpp + assert "sizeof(globals::GlobalsComponent)" in main_cpp diff --git a/tests/component_tests/gpio/test_gpio_binary_sensor.py b/tests/component_tests/gpio/test_gpio_binary_sensor.py index 73665dc45d..f336a9105e 100644 --- a/tests/component_tests/gpio/test_gpio_binary_sensor.py +++ b/tests/component_tests/gpio/test_gpio_binary_sensor.py @@ -16,7 +16,8 @@ def test_gpio_binary_sensor_basic_setup( """ main_cpp = generate_main("tests/component_tests/gpio/test_gpio_binary_sensor.yaml") - assert "new gpio::GPIOBinarySensor();" in main_cpp + assert "static gpio::GPIOBinarySensor *const" in main_cpp + assert ") gpio::GPIOBinarySensor();" in main_cpp assert "App.register_binary_sensor" in main_cpp # set_use_interrupt(true) should NOT be generated (uses C++ default) assert "bs_gpio->set_use_interrupt(true);" not in main_cpp diff --git a/tests/component_tests/image/test_init.py b/tests/component_tests/image/test_init.py index c9481a0e1d..9003a4ee5d 100644 --- a/tests/component_tests/image/test_init.py +++ b/tests/component_tests/image/test_init.py @@ -242,7 +242,15 @@ def test_image_generation( main_cpp = generate_main(component_config_path("image_test.yaml")) assert "uint8_t_id[] PROGMEM = {0x24, 0x21, 0x24, 0x21" in main_cpp assert ( - "cat_img = new image::Image(uint8_t_id, 32, 24, image::IMAGE_TYPE_RGB565, image::TRANSPARENCY_OPAQUE);" + "alignas(image::Image) static unsigned char cat_img__pstorage[sizeof(image::Image)];" + in main_cpp + ) + assert ( + "static image::Image *const cat_img = reinterpret_cast(cat_img__pstorage);" + in main_cpp + ) + assert ( + "new(cat_img) image::Image(uint8_t_id, 32, 24, image::IMAGE_TYPE_RGB565, image::TRANSPARENCY_OPAQUE);" in main_cpp ) diff --git a/tests/component_tests/logger/test_logger.py b/tests/component_tests/logger/test_logger.py index 98aa741964..94a6f7ac7b 100644 --- a/tests/component_tests/logger/test_logger.py +++ b/tests/component_tests/logger/test_logger.py @@ -22,6 +22,10 @@ def test_logger_pre_setup_before_other_components(generate_main): # Find all "new " allocations (component creation) new_allocations = list(re.finditer(r"\bnew [\w:]+", main_cpp)) + # Find all "new(" allocations (component creation) and combine them + new_allocations.extend(re.finditer(r"\bnew\([^)]+\) [\w:]+", main_cpp)) + # Sort allocations by position in the file + new_allocations.sort(key=lambda m: m.start()) assert len(new_allocations) > 0, "No component allocations found" # Separate logger and non-logger allocations diff --git a/tests/component_tests/mipi_dsi/test_mipi_dsi_config.py b/tests/component_tests/mipi_dsi/test_mipi_dsi_config.py index 92f56b5451..e6f344b086 100644 --- a/tests/component_tests/mipi_dsi/test_mipi_dsi_config.py +++ b/tests/component_tests/mipi_dsi/test_mipi_dsi_config.py @@ -119,7 +119,15 @@ def test_code_generation( main_cpp = generate_main(component_fixture_path("mipi_dsi.yaml")) assert ( - "p4_nano = new mipi_dsi::MIPI_DSI(800, 1280, display::COLOR_BITNESS_565, 16);" + "alignas(mipi_dsi::MIPI_DSI) static unsigned char p4_nano__pstorage[sizeof(mipi_dsi::MIPI_DSI)];" + in main_cpp + ) + assert ( + "static mipi_dsi::MIPI_DSI *const p4_nano = reinterpret_cast(p4_nano__pstorage);" + in main_cpp + ) + assert ( + "new(p4_nano) mipi_dsi::MIPI_DSI(800, 1280, display::COLOR_BITNESS_565, 16);" in main_cpp ) assert "set_init_sequence({224, 1, 0, 225, 1, 147, 226, 1," in main_cpp diff --git a/tests/component_tests/status_led/__init__.py b/tests/component_tests/status_led/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/component_tests/status_led/config/status_led_test.yaml b/tests/component_tests/status_led/config/status_led_test.yaml new file mode 100644 index 0000000000..c86197d225 --- /dev/null +++ b/tests/component_tests/status_led/config/status_led_test.yaml @@ -0,0 +1,8 @@ +esphome: + name: test + +esp32: + board: esp32dev + +status_led: + pin: GPIO2 diff --git a/tests/component_tests/status_led/test_status_led.py b/tests/component_tests/status_led/test_status_led.py new file mode 100644 index 0000000000..0e96e631f5 --- /dev/null +++ b/tests/component_tests/status_led/test_status_led.py @@ -0,0 +1,23 @@ +"""Tests for status_led.""" + +from __future__ import annotations + +from collections.abc import Callable +from pathlib import Path + + +def test_status_led_generation( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], +) -> None: + """Test status_led generation.""" + main_cpp = generate_main(component_config_path("status_led_test.yaml")) + assert ( + "alignas(status_led::StatusLED) static unsigned char status_led_statusled_id__pstorage[sizeof(status_led::StatusLED)];" + in main_cpp + ) + assert ( + "static status_led::StatusLED *const status_led_statusled_id = reinterpret_cast(status_led_statusled_id__pstorage);" + in main_cpp + ) + assert "new(status_led_statusled_id) status_led::StatusLED(" in main_cpp diff --git a/tests/component_tests/text/test_text.py b/tests/component_tests/text/test_text.py index c74dfb8a47..63eb4f1951 100644 --- a/tests/component_tests/text/test_text.py +++ b/tests/component_tests/text/test_text.py @@ -13,7 +13,8 @@ def test_text_is_setup(generate_main): main_cpp = generate_main("tests/component_tests/text/test_text.yaml") # Then - assert "new template_::TemplateText();" in main_cpp + assert "static template_::TemplateText *const" in main_cpp + assert ") template_::TemplateText();" in main_cpp assert "App.register_text" in main_cpp diff --git a/tests/component_tests/text_sensor/test_text_sensor.py b/tests/component_tests/text_sensor/test_text_sensor.py index 1ff31ab96b..ae094fadf8 100644 --- a/tests/component_tests/text_sensor/test_text_sensor.py +++ b/tests/component_tests/text_sensor/test_text_sensor.py @@ -13,7 +13,8 @@ def test_text_sensor_is_setup(generate_main): main_cpp = generate_main("tests/component_tests/text_sensor/test_text_sensor.yaml") # Then - assert "new template_::TemplateTextSensor();" in main_cpp + assert "static template_::TemplateTextSensor *const" in main_cpp + assert ") template_::TemplateTextSensor();" in main_cpp assert "App.register_text_sensor" in main_cpp From 9cdc17566a9d9b96ff37943cb815e4d66703e47e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Mar 2026 15:06:45 -1000 Subject: [PATCH 091/166] [combination] Use FixedVector and parent pointer to enable inline Callback storage (#14947) --- .../components/combination/combination.cpp | 34 +++++++++---------- esphome/components/combination/combination.h | 18 +++++++--- esphome/components/combination/sensor.py | 3 ++ 3 files changed, 33 insertions(+), 22 deletions(-) diff --git a/esphome/components/combination/combination.cpp b/esphome/components/combination/combination.cpp index 2f0bd26a02..b858eee4ee 100644 --- a/esphome/components/combination/combination.cpp +++ b/esphome/components/combination/combination.cpp @@ -4,8 +4,6 @@ #include "esphome/core/hal.h" #include -#include -#include namespace esphome { namespace combination { @@ -20,12 +18,12 @@ void CombinationComponent::log_config_(const LogString *combo_type) { void CombinationNoParameterComponent::add_source(Sensor *sensor) { this->sensors_.emplace_back(sensor); } -void CombinationOneParameterComponent::add_source(Sensor *sensor, std::function const &stddev) { - this->sensor_pairs_.emplace_back(sensor, stddev); +void CombinationOneParameterComponent::add_source(Sensor *sensor, std::function const &compute) { + this->sensor_sources_.push_back({sensor, compute, this}); } -void CombinationOneParameterComponent::add_source(Sensor *sensor, float stddev) { - this->add_source(sensor, std::function{[stddev](float x) -> float { return stddev; }}); +void CombinationOneParameterComponent::add_source(Sensor *sensor, float value) { + this->add_source(sensor, std::function{[value](float x) -> float { return value; }}); } void CombinationNoParameterComponent::log_source_sensors() { @@ -37,9 +35,8 @@ void CombinationNoParameterComponent::log_source_sensors() { void CombinationOneParameterComponent::log_source_sensors() { ESP_LOGCONFIG(TAG, " Source Sensors:"); - for (const auto &sensor : this->sensor_pairs_) { - auto &entity = *sensor.first; - ESP_LOGCONFIG(TAG, " - %s", entity.get_name().c_str()); + for (const auto &source : this->sensor_sources_) { + ESP_LOGCONFIG(TAG, " - %s", source.sensor->get_name().c_str()); } } @@ -62,9 +59,12 @@ void KalmanCombinationComponent::dump_config() { } void KalmanCombinationComponent::setup() { - for (const auto &sensor : this->sensor_pairs_) { - const auto stddev = sensor.second; - sensor.first->add_on_state_callback([this, stddev](float x) -> void { this->correct_(x, stddev(x)); }); + for (auto &source : this->sensor_sources_) { + // [&source] is safe: source refers to a FixedVector element that never reallocates, + // so the reference remains valid for the component's lifetime. + source.sensor->add_on_state_callback([&source](float x) -> void { + static_cast(source.parent)->correct_(x, source.compute(x)); + }); } } @@ -117,10 +117,10 @@ void KalmanCombinationComponent::correct_(float value, float stddev) { } void LinearCombinationComponent::setup() { - for (const auto &sensor : this->sensor_pairs_) { + for (auto &source : this->sensor_sources_) { // All sensor updates are deferred until the next loop. This avoids publishing the combined sensor's result // repeatedly in the same loop if multiple source senors update. - sensor.first->add_on_state_callback( + source.sensor->add_on_state_callback( [this](float value) -> void { this->defer("update", [this, value]() { this->handle_new_value(value); }); }); } } @@ -133,10 +133,10 @@ void LinearCombinationComponent::handle_new_value(float value) { float sum = 0.0; - for (const auto &sensor : this->sensor_pairs_) { - const float sensor_state = sensor.first->state; + for (const auto &source : this->sensor_sources_) { + const float sensor_state = source.sensor->state; if (std::isfinite(sensor_state)) { - sum += sensor_state * sensor.second(sensor_state); + sum += sensor_state * source.compute(sensor_state); } } diff --git a/esphome/components/combination/combination.h b/esphome/components/combination/combination.h index fb5e156da9..463eedc564 100644 --- a/esphome/components/combination/combination.h +++ b/esphome/components/combination/combination.h @@ -1,9 +1,10 @@ #pragma once #include "esphome/core/component.h" +#include "esphome/core/helpers.h" #include "esphome/components/sensor/sensor.h" -#include +#include namespace esphome { namespace combination { @@ -41,14 +42,21 @@ class CombinationNoParameterComponent : public CombinationComponent { // Base class for opertions that require one parameter to compute the combination class CombinationOneParameterComponent : public CombinationComponent { public: - void add_source(Sensor *sensor, std::function const &stddev); - void add_source(Sensor *sensor, float stddev); + void set_source_count(size_t count) { this->sensor_sources_.init(count); } + void add_source(Sensor *sensor, std::function const &compute); + void add_source(Sensor *sensor, float value); - /// @brief Logs all source sensor's names in sensor_pairs_ + /// @brief Logs all source sensors' names in sensor_sources_ void log_source_sensors() override; protected: - std::vector>> sensor_pairs_; + struct SensorSource { + sensor::Sensor *sensor; + std::function compute; + CombinationOneParameterComponent *parent; + }; + + FixedVector sensor_sources_; }; class KalmanCombinationComponent : public CombinationOneParameterComponent { diff --git a/esphome/components/combination/sensor.py b/esphome/components/combination/sensor.py index 0204162e8d..327cedee1e 100644 --- a/esphome/components/combination/sensor.py +++ b/esphome/components/combination/sensor.py @@ -180,6 +180,9 @@ async def to_code(config): if proces_std_dev := config.get(CONF_PROCESS_STD_DEV): cg.add(var.set_process_std_dev(proces_std_dev)) + if config[CONF_TYPE] in (CONF_KALMAN, CONF_LINEAR): + cg.add(var.set_source_count(len(config[CONF_SOURCES]))) + for source_conf in config[CONF_SOURCES]: source = await cg.get_variable(source_conf[CONF_SOURCE]) if config[CONF_TYPE] == CONF_KALMAN: From fbe3e7d99c59da1bdbef279cd99501707b2945eb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Mar 2026 15:28:46 -1000 Subject: [PATCH 092/166] [api] Emit raw tag+value writes for forced fixed32 key fields (#15051) --- esphome/components/api/api.proto | 142 ++++++++++---------- esphome/components/api/api_pb2.cpp | 200 ++++++++++++++-------------- esphome/components/api/proto.h | 18 ++- script/api_protobuf/api_protobuf.py | 17 ++- 4 files changed, 202 insertions(+), 175 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 28332d67a5..86daa9a2bf 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -316,7 +316,7 @@ message ListEntitiesBinarySensorResponse { option (ifdef) = "USE_BINARY_SENSOR"; string object_id = 1; - fixed32 key = 2; + fixed32 key = 2 [(force) = true]; string name = 3; reserved 4; // Deprecated: was string unique_id @@ -334,7 +334,7 @@ message BinarySensorStateResponse { option (ifdef) = "USE_BINARY_SENSOR"; option (no_delay) = true; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; bool state = 2; // If the binary sensor does not have a valid state yet. // Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller @@ -350,7 +350,7 @@ message ListEntitiesCoverResponse { option (ifdef) = "USE_COVER"; string object_id = 1; - fixed32 key = 2; + fixed32 key = 2 [(force) = true]; string name = 3; reserved 4; // Deprecated: was string unique_id @@ -383,7 +383,7 @@ message CoverStateResponse { option (ifdef) = "USE_COVER"; option (no_delay) = true; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; // legacy: state has been removed in 1.13 // clients/servers must still send/accept it until the next protocol change // Deprecated in API version 1.1 @@ -409,7 +409,7 @@ message CoverCommandRequest { option (no_delay) = true; option (base_class) = "CommandProtoMessage"; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; // legacy: command has been removed in 1.13 // clients/servers must still send/accept it until the next protocol change @@ -434,7 +434,7 @@ message ListEntitiesFanResponse { option (ifdef) = "USE_FAN"; string object_id = 1; - fixed32 key = 2; + fixed32 key = 2 [(force) = true]; string name = 3; reserved 4; // Deprecated: was string unique_id @@ -466,7 +466,7 @@ message FanStateResponse { option (ifdef) = "USE_FAN"; option (no_delay) = true; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; bool state = 2; bool oscillating = 3; // Deprecated in API version 1.6 @@ -483,7 +483,7 @@ message FanCommandRequest { option (no_delay) = true; option (base_class) = "CommandProtoMessage"; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; bool has_state = 2; bool state = 3; // Deprecated in API version 1.6 @@ -522,7 +522,7 @@ message ListEntitiesLightResponse { option (ifdef) = "USE_LIGHT"; string object_id = 1; - fixed32 key = 2; + fixed32 key = 2 [(force) = true]; string name = 3; reserved 4; // Deprecated: was string unique_id @@ -551,7 +551,7 @@ message LightStateResponse { option (ifdef) = "USE_LIGHT"; option (no_delay) = true; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; bool state = 2; float brightness = 3; ColorMode color_mode = 11; @@ -573,7 +573,7 @@ message LightCommandRequest { option (no_delay) = true; option (base_class) = "CommandProtoMessage"; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; bool has_state = 2; bool state = 3; bool has_brightness = 4; @@ -627,7 +627,7 @@ message ListEntitiesSensorResponse { option (ifdef) = "USE_SENSOR"; string object_id = 1; - fixed32 key = 2; + fixed32 key = 2 [(force) = true]; string name = 3; reserved 4; // Deprecated: was string unique_id @@ -651,7 +651,7 @@ message SensorStateResponse { option (ifdef) = "USE_SENSOR"; option (no_delay) = true; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; float state = 2; // If the sensor does not have a valid state yet. // Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller @@ -667,7 +667,7 @@ message ListEntitiesSwitchResponse { option (ifdef) = "USE_SWITCH"; string object_id = 1; - fixed32 key = 2; + fixed32 key = 2 [(force) = true]; string name = 3; reserved 4; // Deprecated: was string unique_id @@ -685,7 +685,7 @@ message SwitchStateResponse { option (ifdef) = "USE_SWITCH"; option (no_delay) = true; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; bool state = 2; uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"]; } @@ -696,7 +696,7 @@ message SwitchCommandRequest { option (no_delay) = true; option (base_class) = "CommandProtoMessage"; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; bool state = 2; uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"]; } @@ -709,7 +709,7 @@ message ListEntitiesTextSensorResponse { option (ifdef) = "USE_TEXT_SENSOR"; string object_id = 1; - fixed32 key = 2; + fixed32 key = 2 [(force) = true]; string name = 3; reserved 4; // Deprecated: was string unique_id @@ -726,7 +726,7 @@ message TextSensorStateResponse { option (ifdef) = "USE_TEXT_SENSOR"; option (no_delay) = true; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; string state = 2; // If the text sensor does not have a valid state yet. // Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller @@ -922,7 +922,7 @@ message ListEntitiesServicesResponse { option (ifdef) = "USE_API_USER_DEFINED_ACTIONS"; string name = 1; - fixed32 key = 2; + fixed32 key = 2 [(force) = true]; repeated ListEntitiesServicesArgument args = 3 [(fixed_vector) = true]; SupportsResponseType supports_response = 4; } @@ -945,7 +945,7 @@ message ExecuteServiceRequest { option (no_delay) = true; option (ifdef) = "USE_API_USER_DEFINED_ACTIONS"; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; repeated ExecuteServiceArgument args = 2 [(fixed_vector) = true]; uint32 call_id = 3 [(field_ifdef) = "USE_API_USER_DEFINED_ACTION_RESPONSES"]; bool return_response = 4 [(field_ifdef) = "USE_API_USER_DEFINED_ACTION_RESPONSES"]; @@ -972,7 +972,7 @@ message ListEntitiesCameraResponse { option (ifdef) = "USE_CAMERA"; string object_id = 1; - fixed32 key = 2; + fixed32 key = 2 [(force) = true]; string name = 3; reserved 4; // Deprecated: was string unique_id bool disabled_by_default = 5; @@ -987,7 +987,7 @@ message CameraImageResponse { option (source) = SOURCE_SERVER; option (ifdef) = "USE_CAMERA"; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; bytes data = 2; bool done = 3; uint32 device_id = 4 [(field_ifdef) = "USE_DEVICES"]; @@ -1057,7 +1057,7 @@ message ListEntitiesClimateResponse { option (ifdef) = "USE_CLIMATE"; string object_id = 1; - fixed32 key = 2; + fixed32 key = 2 [(force) = true]; string name = 3; reserved 4; // Deprecated: was string unique_id @@ -1095,7 +1095,7 @@ message ClimateStateResponse { option (ifdef) = "USE_CLIMATE"; option (no_delay) = true; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; ClimateMode mode = 2; float current_temperature = 3; float target_temperature = 4; @@ -1121,7 +1121,7 @@ message ClimateCommandRequest { option (no_delay) = true; option (base_class) = "CommandProtoMessage"; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; bool has_mode = 2; ClimateMode mode = 3; bool has_target_temperature = 4; @@ -1168,7 +1168,7 @@ message ListEntitiesWaterHeaterResponse { option (ifdef) = "USE_WATER_HEATER"; string object_id = 1; - fixed32 key = 2; + fixed32 key = 2 [(force) = true]; string name = 3; string icon = 4 [(field_ifdef) = "USE_ENTITY_ICON"]; bool disabled_by_default = 5; @@ -1189,7 +1189,7 @@ message WaterHeaterStateResponse { option (ifdef) = "USE_WATER_HEATER"; option (no_delay) = true; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; float current_temperature = 2; float target_temperature = 3; WaterHeaterMode mode = 4; @@ -1219,7 +1219,7 @@ message WaterHeaterCommandRequest { option (no_delay) = true; option (base_class) = "CommandProtoMessage"; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; // Bitmask of which fields are set (see WaterHeaterCommandHasField) uint32 has_fields = 2; WaterHeaterMode mode = 3; @@ -1244,7 +1244,7 @@ message ListEntitiesNumberResponse { option (ifdef) = "USE_NUMBER"; string object_id = 1; - fixed32 key = 2; + fixed32 key = 2 [(force) = true]; string name = 3; reserved 4; // Deprecated: was string unique_id @@ -1266,7 +1266,7 @@ message NumberStateResponse { option (ifdef) = "USE_NUMBER"; option (no_delay) = true; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; float state = 2; // If the number does not have a valid state yet. // Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller @@ -1280,7 +1280,7 @@ message NumberCommandRequest { option (no_delay) = true; option (base_class) = "CommandProtoMessage"; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; float state = 2; uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"]; } @@ -1293,7 +1293,7 @@ message ListEntitiesSelectResponse { option (ifdef) = "USE_SELECT"; string object_id = 1; - fixed32 key = 2; + fixed32 key = 2 [(force) = true]; string name = 3; reserved 4; // Deprecated: was string unique_id @@ -1310,7 +1310,7 @@ message SelectStateResponse { option (ifdef) = "USE_SELECT"; option (no_delay) = true; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; string state = 2; // If the select does not have a valid state yet. // Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller @@ -1324,7 +1324,7 @@ message SelectCommandRequest { option (no_delay) = true; option (base_class) = "CommandProtoMessage"; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; string state = 2; uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"]; } @@ -1337,7 +1337,7 @@ message ListEntitiesSirenResponse { option (ifdef) = "USE_SIREN"; string object_id = 1; - fixed32 key = 2; + fixed32 key = 2 [(force) = true]; string name = 3; reserved 4; // Deprecated: was string unique_id @@ -1356,7 +1356,7 @@ message SirenStateResponse { option (ifdef) = "USE_SIREN"; option (no_delay) = true; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; bool state = 2; uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"]; } @@ -1367,7 +1367,7 @@ message SirenCommandRequest { option (no_delay) = true; option (base_class) = "CommandProtoMessage"; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; bool has_state = 2; bool state = 3; bool has_tone = 4; @@ -1400,7 +1400,7 @@ message ListEntitiesLockResponse { option (ifdef) = "USE_LOCK"; string object_id = 1; - fixed32 key = 2; + fixed32 key = 2 [(force) = true]; string name = 3; reserved 4; // Deprecated: was string unique_id @@ -1422,7 +1422,7 @@ message LockStateResponse { option (source) = SOURCE_SERVER; option (ifdef) = "USE_LOCK"; option (no_delay) = true; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; LockState state = 2; uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"]; } @@ -1432,7 +1432,7 @@ message LockCommandRequest { option (ifdef) = "USE_LOCK"; option (no_delay) = true; option (base_class) = "CommandProtoMessage"; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; LockCommand command = 2; // Not yet implemented: @@ -1449,7 +1449,7 @@ message ListEntitiesButtonResponse { option (ifdef) = "USE_BUTTON"; string object_id = 1; - fixed32 key = 2; + fixed32 key = 2 [(force) = true]; string name = 3; reserved 4; // Deprecated: was string unique_id @@ -1466,7 +1466,7 @@ message ButtonCommandRequest { option (no_delay) = true; option (base_class) = "CommandProtoMessage"; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; uint32 device_id = 2 [(field_ifdef) = "USE_DEVICES"]; } @@ -1516,7 +1516,7 @@ message ListEntitiesMediaPlayerResponse { option (ifdef) = "USE_MEDIA_PLAYER"; string object_id = 1; - fixed32 key = 2; + fixed32 key = 2 [(force) = true]; string name = 3; reserved 4; // Deprecated: was string unique_id @@ -1538,7 +1538,7 @@ message MediaPlayerStateResponse { option (source) = SOURCE_SERVER; option (ifdef) = "USE_MEDIA_PLAYER"; option (no_delay) = true; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; MediaPlayerState state = 2; float volume = 3; bool muted = 4; @@ -1551,7 +1551,7 @@ message MediaPlayerCommandRequest { option (no_delay) = true; option (base_class) = "CommandProtoMessage"; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; bool has_command = 2; MediaPlayerCommand command = 3; @@ -2104,7 +2104,7 @@ message ListEntitiesAlarmControlPanelResponse { option (ifdef) = "USE_ALARM_CONTROL_PANEL"; string object_id = 1; - fixed32 key = 2; + fixed32 key = 2 [(force) = true]; string name = 3; reserved 4; // Deprecated: was string unique_id string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; @@ -2122,7 +2122,7 @@ message AlarmControlPanelStateResponse { option (source) = SOURCE_SERVER; option (ifdef) = "USE_ALARM_CONTROL_PANEL"; option (no_delay) = true; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; AlarmControlPanelState state = 2; uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"]; } @@ -2133,7 +2133,7 @@ message AlarmControlPanelCommandRequest { option (ifdef) = "USE_ALARM_CONTROL_PANEL"; option (no_delay) = true; option (base_class) = "CommandProtoMessage"; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; AlarmControlPanelStateCommand command = 2; string code = 3; uint32 device_id = 4 [(field_ifdef) = "USE_DEVICES"]; @@ -2151,7 +2151,7 @@ message ListEntitiesTextResponse { option (ifdef) = "USE_TEXT"; string object_id = 1; - fixed32 key = 2; + fixed32 key = 2 [(force) = true]; string name = 3; reserved 4; // Deprecated: was string unique_id string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; @@ -2171,7 +2171,7 @@ message TextStateResponse { option (ifdef) = "USE_TEXT"; option (no_delay) = true; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; string state = 2; // If the Text does not have a valid state yet. // Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller @@ -2185,7 +2185,7 @@ message TextCommandRequest { option (no_delay) = true; option (base_class) = "CommandProtoMessage"; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; string state = 2; uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"]; } @@ -2199,7 +2199,7 @@ message ListEntitiesDateResponse { option (ifdef) = "USE_DATETIME_DATE"; string object_id = 1; - fixed32 key = 2; + fixed32 key = 2 [(force) = true]; string name = 3; reserved 4; // Deprecated: was string unique_id @@ -2215,7 +2215,7 @@ message DateStateResponse { option (ifdef) = "USE_DATETIME_DATE"; option (no_delay) = true; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; // If the date does not have a valid state yet. // Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller bool missing_state = 2; @@ -2231,7 +2231,7 @@ message DateCommandRequest { option (no_delay) = true; option (base_class) = "CommandProtoMessage"; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; uint32 year = 2; uint32 month = 3; uint32 day = 4; @@ -2246,7 +2246,7 @@ message ListEntitiesTimeResponse { option (ifdef) = "USE_DATETIME_TIME"; string object_id = 1; - fixed32 key = 2; + fixed32 key = 2 [(force) = true]; string name = 3; reserved 4; // Deprecated: was string unique_id @@ -2262,7 +2262,7 @@ message TimeStateResponse { option (ifdef) = "USE_DATETIME_TIME"; option (no_delay) = true; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; // If the time does not have a valid state yet. // Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller bool missing_state = 2; @@ -2278,7 +2278,7 @@ message TimeCommandRequest { option (no_delay) = true; option (base_class) = "CommandProtoMessage"; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; uint32 hour = 2; uint32 minute = 3; uint32 second = 4; @@ -2293,7 +2293,7 @@ message ListEntitiesEventResponse { option (ifdef) = "USE_EVENT"; string object_id = 1; - fixed32 key = 2; + fixed32 key = 2 [(force) = true]; string name = 3; reserved 4; // Deprecated: was string unique_id @@ -2311,7 +2311,7 @@ message EventResponse { option (source) = SOURCE_SERVER; option (ifdef) = "USE_EVENT"; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; string event_type = 2; uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"]; } @@ -2324,7 +2324,7 @@ message ListEntitiesValveResponse { option (ifdef) = "USE_VALVE"; string object_id = 1; - fixed32 key = 2; + fixed32 key = 2 [(force) = true]; string name = 3; reserved 4; // Deprecated: was string unique_id @@ -2351,7 +2351,7 @@ message ValveStateResponse { option (ifdef) = "USE_VALVE"; option (no_delay) = true; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; float position = 2; ValveOperation current_operation = 3; uint32 device_id = 4 [(field_ifdef) = "USE_DEVICES"]; @@ -2364,7 +2364,7 @@ message ValveCommandRequest { option (no_delay) = true; option (base_class) = "CommandProtoMessage"; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; bool has_position = 2; float position = 3; bool stop = 4; @@ -2379,7 +2379,7 @@ message ListEntitiesDateTimeResponse { option (ifdef) = "USE_DATETIME_DATETIME"; string object_id = 1; - fixed32 key = 2; + fixed32 key = 2 [(force) = true]; string name = 3; reserved 4; // Deprecated: was string unique_id @@ -2395,7 +2395,7 @@ message DateTimeStateResponse { option (ifdef) = "USE_DATETIME_DATETIME"; option (no_delay) = true; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; // If the datetime does not have a valid state yet. // Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller bool missing_state = 2; @@ -2409,7 +2409,7 @@ message DateTimeCommandRequest { option (no_delay) = true; option (base_class) = "CommandProtoMessage"; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; fixed32 epoch_seconds = 2; uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"]; } @@ -2422,7 +2422,7 @@ message ListEntitiesUpdateResponse { option (ifdef) = "USE_UPDATE"; string object_id = 1; - fixed32 key = 2; + fixed32 key = 2 [(force) = true]; string name = 3; reserved 4; // Deprecated: was string unique_id @@ -2439,7 +2439,7 @@ message UpdateStateResponse { option (ifdef) = "USE_UPDATE"; option (no_delay) = true; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; bool missing_state = 2; bool in_progress = 3; bool has_progress = 4; @@ -2463,7 +2463,7 @@ message UpdateCommandRequest { option (no_delay) = true; option (base_class) = "CommandProtoMessage"; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; UpdateCommand command = 2; uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"]; } @@ -2505,7 +2505,7 @@ message ListEntitiesInfraredResponse { option (ifdef) = "USE_INFRARED"; string object_id = 1; - fixed32 key = 2; + fixed32 key = 2 [(force) = true]; string name = 3; string icon = 4 [(field_ifdef) = "USE_ENTITY_ICON"]; bool disabled_by_default = 5; @@ -2521,7 +2521,7 @@ message InfraredRFTransmitRawTimingsRequest { option (ifdef) = "USE_IR_RF"; uint32 device_id = 1 [(field_ifdef) = "USE_DEVICES"]; - fixed32 key = 2; // Key identifying the transmitter instance + fixed32 key = 2 [(force) = true]; // Key identifying the transmitter instance uint32 carrier_frequency = 3; // Carrier frequency in Hz uint32 repeat_count = 4; // Number of times to transmit (1 = once, 2 = twice, etc.) repeated sint32 timings = 5 [packed = true, (packed_buffer) = true]; // Raw timings in microseconds (zigzag-encoded): positive = mark (LED/TX on), negative = space (LED/TX off) @@ -2535,7 +2535,7 @@ message InfraredRFReceiveEvent { option (no_delay) = true; uint32 device_id = 1 [(field_ifdef) = "USE_DEVICES"]; - fixed32 key = 2; // Key identifying the receiver instance + fixed32 key = 2 [(force) = true]; // Key identifying the receiver instance repeated sint32 timings = 3 [packed = true, (container_pointer_no_template) = "std::vector"]; // Raw timings in microseconds (zigzag-encoded): alternating mark/space periods } diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 01993cc5e5..61b034c7ea 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -208,7 +208,7 @@ uint32_t DeviceInfoResponse::calculate_size() const { #ifdef USE_BINARY_SENSOR void ListEntitiesBinarySensorResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); buffer.encode_string(3, this->name); buffer.encode_string(5, this->device_class); buffer.encode_bool(6, this->is_status_binary_sensor); @@ -224,7 +224,7 @@ void ListEntitiesBinarySensorResponse::encode(ProtoWriteBuffer &buffer) const { uint32_t ListEntitiesBinarySensorResponse::calculate_size() const { uint32_t size = 0; size += ProtoSize::calc_length(1, this->object_id.size()); - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_length(1, this->name.size()); size += ProtoSize::calc_length(1, this->device_class.size()); size += ProtoSize::calc_bool(1, this->is_status_binary_sensor); @@ -239,7 +239,7 @@ uint32_t ListEntitiesBinarySensorResponse::calculate_size() const { return size; } void BinarySensorStateResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_fixed32(1, this->key); + buffer.write_tag_and_fixed32(13, this->key); buffer.encode_bool(2, this->state); buffer.encode_bool(3, this->missing_state); #ifdef USE_DEVICES @@ -248,7 +248,7 @@ void BinarySensorStateResponse::encode(ProtoWriteBuffer &buffer) const { } uint32_t BinarySensorStateResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_bool(1, this->state); size += ProtoSize::calc_bool(1, this->missing_state); #ifdef USE_DEVICES @@ -260,7 +260,7 @@ uint32_t BinarySensorStateResponse::calculate_size() const { #ifdef USE_COVER void ListEntitiesCoverResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); buffer.encode_string(3, this->name); buffer.encode_bool(5, this->assumed_state); buffer.encode_bool(6, this->supports_position); @@ -279,7 +279,7 @@ void ListEntitiesCoverResponse::encode(ProtoWriteBuffer &buffer) const { uint32_t ListEntitiesCoverResponse::calculate_size() const { uint32_t size = 0; size += ProtoSize::calc_length(1, this->object_id.size()); - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_length(1, this->name.size()); size += ProtoSize::calc_bool(1, this->assumed_state); size += ProtoSize::calc_bool(1, this->supports_position); @@ -297,7 +297,7 @@ uint32_t ListEntitiesCoverResponse::calculate_size() const { return size; } void CoverStateResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_fixed32(1, this->key); + buffer.write_tag_and_fixed32(13, this->key); buffer.encode_float(3, this->position); buffer.encode_float(4, this->tilt); buffer.encode_uint32(5, static_cast(this->current_operation)); @@ -307,7 +307,7 @@ void CoverStateResponse::encode(ProtoWriteBuffer &buffer) const { } uint32_t CoverStateResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_float(1, this->position); size += ProtoSize::calc_float(1, this->tilt); size += ProtoSize::calc_uint32(1, static_cast(this->current_operation)); @@ -357,7 +357,7 @@ bool CoverCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #ifdef USE_FAN void ListEntitiesFanResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); buffer.encode_string(3, this->name); buffer.encode_bool(5, this->supports_oscillation); buffer.encode_bool(6, this->supports_speed); @@ -378,7 +378,7 @@ void ListEntitiesFanResponse::encode(ProtoWriteBuffer &buffer) const { uint32_t ListEntitiesFanResponse::calculate_size() const { uint32_t size = 0; size += ProtoSize::calc_length(1, this->object_id.size()); - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_length(1, this->name.size()); size += ProtoSize::calc_bool(1, this->supports_oscillation); size += ProtoSize::calc_bool(1, this->supports_speed); @@ -400,7 +400,7 @@ uint32_t ListEntitiesFanResponse::calculate_size() const { return size; } void FanStateResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_fixed32(1, this->key); + buffer.write_tag_and_fixed32(13, this->key); buffer.encode_bool(2, this->state); buffer.encode_bool(3, this->oscillating); buffer.encode_uint32(5, static_cast(this->direction)); @@ -412,7 +412,7 @@ void FanStateResponse::encode(ProtoWriteBuffer &buffer) const { } uint32_t FanStateResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_bool(1, this->state); size += ProtoSize::calc_bool(1, this->oscillating); size += ProtoSize::calc_uint32(1, static_cast(this->direction)); @@ -487,7 +487,7 @@ bool FanCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #ifdef USE_LIGHT void ListEntitiesLightResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); buffer.encode_string(3, this->name); for (const auto &it : *this->supported_color_modes) { buffer.encode_uint32(12, static_cast(it), true); @@ -509,7 +509,7 @@ void ListEntitiesLightResponse::encode(ProtoWriteBuffer &buffer) const { uint32_t ListEntitiesLightResponse::calculate_size() const { uint32_t size = 0; size += ProtoSize::calc_length(1, this->object_id.size()); - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_length(1, this->name.size()); if (!this->supported_color_modes->empty()) { for (const auto &it : *this->supported_color_modes) { @@ -534,7 +534,7 @@ uint32_t ListEntitiesLightResponse::calculate_size() const { return size; } void LightStateResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_fixed32(1, this->key); + buffer.write_tag_and_fixed32(13, this->key); buffer.encode_bool(2, this->state); buffer.encode_float(3, this->brightness); buffer.encode_uint32(11, static_cast(this->color_mode)); @@ -553,7 +553,7 @@ void LightStateResponse::encode(ProtoWriteBuffer &buffer) const { } uint32_t LightStateResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_bool(1, this->state); size += ProtoSize::calc_float(1, this->brightness); size += ProtoSize::calc_uint32(1, static_cast(this->color_mode)); @@ -683,7 +683,7 @@ bool LightCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #ifdef USE_SENSOR void ListEntitiesSensorResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); buffer.encode_string(3, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); @@ -702,7 +702,7 @@ void ListEntitiesSensorResponse::encode(ProtoWriteBuffer &buffer) const { uint32_t ListEntitiesSensorResponse::calculate_size() const { uint32_t size = 0; size += ProtoSize::calc_length(1, this->object_id.size()); - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON size += ProtoSize::calc_length(1, this->icon.size()); @@ -720,7 +720,7 @@ uint32_t ListEntitiesSensorResponse::calculate_size() const { return size; } void SensorStateResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_fixed32(1, this->key); + buffer.write_tag_and_fixed32(13, this->key); buffer.encode_float(2, this->state); buffer.encode_bool(3, this->missing_state); #ifdef USE_DEVICES @@ -729,7 +729,7 @@ void SensorStateResponse::encode(ProtoWriteBuffer &buffer) const { } uint32_t SensorStateResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_float(1, this->state); size += ProtoSize::calc_bool(1, this->missing_state); #ifdef USE_DEVICES @@ -741,7 +741,7 @@ uint32_t SensorStateResponse::calculate_size() const { #ifdef USE_SWITCH void ListEntitiesSwitchResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); buffer.encode_string(3, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); @@ -757,7 +757,7 @@ void ListEntitiesSwitchResponse::encode(ProtoWriteBuffer &buffer) const { uint32_t ListEntitiesSwitchResponse::calculate_size() const { uint32_t size = 0; size += ProtoSize::calc_length(1, this->object_id.size()); - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON size += ProtoSize::calc_length(1, this->icon.size()); @@ -772,7 +772,7 @@ uint32_t ListEntitiesSwitchResponse::calculate_size() const { return size; } void SwitchStateResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_fixed32(1, this->key); + buffer.write_tag_and_fixed32(13, this->key); buffer.encode_bool(2, this->state); #ifdef USE_DEVICES buffer.encode_uint32(3, this->device_id); @@ -780,7 +780,7 @@ void SwitchStateResponse::encode(ProtoWriteBuffer &buffer) const { } uint32_t SwitchStateResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_bool(1, this->state); #ifdef USE_DEVICES size += ProtoSize::calc_uint32(1, this->device_id); @@ -816,7 +816,7 @@ bool SwitchCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #ifdef USE_TEXT_SENSOR void ListEntitiesTextSensorResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); buffer.encode_string(3, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); @@ -831,7 +831,7 @@ void ListEntitiesTextSensorResponse::encode(ProtoWriteBuffer &buffer) const { uint32_t ListEntitiesTextSensorResponse::calculate_size() const { uint32_t size = 0; size += ProtoSize::calc_length(1, this->object_id.size()); - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON size += ProtoSize::calc_length(1, this->icon.size()); @@ -845,7 +845,7 @@ uint32_t ListEntitiesTextSensorResponse::calculate_size() const { return size; } void TextSensorStateResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_fixed32(1, this->key); + buffer.write_tag_and_fixed32(13, this->key); buffer.encode_string(2, this->state); buffer.encode_bool(3, this->missing_state); #ifdef USE_DEVICES @@ -854,7 +854,7 @@ void TextSensorStateResponse::encode(ProtoWriteBuffer &buffer) const { } uint32_t TextSensorStateResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_length(1, this->state.size()); size += ProtoSize::calc_bool(1, this->missing_state); #ifdef USE_DEVICES @@ -1124,7 +1124,7 @@ uint32_t ListEntitiesServicesArgument::calculate_size() const { } void ListEntitiesServicesResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->name); - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); for (auto &it : this->args) { buffer.encode_sub_message(3, it); } @@ -1133,7 +1133,7 @@ void ListEntitiesServicesResponse::encode(ProtoWriteBuffer &buffer) const { uint32_t ListEntitiesServicesResponse::calculate_size() const { uint32_t size = 0; size += ProtoSize::calc_length(1, this->name.size()); - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; if (!this->args.empty()) { for (const auto &it : this->args) { size += ProtoSize::calc_message_force(1, it.calculate_size()); @@ -1269,7 +1269,7 @@ uint32_t ExecuteServiceResponse::calculate_size() const { #ifdef USE_CAMERA void ListEntitiesCameraResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); buffer.encode_string(3, this->name); buffer.encode_bool(5, this->disabled_by_default); #ifdef USE_ENTITY_ICON @@ -1283,7 +1283,7 @@ void ListEntitiesCameraResponse::encode(ProtoWriteBuffer &buffer) const { uint32_t ListEntitiesCameraResponse::calculate_size() const { uint32_t size = 0; size += ProtoSize::calc_length(1, this->object_id.size()); - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_length(1, this->name.size()); size += ProtoSize::calc_bool(1, this->disabled_by_default); #ifdef USE_ENTITY_ICON @@ -1296,7 +1296,7 @@ uint32_t ListEntitiesCameraResponse::calculate_size() const { return size; } void CameraImageResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_fixed32(1, this->key); + buffer.write_tag_and_fixed32(13, this->key); buffer.encode_bytes(2, this->data_ptr_, this->data_len_); buffer.encode_bool(3, this->done); #ifdef USE_DEVICES @@ -1305,7 +1305,7 @@ void CameraImageResponse::encode(ProtoWriteBuffer &buffer) const { } uint32_t CameraImageResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_length(1, this->data_len_); size += ProtoSize::calc_bool(1, this->done); #ifdef USE_DEVICES @@ -1330,7 +1330,7 @@ bool CameraImageRequest::decode_varint(uint32_t field_id, proto_varint_value_t v #ifdef USE_CLIMATE void ListEntitiesClimateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); buffer.encode_string(3, this->name); buffer.encode_bool(5, this->supports_current_temperature); buffer.encode_bool(6, this->supports_two_point_target_temperature); @@ -1374,7 +1374,7 @@ void ListEntitiesClimateResponse::encode(ProtoWriteBuffer &buffer) const { uint32_t ListEntitiesClimateResponse::calculate_size() const { uint32_t size = 0; size += ProtoSize::calc_length(1, this->object_id.size()); - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_length(1, this->name.size()); size += ProtoSize::calc_bool(1, this->supports_current_temperature); size += ProtoSize::calc_bool(1, this->supports_two_point_target_temperature); @@ -1429,7 +1429,7 @@ uint32_t ListEntitiesClimateResponse::calculate_size() const { return size; } void ClimateStateResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_fixed32(1, this->key); + buffer.write_tag_and_fixed32(13, this->key); buffer.encode_uint32(2, static_cast(this->mode)); buffer.encode_float(3, this->current_temperature); buffer.encode_float(4, this->target_temperature); @@ -1449,7 +1449,7 @@ void ClimateStateResponse::encode(ProtoWriteBuffer &buffer) const { } uint32_t ClimateStateResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_uint32(1, static_cast(this->mode)); size += ProtoSize::calc_float(1, this->current_temperature); size += ProtoSize::calc_float(1, this->target_temperature); @@ -1563,7 +1563,7 @@ bool ClimateCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #ifdef USE_WATER_HEATER void ListEntitiesWaterHeaterResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); buffer.encode_string(3, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(4, this->icon); @@ -1584,7 +1584,7 @@ void ListEntitiesWaterHeaterResponse::encode(ProtoWriteBuffer &buffer) const { uint32_t ListEntitiesWaterHeaterResponse::calculate_size() const { uint32_t size = 0; size += ProtoSize::calc_length(1, this->object_id.size()); - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON size += ProtoSize::calc_length(1, this->icon.size()); @@ -1606,7 +1606,7 @@ uint32_t ListEntitiesWaterHeaterResponse::calculate_size() const { return size; } void WaterHeaterStateResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_fixed32(1, this->key); + buffer.write_tag_and_fixed32(13, this->key); buffer.encode_float(2, this->current_temperature); buffer.encode_float(3, this->target_temperature); buffer.encode_uint32(4, static_cast(this->mode)); @@ -1619,7 +1619,7 @@ void WaterHeaterStateResponse::encode(ProtoWriteBuffer &buffer) const { } uint32_t WaterHeaterStateResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_float(1, this->current_temperature); size += ProtoSize::calc_float(1, this->target_temperature); size += ProtoSize::calc_uint32(1, static_cast(this->mode)); @@ -1675,7 +1675,7 @@ bool WaterHeaterCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value #ifdef USE_NUMBER void ListEntitiesNumberResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); buffer.encode_string(3, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); @@ -1695,7 +1695,7 @@ void ListEntitiesNumberResponse::encode(ProtoWriteBuffer &buffer) const { uint32_t ListEntitiesNumberResponse::calculate_size() const { uint32_t size = 0; size += ProtoSize::calc_length(1, this->object_id.size()); - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON size += ProtoSize::calc_length(1, this->icon.size()); @@ -1714,7 +1714,7 @@ uint32_t ListEntitiesNumberResponse::calculate_size() const { return size; } void NumberStateResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_fixed32(1, this->key); + buffer.write_tag_and_fixed32(13, this->key); buffer.encode_float(2, this->state); buffer.encode_bool(3, this->missing_state); #ifdef USE_DEVICES @@ -1723,7 +1723,7 @@ void NumberStateResponse::encode(ProtoWriteBuffer &buffer) const { } uint32_t NumberStateResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_float(1, this->state); size += ProtoSize::calc_bool(1, this->missing_state); #ifdef USE_DEVICES @@ -1760,7 +1760,7 @@ bool NumberCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #ifdef USE_SELECT void ListEntitiesSelectResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); buffer.encode_string(3, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); @@ -1777,7 +1777,7 @@ void ListEntitiesSelectResponse::encode(ProtoWriteBuffer &buffer) const { uint32_t ListEntitiesSelectResponse::calculate_size() const { uint32_t size = 0; size += ProtoSize::calc_length(1, this->object_id.size()); - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON size += ProtoSize::calc_length(1, this->icon.size()); @@ -1795,7 +1795,7 @@ uint32_t ListEntitiesSelectResponse::calculate_size() const { return size; } void SelectStateResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_fixed32(1, this->key); + buffer.write_tag_and_fixed32(13, this->key); buffer.encode_string(2, this->state); buffer.encode_bool(3, this->missing_state); #ifdef USE_DEVICES @@ -1804,7 +1804,7 @@ void SelectStateResponse::encode(ProtoWriteBuffer &buffer) const { } uint32_t SelectStateResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_length(1, this->state.size()); size += ProtoSize::calc_bool(1, this->missing_state); #ifdef USE_DEVICES @@ -1849,7 +1849,7 @@ bool SelectCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #ifdef USE_SIREN void ListEntitiesSirenResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); buffer.encode_string(3, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); @@ -1868,7 +1868,7 @@ void ListEntitiesSirenResponse::encode(ProtoWriteBuffer &buffer) const { uint32_t ListEntitiesSirenResponse::calculate_size() const { uint32_t size = 0; size += ProtoSize::calc_length(1, this->object_id.size()); - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON size += ProtoSize::calc_length(1, this->icon.size()); @@ -1888,7 +1888,7 @@ uint32_t ListEntitiesSirenResponse::calculate_size() const { return size; } void SirenStateResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_fixed32(1, this->key); + buffer.write_tag_and_fixed32(13, this->key); buffer.encode_bool(2, this->state); #ifdef USE_DEVICES buffer.encode_uint32(3, this->device_id); @@ -1896,7 +1896,7 @@ void SirenStateResponse::encode(ProtoWriteBuffer &buffer) const { } uint32_t SirenStateResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_bool(1, this->state); #ifdef USE_DEVICES size += ProtoSize::calc_uint32(1, this->device_id); @@ -1961,7 +1961,7 @@ bool SirenCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #ifdef USE_LOCK void ListEntitiesLockResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); buffer.encode_string(3, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); @@ -1979,7 +1979,7 @@ void ListEntitiesLockResponse::encode(ProtoWriteBuffer &buffer) const { uint32_t ListEntitiesLockResponse::calculate_size() const { uint32_t size = 0; size += ProtoSize::calc_length(1, this->object_id.size()); - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON size += ProtoSize::calc_length(1, this->icon.size()); @@ -1996,7 +1996,7 @@ uint32_t ListEntitiesLockResponse::calculate_size() const { return size; } void LockStateResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_fixed32(1, this->key); + buffer.write_tag_and_fixed32(13, this->key); buffer.encode_uint32(2, static_cast(this->state)); #ifdef USE_DEVICES buffer.encode_uint32(3, this->device_id); @@ -2004,7 +2004,7 @@ void LockStateResponse::encode(ProtoWriteBuffer &buffer) const { } uint32_t LockStateResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_uint32(1, static_cast(this->state)); #ifdef USE_DEVICES size += ProtoSize::calc_uint32(1, this->device_id); @@ -2054,7 +2054,7 @@ bool LockCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #ifdef USE_BUTTON void ListEntitiesButtonResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); buffer.encode_string(3, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); @@ -2069,7 +2069,7 @@ void ListEntitiesButtonResponse::encode(ProtoWriteBuffer &buffer) const { uint32_t ListEntitiesButtonResponse::calculate_size() const { uint32_t size = 0; size += ProtoSize::calc_length(1, this->object_id.size()); - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON size += ProtoSize::calc_length(1, this->icon.size()); @@ -2124,7 +2124,7 @@ uint32_t MediaPlayerSupportedFormat::calculate_size() const { } void ListEntitiesMediaPlayerResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); buffer.encode_string(3, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); @@ -2143,7 +2143,7 @@ void ListEntitiesMediaPlayerResponse::encode(ProtoWriteBuffer &buffer) const { uint32_t ListEntitiesMediaPlayerResponse::calculate_size() const { uint32_t size = 0; size += ProtoSize::calc_length(1, this->object_id.size()); - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON size += ProtoSize::calc_length(1, this->icon.size()); @@ -2163,7 +2163,7 @@ uint32_t ListEntitiesMediaPlayerResponse::calculate_size() const { return size; } void MediaPlayerStateResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_fixed32(1, this->key); + buffer.write_tag_and_fixed32(13, this->key); buffer.encode_uint32(2, static_cast(this->state)); buffer.encode_float(3, this->volume); buffer.encode_bool(4, this->muted); @@ -2173,7 +2173,7 @@ void MediaPlayerStateResponse::encode(ProtoWriteBuffer &buffer) const { } uint32_t MediaPlayerStateResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_uint32(1, static_cast(this->state)); size += ProtoSize::calc_float(1, this->volume); size += ProtoSize::calc_bool(1, this->muted); @@ -2942,7 +2942,7 @@ bool VoiceAssistantSetConfiguration::decode_length(uint32_t field_id, ProtoLengt #ifdef USE_ALARM_CONTROL_PANEL void ListEntitiesAlarmControlPanelResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); buffer.encode_string(3, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); @@ -2959,7 +2959,7 @@ void ListEntitiesAlarmControlPanelResponse::encode(ProtoWriteBuffer &buffer) con uint32_t ListEntitiesAlarmControlPanelResponse::calculate_size() const { uint32_t size = 0; size += ProtoSize::calc_length(1, this->object_id.size()); - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON size += ProtoSize::calc_length(1, this->icon.size()); @@ -2975,7 +2975,7 @@ uint32_t ListEntitiesAlarmControlPanelResponse::calculate_size() const { return size; } void AlarmControlPanelStateResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_fixed32(1, this->key); + buffer.write_tag_and_fixed32(13, this->key); buffer.encode_uint32(2, static_cast(this->state)); #ifdef USE_DEVICES buffer.encode_uint32(3, this->device_id); @@ -2983,7 +2983,7 @@ void AlarmControlPanelStateResponse::encode(ProtoWriteBuffer &buffer) const { } uint32_t AlarmControlPanelStateResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_uint32(1, static_cast(this->state)); #ifdef USE_DEVICES size += ProtoSize::calc_uint32(1, this->device_id); @@ -3030,7 +3030,7 @@ bool AlarmControlPanelCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit #ifdef USE_TEXT void ListEntitiesTextResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); buffer.encode_string(3, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); @@ -3048,7 +3048,7 @@ void ListEntitiesTextResponse::encode(ProtoWriteBuffer &buffer) const { uint32_t ListEntitiesTextResponse::calculate_size() const { uint32_t size = 0; size += ProtoSize::calc_length(1, this->object_id.size()); - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON size += ProtoSize::calc_length(1, this->icon.size()); @@ -3065,7 +3065,7 @@ uint32_t ListEntitiesTextResponse::calculate_size() const { return size; } void TextStateResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_fixed32(1, this->key); + buffer.write_tag_and_fixed32(13, this->key); buffer.encode_string(2, this->state); buffer.encode_bool(3, this->missing_state); #ifdef USE_DEVICES @@ -3074,7 +3074,7 @@ void TextStateResponse::encode(ProtoWriteBuffer &buffer) const { } uint32_t TextStateResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_length(1, this->state.size()); size += ProtoSize::calc_bool(1, this->missing_state); #ifdef USE_DEVICES @@ -3119,7 +3119,7 @@ bool TextCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #ifdef USE_DATETIME_DATE void ListEntitiesDateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); buffer.encode_string(3, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); @@ -3133,7 +3133,7 @@ void ListEntitiesDateResponse::encode(ProtoWriteBuffer &buffer) const { uint32_t ListEntitiesDateResponse::calculate_size() const { uint32_t size = 0; size += ProtoSize::calc_length(1, this->object_id.size()); - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON size += ProtoSize::calc_length(1, this->icon.size()); @@ -3146,7 +3146,7 @@ uint32_t ListEntitiesDateResponse::calculate_size() const { return size; } void DateStateResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_fixed32(1, this->key); + buffer.write_tag_and_fixed32(13, this->key); buffer.encode_bool(2, this->missing_state); buffer.encode_uint32(3, this->year); buffer.encode_uint32(4, this->month); @@ -3157,7 +3157,7 @@ void DateStateResponse::encode(ProtoWriteBuffer &buffer) const { } uint32_t DateStateResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_bool(1, this->missing_state); size += ProtoSize::calc_uint32(1, this->year); size += ProtoSize::calc_uint32(1, this->month); @@ -3202,7 +3202,7 @@ bool DateCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #ifdef USE_DATETIME_TIME void ListEntitiesTimeResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); buffer.encode_string(3, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); @@ -3216,7 +3216,7 @@ void ListEntitiesTimeResponse::encode(ProtoWriteBuffer &buffer) const { uint32_t ListEntitiesTimeResponse::calculate_size() const { uint32_t size = 0; size += ProtoSize::calc_length(1, this->object_id.size()); - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON size += ProtoSize::calc_length(1, this->icon.size()); @@ -3229,7 +3229,7 @@ uint32_t ListEntitiesTimeResponse::calculate_size() const { return size; } void TimeStateResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_fixed32(1, this->key); + buffer.write_tag_and_fixed32(13, this->key); buffer.encode_bool(2, this->missing_state); buffer.encode_uint32(3, this->hour); buffer.encode_uint32(4, this->minute); @@ -3240,7 +3240,7 @@ void TimeStateResponse::encode(ProtoWriteBuffer &buffer) const { } uint32_t TimeStateResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_bool(1, this->missing_state); size += ProtoSize::calc_uint32(1, this->hour); size += ProtoSize::calc_uint32(1, this->minute); @@ -3285,7 +3285,7 @@ bool TimeCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #ifdef USE_EVENT void ListEntitiesEventResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); buffer.encode_string(3, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); @@ -3303,7 +3303,7 @@ void ListEntitiesEventResponse::encode(ProtoWriteBuffer &buffer) const { uint32_t ListEntitiesEventResponse::calculate_size() const { uint32_t size = 0; size += ProtoSize::calc_length(1, this->object_id.size()); - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON size += ProtoSize::calc_length(1, this->icon.size()); @@ -3322,7 +3322,7 @@ uint32_t ListEntitiesEventResponse::calculate_size() const { return size; } void EventResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_fixed32(1, this->key); + buffer.write_tag_and_fixed32(13, this->key); buffer.encode_string(2, this->event_type); #ifdef USE_DEVICES buffer.encode_uint32(3, this->device_id); @@ -3330,7 +3330,7 @@ void EventResponse::encode(ProtoWriteBuffer &buffer) const { } uint32_t EventResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_length(1, this->event_type.size()); #ifdef USE_DEVICES size += ProtoSize::calc_uint32(1, this->device_id); @@ -3341,7 +3341,7 @@ uint32_t EventResponse::calculate_size() const { #ifdef USE_VALVE void ListEntitiesValveResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); buffer.encode_string(3, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); @@ -3359,7 +3359,7 @@ void ListEntitiesValveResponse::encode(ProtoWriteBuffer &buffer) const { uint32_t ListEntitiesValveResponse::calculate_size() const { uint32_t size = 0; size += ProtoSize::calc_length(1, this->object_id.size()); - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON size += ProtoSize::calc_length(1, this->icon.size()); @@ -3376,7 +3376,7 @@ uint32_t ListEntitiesValveResponse::calculate_size() const { return size; } void ValveStateResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_fixed32(1, this->key); + buffer.write_tag_and_fixed32(13, this->key); buffer.encode_float(2, this->position); buffer.encode_uint32(3, static_cast(this->current_operation)); #ifdef USE_DEVICES @@ -3385,7 +3385,7 @@ void ValveStateResponse::encode(ProtoWriteBuffer &buffer) const { } uint32_t ValveStateResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_float(1, this->position); size += ProtoSize::calc_uint32(1, static_cast(this->current_operation)); #ifdef USE_DEVICES @@ -3428,7 +3428,7 @@ bool ValveCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #ifdef USE_DATETIME_DATETIME void ListEntitiesDateTimeResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); buffer.encode_string(3, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); @@ -3442,7 +3442,7 @@ void ListEntitiesDateTimeResponse::encode(ProtoWriteBuffer &buffer) const { uint32_t ListEntitiesDateTimeResponse::calculate_size() const { uint32_t size = 0; size += ProtoSize::calc_length(1, this->object_id.size()); - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON size += ProtoSize::calc_length(1, this->icon.size()); @@ -3455,7 +3455,7 @@ uint32_t ListEntitiesDateTimeResponse::calculate_size() const { return size; } void DateTimeStateResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_fixed32(1, this->key); + buffer.write_tag_and_fixed32(13, this->key); buffer.encode_bool(2, this->missing_state); buffer.encode_fixed32(3, this->epoch_seconds); #ifdef USE_DEVICES @@ -3464,7 +3464,7 @@ void DateTimeStateResponse::encode(ProtoWriteBuffer &buffer) const { } uint32_t DateTimeStateResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_bool(1, this->missing_state); size += ProtoSize::calc_fixed32(1, this->epoch_seconds); #ifdef USE_DEVICES @@ -3501,7 +3501,7 @@ bool DateTimeCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #ifdef USE_UPDATE void ListEntitiesUpdateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); buffer.encode_string(3, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); @@ -3516,7 +3516,7 @@ void ListEntitiesUpdateResponse::encode(ProtoWriteBuffer &buffer) const { uint32_t ListEntitiesUpdateResponse::calculate_size() const { uint32_t size = 0; size += ProtoSize::calc_length(1, this->object_id.size()); - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON size += ProtoSize::calc_length(1, this->icon.size()); @@ -3530,7 +3530,7 @@ uint32_t ListEntitiesUpdateResponse::calculate_size() const { return size; } void UpdateStateResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_fixed32(1, this->key); + buffer.write_tag_and_fixed32(13, this->key); buffer.encode_bool(2, this->missing_state); buffer.encode_bool(3, this->in_progress); buffer.encode_bool(4, this->has_progress); @@ -3546,7 +3546,7 @@ void UpdateStateResponse::encode(ProtoWriteBuffer &buffer) const { } uint32_t UpdateStateResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_bool(1, this->missing_state); size += ProtoSize::calc_bool(1, this->in_progress); size += ProtoSize::calc_bool(1, this->has_progress); @@ -3642,7 +3642,7 @@ uint32_t ZWaveProxyRequest::calculate_size() const { #ifdef USE_INFRARED void ListEntitiesInfraredResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); buffer.encode_string(3, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(4, this->icon); @@ -3657,7 +3657,7 @@ void ListEntitiesInfraredResponse::encode(ProtoWriteBuffer &buffer) const { uint32_t ListEntitiesInfraredResponse::calculate_size() const { uint32_t size = 0; size += ProtoSize::calc_length(1, this->object_id.size()); - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON size += ProtoSize::calc_length(1, this->icon.size()); @@ -3717,7 +3717,7 @@ void InfraredRFReceiveEvent::encode(ProtoWriteBuffer &buffer) const { #ifdef USE_DEVICES buffer.encode_uint32(1, this->device_id); #endif - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); for (const auto &it : *this->timings) { buffer.encode_sint32(3, it, true); } @@ -3727,7 +3727,7 @@ uint32_t InfraredRFReceiveEvent::calculate_size() const { #ifdef USE_DEVICES size += ProtoSize::calc_uint32(1, this->device_id); #endif - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; if (!this->timings->empty()) { for (const auto &it : *this->timings) { size += ProtoSize::calc_sint32_force(1, it); diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index d6e993d3a5..cd22915703 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -236,6 +236,21 @@ class ProtoWriteBuffer { * Following https://protobuf.dev/programming-guides/encoding/#structure */ void encode_field_raw(uint32_t field_id, uint32_t type) { this->encode_varint_raw((field_id << 3) | type); } + /// Write a precomputed tag byte + 32-bit value in one operation. + /// Tag must be a single-byte varint (< 128). No zero check. + inline void write_tag_and_fixed32(uint8_t tag, uint32_t value) ESPHOME_ALWAYS_INLINE { + this->debug_check_bounds_(5); + this->pos_[0] = tag; +#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ + std::memcpy(this->pos_ + 1, &value, 4); +#else + this->pos_[1] = static_cast(value & 0xFF); + this->pos_[2] = static_cast((value >> 8) & 0xFF); + this->pos_[3] = static_cast((value >> 16) & 0xFF); + this->pos_[4] = static_cast((value >> 24) & 0xFF); +#endif + this->pos_ += 5; + } void encode_string(uint32_t field_id, const char *string, size_t len, bool force = false) { if (len == 0 && !force) return; @@ -276,8 +291,7 @@ class ProtoWriteBuffer { this->debug_check_bounds_(1); *this->pos_++ = value ? 0x01 : 0x00; } - // noinline: 51 call sites; inlining causes net code growth vs a single out-of-line copy - __attribute__((noinline)) void encode_fixed32(uint32_t field_id, uint32_t value, bool force = false) { + void encode_fixed32(uint32_t field_id, uint32_t value, bool force = false) { if (value == 0 && !force) return; diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index dff6c7690a..aca31e49c8 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -254,14 +254,17 @@ class TypeInfo(ABC): def dump(self, name: str) -> str: """Dump the value to the output.""" + def calculate_tag(self) -> int: + """Calculate the protobuf tag (field_id << 3 | wire_type).""" + return (self.number << 3) | (self.wire_type & 0b111) + def calculate_field_id_size(self) -> int: """Calculates the size of a field ID in bytes. Returns: The number of bytes needed to encode the field ID """ - # Calculate the tag by combining field_id and wire_type - tag = (self.number << 3) | (self.wire_type & 0b111) + tag = self.calculate_tag() # Calculate the varint size if tag < 128: @@ -556,6 +559,16 @@ class Fixed32Type(TypeInfo): o += "out.append(buffer);" return o + @property + def encode_content(self) -> str: + tag = self.calculate_tag() + if self.force and tag < 128: + # Emit combined tag+value write: precomputed tag + direct memcpy + return f"buffer.write_tag_and_fixed32({tag}, this->{self.field_name});" + if self.force: + return f"buffer.{self.encode_func}({self.number}, this->{self.field_name}, true);" + return f"buffer.{self.encode_func}({self.number}, this->{self.field_name});" + def get_size_calculation(self, name: str, force: bool = False) -> str: field_id_size = self.calculate_field_id_size() if force: From 6992219e341b4eca9f2bfdf28474cbf45883bd08 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Mar 2026 16:27:07 -1000 Subject: [PATCH 093/166] [core] Attribute placement new storage symbols to components (#15092) --- esphome/analyze_memory/__init__.py | 29 ++++++ esphome/analyze_memory/cli.py | 48 +++++++--- esphome/cpp_generator.py | 11 ++- .../deep_sleep/test_deep_sleep.py | 2 +- tests/component_tests/image/test_init.py | 4 +- .../mipi_dsi/test_mipi_dsi_config.py | 4 +- .../status_led/test_status_led.py | 4 +- .../test_pstorage_attribution.py | 90 +++++++++++++++++++ 8 files changed, 171 insertions(+), 21 deletions(-) create mode 100644 tests/unit_tests/analyze_memory/test_pstorage_attribution.py diff --git a/esphome/analyze_memory/__init__.py b/esphome/analyze_memory/__init__.py index 48ecf2c1dc..f56d720ec2 100644 --- a/esphome/analyze_memory/__init__.py +++ b/esphome/analyze_memory/__init__.py @@ -56,6 +56,10 @@ _COMPONENT_PREFIX_LIB = "[lib]" _COMPONENT_CORE = f"{_COMPONENT_PREFIX_ESPHOME}core" _COMPONENT_API = f"{_COMPONENT_PREFIX_ESPHOME}api" +# Placement new storage suffix (generated by codegen Pvariable) +_PSTORAGE_SUFFIX = "__pstorage" + + # C++ namespace prefixes _NAMESPACE_ESPHOME = "esphome::" _NAMESPACE_STD = "std::" @@ -332,6 +336,13 @@ class MemoryAnalyzer: # Demangle C++ names if needed demangled = self._demangle_symbol(symbol_name) + # Check for placement new storage symbols (generated by codegen) + # Format: {component}__{id}__pstorage + if demangled.endswith(_PSTORAGE_SUFFIX) and ( + component := self._match_pstorage_component(demangled) + ): + return component + # Check for special component classes first (before namespace pattern) # This handles cases like esphome::ESPHomeOTAComponent which should map to ota if _NAMESPACE_ESPHOME in demangled: @@ -399,6 +410,24 @@ class MemoryAnalyzer: # Track uncategorized symbols for analysis return "other" + def _match_pstorage_component(self, symbol_name: str) -> str | None: + """Match a __pstorage symbol to its ESPHome component. + + Symbol format: {component}__{id}__pstorage + The component namespace is embedded by codegen before the double underscore. + """ + prefix = symbol_name[: -len(_PSTORAGE_SUFFIX)] + # Extract component namespace before the first double underscore + dunder_pos = prefix.find("__") + if dunder_pos == -1: + return None + component_name = prefix[:dunder_pos] + if component_name in get_esphome_components(): + return f"{_COMPONENT_PREFIX_ESPHOME}{component_name}" + if component_name in self.external_components: + return f"{_COMPONENT_PREFIX_EXTERNAL}{component_name}" + return None + def _batch_demangle_symbols(self, symbols: list[str]) -> None: """Batch demangle C++ symbol names for efficiency.""" if not symbols: diff --git a/esphome/analyze_memory/cli.py b/esphome/analyze_memory/cli.py index acaf5f4562..b7561e8ffc 100644 --- a/esphome/analyze_memory/cli.py +++ b/esphome/analyze_memory/cli.py @@ -15,6 +15,7 @@ from . import ( _COMPONENT_PREFIX_ESPHOME, _COMPONENT_PREFIX_EXTERNAL, _COMPONENT_PREFIX_LIB, + _PSTORAGE_SUFFIX, RAM_SECTIONS, MemoryAnalyzer, ) @@ -23,6 +24,17 @@ if TYPE_CHECKING: from . import ComponentMemory +def _format_pstorage_name(name: str) -> str: + """Format a __pstorage symbol as 'storage for {id}'.""" + if not name.endswith(_PSTORAGE_SUFFIX): + return name + prefix = name[: -len(_PSTORAGE_SUFFIX)] + # Strip component namespace prefix: {component}__{id} -> {id} + dunder_pos = prefix.find("__") + var_id = prefix[dunder_pos + 2 :] if dunder_pos != -1 else prefix + return f"storage for {var_id}" + + class MemoryAnalyzerCLI(MemoryAnalyzer): """Memory analyzer with CLI-specific report generation.""" @@ -148,11 +160,14 @@ class MemoryAnalyzerCLI(MemoryAnalyzer): If section is one of the RAM sections (.data or .bss), a label like " [data]" or " [bss]" is appended. For non-RAM sections or when section is None, no section label is added. + + Placement new storage symbols are formatted as "storage for {id}". """ + display_name = _format_pstorage_name(demangled) section_label = "" if section in RAM_SECTIONS: section_label = f" [{section[1:]}]" # .data -> [data], .bss -> [bss] - return f"{demangled} ({size:,} B){section_label}" + return f"{display_name} ({size:,} B){section_label}" def _add_top_symbols(self, lines: list[str]) -> None: """Add a section showing the top largest symbols in the binary.""" @@ -175,11 +190,13 @@ class MemoryAnalyzerCLI(MemoryAnalyzer): for i, (_, demangled, size, section, component) in enumerate(top_symbols): # Format section label section_label = f"[{section[1:]}]" if section else "" - # Truncate demangled name if too long + # Format storage symbols readably + display_name = _format_pstorage_name(demangled) + # Truncate if too long demangled_display = ( - f"{demangled[:truncate_limit]}..." - if len(demangled) > self.COL_TOP_SYMBOL_NAME - else demangled + f"{display_name[:truncate_limit]}..." + if len(display_name) > self.COL_TOP_SYMBOL_NAME + else display_name ) lines.append( f"{i + 1:>2}. {size:>7,} B {section_label:<8} {demangled_display:<{self.COL_TOP_SYMBOL_NAME}} {component}" @@ -573,15 +590,16 @@ class MemoryAnalyzerCLI(MemoryAnalyzer): lines.append(f"Total size: {comp_mem.flash_total:,} B") lines.append("") - # Show all symbols above threshold for better visibility + # Show symbols above threshold, always include storage symbols large_symbols = [ (sym, dem, size, sec) for sym, dem, size, sec in sorted_symbols if size > self.SYMBOL_SIZE_THRESHOLD + or dem.endswith(_PSTORAGE_SUFFIX) ] lines.append( - f"{comp_name} Symbols > {self.SYMBOL_SIZE_THRESHOLD} B ({len(large_symbols)} symbols):" + f"{comp_name} Symbols > {self.SYMBOL_SIZE_THRESHOLD} B & storage ({len(large_symbols)} symbols):" ) for i, (symbol, demangled, size, section) in enumerate(large_symbols): lines.append( @@ -604,7 +622,10 @@ class MemoryAnalyzerCLI(MemoryAnalyzer): # Sort by size descending sorted_ram_syms = sorted(ram_syms, key=lambda x: x[2], reverse=True) large_ram_syms = [ - s for s in sorted_ram_syms if s[2] > self.RAM_SYMBOL_SIZE_THRESHOLD + s + for s in sorted_ram_syms + if s[2] > self.RAM_SYMBOL_SIZE_THRESHOLD + or s[1].endswith(_PSTORAGE_SUFFIX) ] lines.append(f"{name} ({mem.ram_total:,} B total RAM):") @@ -622,13 +643,14 @@ class MemoryAnalyzerCLI(MemoryAnalyzer): for symbol, demangled, size, section in large_ram_syms[:10]: # Format section label consistently by stripping leading dot section_label = section.lstrip(".") if section else "" + display_name = _format_pstorage_name(demangled) # Add ellipsis if name is truncated - demangled_display = ( - f"{demangled[:70]}..." if len(demangled) > 70 else demangled - ) - lines.append( - f" {size:>6,} B [{section_label}] {demangled_display}" + display_name = ( + f"{display_name[:70]}..." + if len(display_name) > 70 + else display_name ) + lines.append(f" {size:>6,} B [{section_label}] {display_name}") if len(large_ram_syms) > 10: lines.append(f" ... and {len(large_ram_syms) - 10} more") lines.append("") diff --git a/esphome/cpp_generator.py b/esphome/cpp_generator.py index 3ed5d0ba37..e97bd71a48 100644 --- a/esphome/cpp_generator.py +++ b/esphome/cpp_generator.py @@ -584,7 +584,16 @@ def Pvariable(id_: ID, rhs: SafeExpType, type_: "MockObj" = None) -> "MockObj": # For 'new' allocations, use placement new into static storage # to avoid heap fragmentation on embedded devices. the_type = id_.type - storage_name = f"{id_.id}__pstorage" + # Extract component namespace from type for memory analysis attribution + type_str = str(the_type) + # Strip leading esphome:: to get the component namespace + # e.g. esphome::dsmr::Dsmr -> dsmr, logger::Logger -> logger + bare = type_str.removeprefix("esphome::") + if "::" in bare: + component_ns = bare.split("::", maxsplit=1)[0].rstrip("_") + else: + component_ns = "esphome" + storage_name = f"{component_ns}__{id_.id}__pstorage" # Declare aligned byte array for the object storage CORE.add_global( diff --git a/tests/component_tests/deep_sleep/test_deep_sleep.py b/tests/component_tests/deep_sleep/test_deep_sleep.py index 212f61e44b..8c1278a332 100644 --- a/tests/component_tests/deep_sleep/test_deep_sleep.py +++ b/tests/component_tests/deep_sleep/test_deep_sleep.py @@ -8,7 +8,7 @@ def test_deep_sleep_setup(generate_main): main_cpp = generate_main("tests/component_tests/deep_sleep/test_deep_sleep1.yaml") assert ( - "static deep_sleep::DeepSleepComponent *const deepsleep = reinterpret_cast(deepsleep__pstorage);" + "static deep_sleep::DeepSleepComponent *const deepsleep = reinterpret_cast(deep_sleep__deepsleep__pstorage);" in main_cpp ) assert "new(deepsleep) deep_sleep::DeepSleepComponent();" in main_cpp diff --git a/tests/component_tests/image/test_init.py b/tests/component_tests/image/test_init.py index 9003a4ee5d..6f73888c7d 100644 --- a/tests/component_tests/image/test_init.py +++ b/tests/component_tests/image/test_init.py @@ -242,11 +242,11 @@ def test_image_generation( main_cpp = generate_main(component_config_path("image_test.yaml")) assert "uint8_t_id[] PROGMEM = {0x24, 0x21, 0x24, 0x21" in main_cpp assert ( - "alignas(image::Image) static unsigned char cat_img__pstorage[sizeof(image::Image)];" + "alignas(image::Image) static unsigned char image__cat_img__pstorage[sizeof(image::Image)];" in main_cpp ) assert ( - "static image::Image *const cat_img = reinterpret_cast(cat_img__pstorage);" + "static image::Image *const cat_img = reinterpret_cast(image__cat_img__pstorage);" in main_cpp ) assert ( diff --git a/tests/component_tests/mipi_dsi/test_mipi_dsi_config.py b/tests/component_tests/mipi_dsi/test_mipi_dsi_config.py index e6f344b086..1ae8cc644e 100644 --- a/tests/component_tests/mipi_dsi/test_mipi_dsi_config.py +++ b/tests/component_tests/mipi_dsi/test_mipi_dsi_config.py @@ -119,11 +119,11 @@ def test_code_generation( main_cpp = generate_main(component_fixture_path("mipi_dsi.yaml")) assert ( - "alignas(mipi_dsi::MIPI_DSI) static unsigned char p4_nano__pstorage[sizeof(mipi_dsi::MIPI_DSI)];" + "alignas(mipi_dsi::MIPI_DSI) static unsigned char mipi_dsi__p4_nano__pstorage[sizeof(mipi_dsi::MIPI_DSI)];" in main_cpp ) assert ( - "static mipi_dsi::MIPI_DSI *const p4_nano = reinterpret_cast(p4_nano__pstorage);" + "static mipi_dsi::MIPI_DSI *const p4_nano = reinterpret_cast(mipi_dsi__p4_nano__pstorage);" in main_cpp ) assert ( diff --git a/tests/component_tests/status_led/test_status_led.py b/tests/component_tests/status_led/test_status_led.py index 0e96e631f5..f7e0a9de86 100644 --- a/tests/component_tests/status_led/test_status_led.py +++ b/tests/component_tests/status_led/test_status_led.py @@ -13,11 +13,11 @@ def test_status_led_generation( """Test status_led generation.""" main_cpp = generate_main(component_config_path("status_led_test.yaml")) assert ( - "alignas(status_led::StatusLED) static unsigned char status_led_statusled_id__pstorage[sizeof(status_led::StatusLED)];" + "alignas(status_led::StatusLED) static unsigned char status_led__status_led_statusled_id__pstorage[sizeof(status_led::StatusLED)];" in main_cpp ) assert ( - "static status_led::StatusLED *const status_led_statusled_id = reinterpret_cast(status_led_statusled_id__pstorage);" + "static status_led::StatusLED *const status_led_statusled_id = reinterpret_cast(status_led__status_led_statusled_id__pstorage);" in main_cpp ) assert "new(status_led_statusled_id) status_led::StatusLED(" in main_cpp diff --git a/tests/unit_tests/analyze_memory/test_pstorage_attribution.py b/tests/unit_tests/analyze_memory/test_pstorage_attribution.py new file mode 100644 index 0000000000..a57b283f44 --- /dev/null +++ b/tests/unit_tests/analyze_memory/test_pstorage_attribution.py @@ -0,0 +1,90 @@ +"""Tests for __pstorage symbol attribution in memory analyzer.""" + +from unittest.mock import patch + +from esphome.analyze_memory import _PSTORAGE_SUFFIX, MemoryAnalyzer + + +def _make_analyzer(external_components: set[str] | None = None) -> MemoryAnalyzer: + """Create a MemoryAnalyzer with mocked dependencies.""" + with patch.object(MemoryAnalyzer, "__init__", lambda self, *a, **kw: None): + analyzer = MemoryAnalyzer.__new__(MemoryAnalyzer) + analyzer.external_components = external_components or set() + return analyzer + + +def test_pstorage_suffix_constant() -> None: + """Verify the suffix constant matches what codegen produces.""" + assert _PSTORAGE_SUFFIX == "__pstorage" + + +def test_match_pstorage_simple_component() -> None: + """Simple component name like 'logger'.""" + analyzer = _make_analyzer() + result = analyzer._match_pstorage_component("logger__logger_id__pstorage") + assert result == "[esphome]logger" + + +def test_match_pstorage_underscore_component() -> None: + """Component with underscore like 'web_server'.""" + analyzer = _make_analyzer() + result = analyzer._match_pstorage_component("web_server__webserver_id__pstorage") + assert result == "[esphome]web_server" + + +def test_match_pstorage_api() -> None: + """API component.""" + analyzer = _make_analyzer() + result = analyzer._match_pstorage_component("api__apiserver_id__pstorage") + assert result == "[esphome]api" + + +def test_match_pstorage_deep_sleep() -> None: + """Component with underscore: deep_sleep.""" + analyzer = _make_analyzer() + result = analyzer._match_pstorage_component("deep_sleep__deepsleep__pstorage") + assert result == "[esphome]deep_sleep" + + +def test_match_pstorage_status_led() -> None: + """Component with underscore: status_led.""" + analyzer = _make_analyzer() + result = analyzer._match_pstorage_component("status_led__statusled_id__pstorage") + assert result == "[esphome]status_led" + + +def test_match_pstorage_external_component() -> None: + """External component should be attributed correctly.""" + analyzer = _make_analyzer(external_components={"my_custom"}) + result = analyzer._match_pstorage_component("my_custom__thing_id__pstorage") + assert result == "[external]my_custom" + + +def test_match_pstorage_no_dunder_returns_none() -> None: + """Symbol without double underscore separator returns None.""" + analyzer = _make_analyzer() + result = analyzer._match_pstorage_component("something__pstorage") + assert result is None + + +def test_match_pstorage_unknown_component_returns_none() -> None: + """Unknown component namespace returns None.""" + analyzer = _make_analyzer() + result = analyzer._match_pstorage_component("nonexistent__thing_id__pstorage") + assert result is None + + +def test_match_pstorage_esphome_component() -> None: + """esphome:: namespace types map to the esphome component.""" + analyzer = _make_analyzer() + result = analyzer._match_pstorage_component( + "esphome__esphomeotacomponent_id__pstorage" + ) + assert result == "[esphome]esphome" + + +def test_match_pstorage_user_id_with_component_prefix() -> None: + """User-chosen ID that happens to contain a component name.""" + analyzer = _make_analyzer() + result = analyzer._match_pstorage_component("logger__relay1__pstorage") + assert result == "[esphome]logger" From 98d9fd76b35f995e14fca86e07515317f2220d22 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Mar 2026 16:27:20 -1000 Subject: [PATCH 094/166] [mqtt] Fix const-correctness for trigger constructors (#15093) --- esphome/components/mqtt/mqtt_client.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/mqtt/mqtt_client.h b/esphome/components/mqtt/mqtt_client.h index 0d52d98d2f..14473f737a 100644 --- a/esphome/components/mqtt/mqtt_client.h +++ b/esphome/components/mqtt/mqtt_client.h @@ -366,14 +366,14 @@ class MQTTJsonMessageTrigger : public Trigger { class MQTTConnectTrigger : public Trigger { public: - explicit MQTTConnectTrigger(MQTTClientComponent *&client) { + explicit MQTTConnectTrigger(MQTTClientComponent *client) { client->set_on_connect([this](bool session_present) { this->trigger(session_present); }); } }; class MQTTDisconnectTrigger : public Trigger { public: - explicit MQTTDisconnectTrigger(MQTTClientComponent *&client) { + explicit MQTTDisconnectTrigger(MQTTClientComponent *client) { client->set_on_disconnect([this](MQTTClientDisconnectReason reason) { this->trigger(reason); }); } }; From 8a3b5a8defe9d2f58ce9b1ca4447e2d0bfe8f77e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Mar 2026 17:09:23 -1000 Subject: [PATCH 095/166] [core] Fix placement new storage name for templated types (#15096) --- esphome/cpp_generator.py | 32 ++++++++++++++++++++++++-------- tests/unit_tests/test_codegen.py | 27 +++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 8 deletions(-) diff --git a/esphome/cpp_generator.py b/esphome/cpp_generator.py index e97bd71a48..a8efe96cce 100644 --- a/esphome/cpp_generator.py +++ b/esphome/cpp_generator.py @@ -565,6 +565,29 @@ def new_variable( return obj +def _extract_component_ns(type_str: str) -> str: + """Extract the component namespace from a fully-qualified C++ type string. + + Strips leading ``esphome::`` and template arguments, then returns + the first namespace segment. Falls back to ``"esphome"`` when the + type has no namespace qualifier (after stripping templates). + + Examples:: + + esphome::dsmr::Dsmr -> dsmr + esphome::logger::Logger -> logger + esphome::Automation, std::optional> -> esphome + Logger -> esphome + """ + bare = type_str.removeprefix("esphome::") + # Strip template arguments before namespace extraction to avoid + # matching :: inside template params (e.g. Automation>) + bare_no_template = bare.split("<", maxsplit=1)[0] + if "::" in bare_no_template: + return bare_no_template.split("::", maxsplit=1)[0].rstrip("_") + return "esphome" + + def Pvariable(id_: ID, rhs: SafeExpType, type_: "MockObj" = None) -> "MockObj": """Declare a new pointer variable in the code generation. @@ -585,14 +608,7 @@ def Pvariable(id_: ID, rhs: SafeExpType, type_: "MockObj" = None) -> "MockObj": # to avoid heap fragmentation on embedded devices. the_type = id_.type # Extract component namespace from type for memory analysis attribution - type_str = str(the_type) - # Strip leading esphome:: to get the component namespace - # e.g. esphome::dsmr::Dsmr -> dsmr, logger::Logger -> logger - bare = type_str.removeprefix("esphome::") - if "::" in bare: - component_ns = bare.split("::", maxsplit=1)[0].rstrip("_") - else: - component_ns = "esphome" + component_ns = _extract_component_ns(str(the_type)) storage_name = f"{component_ns}__{id_.id}__pstorage" # Declare aligned byte array for the object storage diff --git a/tests/unit_tests/test_codegen.py b/tests/unit_tests/test_codegen.py index 3f32a117ff..8d01fef7c2 100644 --- a/tests/unit_tests/test_codegen.py +++ b/tests/unit_tests/test_codegen.py @@ -1,6 +1,7 @@ import pytest from esphome import codegen as cg +from esphome.cpp_generator import _extract_component_ns # Test interface remains the same. @@ -75,3 +76,29 @@ from esphome import codegen as cg ) def test_exists(attr): assert hasattr(cg, attr) + + +@pytest.mark.parametrize( + ("type_str", "expected"), + ( + ("esphome::dsmr::Dsmr", "dsmr"), + ("esphome::logger::Logger", "logger"), + ("esphome::web_server::WebServer", "web_server"), + ("esphome::deep_sleep::DeepSleep", "deep_sleep"), + ("esphome::Component", "esphome"), + ("Logger", "esphome"), + # Template types with :: in template args must not confuse extraction + ( + "esphome::Automation, std::optional>", + "esphome", + ), + ( + "esphome::StatelessLambdaAction, std::optional>", + "esphome", + ), + # Namespaced template type + ("esphome::sensor::Sensor", "sensor"), + ), +) +def test_extract_component_ns(type_str, expected): + assert _extract_component_ns(type_str) == expected From 597bb185432ac4b5c069bf7936ff9a425af94641 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Mar 2026 17:30:57 -1000 Subject: [PATCH 096/166] [benchmark] Add binary sensor publish and sensor filter benchmarks (#15035) --- .../components/binary_sensor/__init__.py | 5 ++ .../bench_binary_sensor_publish.cpp | 61 +++++++++++++++ .../components/binary_sensor/benchmark.yaml | 1 + .../benchmarks/components/sensor/__init__.py | 12 +++ .../components/sensor/bench_sensor_filter.cpp | 78 +++++++++++++++++++ .../components/sensor/benchmark.yaml | 1 + 6 files changed, 158 insertions(+) create mode 100644 tests/benchmarks/components/binary_sensor/__init__.py create mode 100644 tests/benchmarks/components/binary_sensor/bench_binary_sensor_publish.cpp create mode 100644 tests/benchmarks/components/binary_sensor/benchmark.yaml create mode 100644 tests/benchmarks/components/sensor/__init__.py create mode 100644 tests/benchmarks/components/sensor/bench_sensor_filter.cpp create mode 100644 tests/benchmarks/components/sensor/benchmark.yaml diff --git a/tests/benchmarks/components/binary_sensor/__init__.py b/tests/benchmarks/components/binary_sensor/__init__.py new file mode 100644 index 0000000000..b08f67a095 --- /dev/null +++ b/tests/benchmarks/components/binary_sensor/__init__.py @@ -0,0 +1,5 @@ +from tests.testing_helpers import ComponentManifestOverride + + +def override_manifest(manifest: ComponentManifestOverride) -> None: + manifest.enable_codegen() diff --git a/tests/benchmarks/components/binary_sensor/bench_binary_sensor_publish.cpp b/tests/benchmarks/components/binary_sensor/bench_binary_sensor_publish.cpp new file mode 100644 index 0000000000..8bae943e2e --- /dev/null +++ b/tests/benchmarks/components/binary_sensor/bench_binary_sensor_publish.cpp @@ -0,0 +1,61 @@ +#include + +#include "esphome/components/binary_sensor/binary_sensor.h" + +namespace esphome::binary_sensor::benchmarks { + +static constexpr int kInnerIterations = 2000; + +// Benchmark: publish_state with alternating values (forces state change every time) +static void BinarySensorPublish_Alternating(benchmark::State &state) { + BinarySensor sensor; + + // First publish to establish initial state + sensor.publish_initial_state(false); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + sensor.publish_state(i % 2 == 0); + } + benchmark::DoNotOptimize(sensor.state); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(BinarySensorPublish_Alternating); + +// Benchmark: publish_state with same value (tests dedup fast path) +static void BinarySensorPublish_NoChange(benchmark::State &state) { + BinarySensor sensor; + + sensor.publish_initial_state(true); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + sensor.publish_state(true); + } + benchmark::DoNotOptimize(sensor.state); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(BinarySensorPublish_NoChange); + +// Benchmark: publish_state with a callback registered +static void BinarySensorPublish_WithCallback(benchmark::State &state) { + BinarySensor sensor; + + int callback_count = 0; + sensor.add_on_state_callback([&callback_count](bool) { callback_count++; }); + + sensor.publish_initial_state(false); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + sensor.publish_state(i % 2 == 0); + } + benchmark::DoNotOptimize(callback_count); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(BinarySensorPublish_WithCallback); + +} // namespace esphome::binary_sensor::benchmarks diff --git a/tests/benchmarks/components/binary_sensor/benchmark.yaml b/tests/benchmarks/components/binary_sensor/benchmark.yaml new file mode 100644 index 0000000000..fc0db6c52c --- /dev/null +++ b/tests/benchmarks/components/binary_sensor/benchmark.yaml @@ -0,0 +1 @@ +binary_sensor: diff --git a/tests/benchmarks/components/sensor/__init__.py b/tests/benchmarks/components/sensor/__init__.py new file mode 100644 index 0000000000..5a593aa8c2 --- /dev/null +++ b/tests/benchmarks/components/sensor/__init__.py @@ -0,0 +1,12 @@ +import esphome.codegen as cg +from tests.testing_helpers import ComponentManifestOverride + + +def override_manifest(manifest: ComponentManifestOverride) -> None: + # Sensor filter benchmarks need USE_SENSOR_FILTER defined. + # We use a custom to_code instead of enable_codegen() to avoid + # pulling in the full sensor component setup. + async def to_code(config): + cg.add_define("USE_SENSOR_FILTER") + + manifest.to_code = to_code diff --git a/tests/benchmarks/components/sensor/bench_sensor_filter.cpp b/tests/benchmarks/components/sensor/bench_sensor_filter.cpp new file mode 100644 index 0000000000..e4aa397690 --- /dev/null +++ b/tests/benchmarks/components/sensor/bench_sensor_filter.cpp @@ -0,0 +1,78 @@ +#include + +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/sensor/filter.h" + +namespace esphome::sensor::benchmarks { + +static constexpr int kInnerIterations = 2000; + +// Benchmark: sensor publish through a SlidingWindowMovingAverageFilter (window=5, send_every=1) +static void SensorFilter_SlidingWindowAvg(benchmark::State &state) { + Sensor sensor; + + // Create filter: window_size=5, send_every=1, send_first_at=1 + auto *filter = new SlidingWindowMovingAverageFilter(5, 1, 1); + sensor.add_filter(filter); + + float value = 0.0f; + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + sensor.publish_state(value); + value += 0.1f; + if (value > 1000.0f) + value = 0.0f; + } + benchmark::DoNotOptimize(sensor.state); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(SensorFilter_SlidingWindowAvg); + +// Benchmark: sensor publish through ExponentialMovingAverageFilter +static void SensorFilter_ExponentialMovingAvg(benchmark::State &state) { + Sensor sensor; + + // alpha=0.1, send_every=1, send_first_at=1 + auto *filter = new ExponentialMovingAverageFilter(0.1f, 1, 1); + sensor.add_filter(filter); + + float value = 0.0f; + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + sensor.publish_state(value); + value += 0.1f; + if (value > 1000.0f) + value = 0.0f; + } + benchmark::DoNotOptimize(sensor.state); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(SensorFilter_ExponentialMovingAvg); + +// Benchmark: sensor publish through a chain of 3 filters (offset + multiply + sliding window) +static void SensorFilter_Chain3(benchmark::State &state) { + Sensor sensor; + + sensor.add_filters({ + new OffsetFilter(1.0f), + new MultiplyFilter(2.0f), + new SlidingWindowMovingAverageFilter(5, 1, 1), + }); + + float value = 0.0f; + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + sensor.publish_state(value); + value += 0.1f; + if (value > 1000.0f) + value = 0.0f; + } + benchmark::DoNotOptimize(sensor.state); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(SensorFilter_Chain3); + +} // namespace esphome::sensor::benchmarks diff --git a/tests/benchmarks/components/sensor/benchmark.yaml b/tests/benchmarks/components/sensor/benchmark.yaml new file mode 100644 index 0000000000..e1fb52cdd6 --- /dev/null +++ b/tests/benchmarks/components/sensor/benchmark.yaml @@ -0,0 +1 @@ +sensor: From 0de2c758aa1c42d6b9734fb410244c18bf7156f7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Mar 2026 17:31:27 -1000 Subject: [PATCH 097/166] [scheduler] Use placement-new for std::function move in set_timer_common_ (#14757) --- esphome/core/scheduler.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index 51cbfb208e..9ee3b2fdd2 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -166,7 +166,13 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type item->component = component; item->set_name(name_type, static_name, hash_or_id); item->type = type; - item->callback = std::move(func); + // Use destroy + placement-new instead of move-assignment. + // GCC's std::function::operator=(function&&) does a full swap dance even when the + // target is empty. Since recycled/new items always have an empty callback, we can + // destroy the empty one (no-op) and move-construct directly, saving ~40 bytes of + // swap/destructor code on Xtensa. + item->callback.~function(); + new (&item->callback) std::function(std::move(func)); // Reset remove flag - recycled items may have been cancelled (remove=true) in previous use this->set_item_removed_(item, false); item->is_retry = is_retry; From baf365404cb2b2d9a359718661a31f1c8d2604f5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Mar 2026 18:18:43 -1000 Subject: [PATCH 098/166] [network] Inline get_use_address() to eliminate function call overhead (#14942) --- .../ethernet/ethernet_component.cpp | 6 ----- .../components/ethernet/ethernet_component.h | 4 ++-- esphome/components/network/util.cpp | 24 ------------------- esphome/components/network/util.h | 24 ++++++++++++++++++- esphome/components/openthread/openthread.cpp | 6 ----- esphome/components/openthread/openthread.h | 4 ++-- esphome/components/wifi/wifi_component.cpp | 4 ---- esphome/components/wifi/wifi_component.h | 4 ++-- 8 files changed, 29 insertions(+), 47 deletions(-) diff --git a/esphome/components/ethernet/ethernet_component.cpp b/esphome/components/ethernet/ethernet_component.cpp index 4421a1c7aa..42cb0b3cfc 100644 --- a/esphome/components/ethernet/ethernet_component.cpp +++ b/esphome/components/ethernet/ethernet_component.cpp @@ -18,12 +18,6 @@ void EthernetComponent::set_type(EthernetType type) { this->type_ = type; } void EthernetComponent::set_manual_ip(const ManualIP &manual_ip) { this->manual_ip_ = manual_ip; } #endif -// set_use_address() is guaranteed to be called during component setup by Python code generation, -// so use_address_ will always be valid when get_use_address() is called - no fallback needed. -const char *EthernetComponent::get_use_address() const { return this->use_address_; } - -void EthernetComponent::set_use_address(const char *use_address) { this->use_address_ = use_address; } - #ifdef USE_ETHERNET_IP_STATE_LISTENERS void EthernetComponent::notify_ip_state_listeners_() { auto ips = this->get_ip_addresses(); diff --git a/esphome/components/ethernet/ethernet_component.h b/esphome/components/ethernet/ethernet_component.h index 88a86bc043..b6699e8020 100644 --- a/esphome/components/ethernet/ethernet_component.h +++ b/esphome/components/ethernet/ethernet_component.h @@ -103,8 +103,8 @@ class EthernetComponent final : public Component { network::IPAddresses get_ip_addresses(); network::IPAddress get_dns_address(uint8_t num); - const char *get_use_address() const; - void set_use_address(const char *use_address); + const char *get_use_address() const { return this->use_address_; } + void set_use_address(const char *use_address) { this->use_address_ = use_address; } void get_eth_mac_address_raw(uint8_t *mac); // Remove before 2026.9.0 ESPDEPRECATED("Use get_eth_mac_address_pretty_into_buffer() instead. Removed in 2026.9.0", "2026.3.0") diff --git a/esphome/components/network/util.cpp b/esphome/components/network/util.cpp index 226b11b8cd..79ddd3844c 100644 --- a/esphome/components/network/util.cpp +++ b/esphome/components/network/util.cpp @@ -42,29 +42,5 @@ network::IPAddresses get_ip_addresses() { return {}; } -const char *get_use_address() { - // Global component pointers are guaranteed to be set by component constructors when USE_* is defined -#ifdef USE_ETHERNET - return ethernet::global_eth_component->get_use_address(); -#endif - -#ifdef USE_MODEM - return modem::global_modem_component->get_use_address(); -#endif - -#ifdef USE_WIFI - return wifi::global_wifi_component->get_use_address(); -#endif - -#ifdef USE_OPENTHREAD - return openthread::global_openthread_component->get_use_address(); -#endif - -#if !defined(USE_ETHERNET) && !defined(USE_MODEM) && !defined(USE_WIFI) && !defined(USE_OPENTHREAD) - // Fallback when no network component is defined (e.g., host platform) - return ""; -#endif -} - } // namespace esphome::network #endif diff --git a/esphome/components/network/util.h b/esphome/components/network/util.h index 4b700fe74c..e4e8a01f8c 100644 --- a/esphome/components/network/util.h +++ b/esphome/components/network/util.h @@ -54,7 +54,29 @@ ESPHOME_ALWAYS_INLINE inline bool is_connected() { /// Return whether the network is disabled (only wifi for now) bool is_disabled(); /// Get the active network hostname -const char *get_use_address(); +ESPHOME_ALWAYS_INLINE inline const char *get_use_address() { + // Global component pointers are guaranteed to be set by component constructors when USE_* is defined +#ifdef USE_ETHERNET + return ethernet::global_eth_component->get_use_address(); +#endif + +#ifdef USE_MODEM + return modem::global_modem_component->get_use_address(); +#endif + +#ifdef USE_WIFI + return wifi::global_wifi_component->get_use_address(); +#endif + +#ifdef USE_OPENTHREAD + return openthread::global_openthread_component->get_use_address(); +#endif + +#if !defined(USE_ETHERNET) && !defined(USE_MODEM) && !defined(USE_WIFI) && !defined(USE_OPENTHREAD) + // Fallback when no network component is defined (e.g., host platform) + return ""; +#endif +} IPAddresses get_ip_addresses(); } // namespace esphome::network diff --git a/esphome/components/openthread/openthread.cpp b/esphome/components/openthread/openthread.cpp index 1596b6e990..7c9a308303 100644 --- a/esphome/components/openthread/openthread.cpp +++ b/esphome/components/openthread/openthread.cpp @@ -257,11 +257,5 @@ void OpenThreadComponent::on_factory_reset(std::function callback) { ESP_LOGD(TAG, "Waiting on Confirmation Removal SRP Host and Services"); } -// set_use_address() is guaranteed to be called during component setup by Python code generation, -// so use_address_ will always be valid when get_use_address() is called - no fallback needed. -const char *OpenThreadComponent::get_use_address() const { return this->use_address_; } - -void OpenThreadComponent::set_use_address(const char *use_address) { this->use_address_ = use_address; } - } // namespace esphome::openthread #endif diff --git a/esphome/components/openthread/openthread.h b/esphome/components/openthread/openthread.h index bd10774fcf..b42fdd2d30 100644 --- a/esphome/components/openthread/openthread.h +++ b/esphome/components/openthread/openthread.h @@ -37,8 +37,8 @@ class OpenThreadComponent : public Component { void on_factory_reset(std::function callback); void defer_factory_reset_external_callback(); - const char *get_use_address() const; - void set_use_address(const char *use_address); + const char *get_use_address() const { return this->use_address_; } + void set_use_address(const char *use_address) { this->use_address_ = use_address; } #if CONFIG_OPENTHREAD_MTD void set_poll_period(uint32_t poll_period) { this->poll_period_ = poll_period; } #endif diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 08065a7544..0fd1385258 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -891,10 +891,6 @@ network::IPAddress WiFiComponent::get_dns_address(int num) { return this->wifi_dns_ip_(num); return {}; } -// set_use_address() is guaranteed to be called during component setup by Python code generation, -// so use_address_ will always be valid when get_use_address() is called - no fallback needed. -const char *WiFiComponent::get_use_address() const { return this->use_address_; } -void WiFiComponent::set_use_address(const char *use_address) { this->use_address_ = use_address; } #ifdef USE_WIFI_AP void WiFiComponent::setup_ap_config_() { diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index bd202604d6..718f4a6e12 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -481,8 +481,8 @@ class WiFiComponent final : public Component { network::IPAddress get_dns_address(int num); network::IPAddresses get_ip_addresses(); - const char *get_use_address() const; - void set_use_address(const char *use_address); + const char *get_use_address() const { return this->use_address_; } + void set_use_address(const char *use_address) { this->use_address_ = use_address; } const wifi_scan_vector_t &get_scan_result() const { return scan_result_; } From e67b5a78d0b4fd4d06ddc347141eb4e19c7d4f10 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Mar 2026 20:51:40 -1000 Subject: [PATCH 099/166] [esp32] Patch DRAM segment for testing mode to fix grouped component test overflow (#15102) --- esphome/components/esp32/iram_fix.py.script | 70 ++++++++++++--------- 1 file changed, 42 insertions(+), 28 deletions(-) diff --git a/esphome/components/esp32/iram_fix.py.script b/esphome/components/esp32/iram_fix.py.script index 0d23f9a81b..b656d7d1a6 100644 --- a/esphome/components/esp32/iram_fix.py.script +++ b/esphome/components/esp32/iram_fix.py.script @@ -4,12 +4,40 @@ import re # pylint: disable=E0602 Import("env") # noqa -# IRAM size for testing mode (2MB - large enough to accommodate grouped tests) -TESTING_IRAM_SIZE = 0x200000 +# Memory sizes for testing mode (large enough to accommodate grouped tests) +TESTING_IRAM_SIZE = 0x200000 # 2MB +TESTING_DRAM_SIZE = 0x200000 # 2MB + + +def patch_segment(content, segment_name, new_size): + """Patch a memory segment's length in linker script content. + + Handles both single-line and multi-line segment definitions, e.g.: + iram0_0_seg (RX) : org = 0x40080000, len = 0x20000 + 0x0 + or split across lines: + dram0_0_seg (RW) : org = 0x3FFB0000 + 0xdb5c, + len = 0x2c200 - 0xdb5c + + Args: + content: Full linker script content as string + segment_name: Name of the segment (e.g., 'iram0_0_seg') + new_size: New size as integer + + Returns: + Tuple of (new_content, was_patched) + """ + # Match segment name through to "len = " allowing newlines between org and len + pattern = rf'({re.escape(segment_name)}\s*\([^)]*\)\s*:\s*org\s*=\s*.+?,\s*len\s*=\s*)(\S+[^\n]*)' + if match := re.search(pattern, content, re.DOTALL): + replacement = f"{match.group(1)}{new_size:#x}" + new_content = content[:match.start()] + replacement + content[match.end():] + if new_content != content: + return new_content, True + return content, False def patch_idf_linker_script(source, target, env): - """Patch ESP-IDF linker script to increase IRAM size for testing mode.""" + """Patch ESP-IDF linker script to increase IRAM and DRAM size for testing mode.""" # Check if we're in testing mode by looking for the define build_flags = env.get("BUILD_FLAGS", []) testing_mode = any("-DESPHOME_TESTING_MODE" in flag for flag in build_flags) @@ -34,36 +62,22 @@ def patch_idf_linker_script(source, target, env): print(f"ESPHome: Error reading linker script: {e}") return - # Check if this file contains iram0_0_seg - if 'iram0_0_seg' not in content: - print(f"ESPHome: Warning - iram0_0_seg not found in {memory_ld}") - return + patches = [] - # Look for iram0_0_seg definition and increase its length - # ESP-IDF format can be: - # iram0_0_seg (RX) : org = 0x40080000, len = 0x20000 + 0x0 - # or more complex with nested parentheses: - # iram0_0_seg (RX) : org = (0x40370000 + 0x4000), len = (((0x403CB700 - (0x40378000 - 0x3FC88000)) - 0x3FC88000) + 0x8000 - 0x4000) - # We want to change len to TESTING_IRAM_SIZE for testing + content, patched = patch_segment(content, 'iram0_0_seg', TESTING_IRAM_SIZE) + if patched: + patches.append(f"IRAM={TESTING_IRAM_SIZE:#x}") - # Use a more robust approach: find the line and manually parse it - lines = content.split('\n') - for i, line in enumerate(lines): - if 'iram0_0_seg' in line and 'len' in line: - # Find the position of "len = " and replace everything after it until the end of the statement - match = re.search(r'(iram0_0_seg\s*\([^)]*\)\s*:\s*org\s*=\s*(?:\([^)]+\)|0x[0-9a-fA-F]+)\s*,\s*len\s*=\s*)(.+?)(\s*)$', line) - if match: - lines[i] = f"{match.group(1)}{TESTING_IRAM_SIZE:#x}{match.group(3)}" - break + content, patched = patch_segment(content, 'dram0_0_seg', TESTING_DRAM_SIZE) + if patched: + patches.append(f"DRAM={TESTING_DRAM_SIZE:#x}") - updated = '\n'.join(lines) - - if updated != content: + if patches: with open(memory_ld, "w") as f: - f.write(updated) - print(f"ESPHome: Patched IRAM size to {TESTING_IRAM_SIZE:#x} in {memory_ld} for testing mode") + f.write(content) + print(f"ESPHome: Patched {', '.join(patches)} in {memory_ld} for testing mode") else: - print(f"ESPHome: Warning - could not patch iram0_0_seg in {memory_ld}") + print(f"ESPHome: Warning - could not patch memory segments in {memory_ld}") # Hook into the build process before linking From 225330413a137acbff12e81127b983ad05d92e00 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Mon, 23 Mar 2026 01:55:14 -0500 Subject: [PATCH 100/166] [uart] Rename `FlushResult` to `UARTFlushResult` with `UART_FLUSH_RESULT_` prefix (#15101) Co-authored-by: Claude Sonnet 4.6 --- esphome/components/api/api_connection.cpp | 8 ++++---- esphome/components/ble_nus/ble_nus.cpp | 6 +++--- esphome/components/ble_nus/ble_nus.h | 2 +- .../components/serial_proxy/serial_proxy.cpp | 2 +- .../components/serial_proxy/serial_proxy.h | 2 +- esphome/components/uart/uart.h | 2 +- esphome/components/uart/uart_component.h | 19 +++++++------------ .../uart/uart_component_esp8266.cpp | 4 ++-- .../components/uart/uart_component_esp8266.h | 2 +- .../uart/uart_component_esp_idf.cpp | 8 ++++---- .../components/uart/uart_component_esp_idf.h | 2 +- .../components/uart/uart_component_host.cpp | 6 +++--- esphome/components/uart/uart_component_host.h | 2 +- .../uart/uart_component_libretiny.cpp | 4 ++-- .../uart/uart_component_libretiny.h | 2 +- .../components/uart/uart_component_rp2040.cpp | 4 ++-- .../components/uart/uart_component_rp2040.h | 2 +- esphome/components/usb_cdc_acm/usb_cdc_acm.h | 2 +- .../usb_cdc_acm/usb_cdc_acm_esp32.cpp | 10 +++++----- esphome/components/usb_uart/usb_uart.cpp | 6 +++--- esphome/components/usb_uart/usb_uart.h | 2 +- esphome/components/weikai/weikai.cpp | 6 +++--- esphome/components/weikai/weikai.h | 2 +- tests/components/ld2450/common.h | 2 +- tests/components/uart/common.h | 2 +- .../uart_mock/uart_mock.cpp | 4 ++-- .../external_components/uart_mock/uart_mock.h | 2 +- 27 files changed, 55 insertions(+), 60 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 40c27b224b..82d7e3f674 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1519,16 +1519,16 @@ void APIConnection::on_serial_proxy_request(const SerialProxyRequest &msg) { resp.instance = msg.instance; resp.type = enums::SERIAL_PROXY_REQUEST_TYPE_FLUSH; switch (proxies[msg.instance]->flush_port()) { - case uart::FlushResult::SUCCESS: + case uart::UARTFlushResult::UART_FLUSH_RESULT_SUCCESS: resp.status = enums::SERIAL_PROXY_STATUS_OK; break; - case uart::FlushResult::ASSUMED_SUCCESS: + case uart::UARTFlushResult::UART_FLUSH_RESULT_ASSUMED_SUCCESS: resp.status = enums::SERIAL_PROXY_STATUS_ASSUMED_SUCCESS; break; - case uart::FlushResult::TIMEOUT: + case uart::UARTFlushResult::UART_FLUSH_RESULT_TIMEOUT: resp.status = enums::SERIAL_PROXY_STATUS_TIMEOUT; break; - case uart::FlushResult::FAILED: + case uart::UARTFlushResult::UART_FLUSH_RESULT_FAILED: resp.status = enums::SERIAL_PROXY_STATUS_ERROR; break; } diff --git a/esphome/components/ble_nus/ble_nus.cpp b/esphome/components/ble_nus/ble_nus.cpp index 2f60f81471..71d98332e0 100644 --- a/esphome/components/ble_nus/ble_nus.cpp +++ b/esphome/components/ble_nus/ble_nus.cpp @@ -103,17 +103,17 @@ size_t BLENUS::available() { #endif } -uart::FlushResult BLENUS::flush() { +uart::UARTFlushResult BLENUS::flush() { constexpr uint32_t timeout_500ms = 500; uint32_t start = millis(); while (atomic_get(&this->tx_status_) != TX_DISABLED && !ring_buf_is_empty(&global_ble_tx_ring_buf)) { if (millis() - start > timeout_500ms) { ESP_LOGW(TAG, "Flush timeout"); - return uart::FlushResult::TIMEOUT; + return uart::UARTFlushResult::UART_FLUSH_RESULT_TIMEOUT; } delay(1); } - return uart::FlushResult::SUCCESS; + return uart::UARTFlushResult::UART_FLUSH_RESULT_SUCCESS; } void BLENUS::connected(bt_conn *conn, uint8_t err) { diff --git a/esphome/components/ble_nus/ble_nus.h b/esphome/components/ble_nus/ble_nus.h index b482c240e5..f1afd54af9 100644 --- a/esphome/components/ble_nus/ble_nus.h +++ b/esphome/components/ble_nus/ble_nus.h @@ -26,7 +26,7 @@ class BLENUS : public uart::UARTComponent, public Component { bool peek_byte(uint8_t *data) override; bool read_array(uint8_t *data, size_t len) override; size_t available() override; - uart::FlushResult flush() override; + uart::UARTFlushResult flush() override; void check_logger_conflict() override {} void set_expose_log(bool expose_log) { this->expose_log_ = expose_log; } #ifdef USE_LOGGER diff --git a/esphome/components/serial_proxy/serial_proxy.cpp b/esphome/components/serial_proxy/serial_proxy.cpp index 00d822b75c..f3c256c62a 100644 --- a/esphome/components/serial_proxy/serial_proxy.cpp +++ b/esphome/components/serial_proxy/serial_proxy.cpp @@ -165,7 +165,7 @@ uint32_t SerialProxy::get_modem_pins() const { (this->dtr_state_ ? SERIAL_PROXY_LINE_STATE_FLAG_DTR : 0u); } -uart::FlushResult SerialProxy::flush_port() { +uart::UARTFlushResult SerialProxy::flush_port() { ESP_LOGV(TAG, "Flushing serial proxy [%u]", this->instance_index_); return this->flush(); } diff --git a/esphome/components/serial_proxy/serial_proxy.h b/esphome/components/serial_proxy/serial_proxy.h index 5adfa4fe53..c435787a61 100644 --- a/esphome/components/serial_proxy/serial_proxy.h +++ b/esphome/components/serial_proxy/serial_proxy.h @@ -92,7 +92,7 @@ class SerialProxy : public uart::UARTDevice, public Component { uint32_t get_modem_pins() const; /// Flush the serial port (block until all TX data is sent) - uart::FlushResult flush_port(); + uart::UARTFlushResult flush_port(); /// Set the RTS GPIO pin (from YAML configuration) void set_rts_pin(GPIOPin *pin) { this->rts_pin_ = pin; } diff --git a/esphome/components/uart/uart.h b/esphome/components/uart/uart.h index 2c4fb34c9a..899d349e21 100644 --- a/esphome/components/uart/uart.h +++ b/esphome/components/uart/uart.h @@ -45,7 +45,7 @@ class UARTDevice { size_t available() { return this->parent_->available(); } - FlushResult flush() { return this->parent_->flush(); } + UARTFlushResult flush() { return this->parent_->flush(); } // Compat APIs int read() { diff --git a/esphome/components/uart/uart_component.h b/esphome/components/uart/uart_component.h index 00a4c78878..abc77fbae8 100644 --- a/esphome/components/uart/uart_component.h +++ b/esphome/components/uart/uart_component.h @@ -30,17 +30,12 @@ enum UARTDirection { const LogString *parity_to_str(UARTParityOptions parity); /// Result of a flush() call. -// Some vendor SDKs (e.g., Realtek) define SUCCESS as a macro. -// Save and restore around the enum to avoid collisions with our scoped enum value. -#pragma push_macro("SUCCESS") -#undef SUCCESS -enum class FlushResult { - SUCCESS, ///< Confirmed: all bytes left the TX FIFO. - TIMEOUT, ///< Confirmed: timed out before TX completed. - FAILED, ///< Confirmed: driver or hardware error. - ASSUMED_SUCCESS, ///< Platform cannot report result; success is assumed. +enum class UARTFlushResult { + UART_FLUSH_RESULT_SUCCESS, ///< Confirmed: all bytes left the TX FIFO. + UART_FLUSH_RESULT_TIMEOUT, ///< Confirmed: timed out before TX completed. + UART_FLUSH_RESULT_FAILED, ///< Confirmed: driver or hardware error. + UART_FLUSH_RESULT_ASSUMED_SUCCESS, ///< Platform cannot report result; success is assumed. }; -#pragma pop_macro("SUCCESS") class UARTComponent { public: @@ -87,8 +82,8 @@ class UARTComponent { virtual size_t available() = 0; // Pure virtual method to block until all bytes have been written to the UART bus. - // @return FlushResult indicating whether the flush was confirmed, timed out, failed, or assumed successful. - virtual FlushResult flush() = 0; + // @return UARTFlushResult indicating whether the flush was confirmed, timed out, failed, or assumed successful. + virtual UARTFlushResult flush() = 0; // Sets the maximum time to wait for TX to drain during flush(). // Only meaningful on ESP32 (IDF). Other platforms ignore this value. diff --git a/esphome/components/uart/uart_component_esp8266.cpp b/esphome/components/uart/uart_component_esp8266.cpp index 91218c4300..0ea7930760 100644 --- a/esphome/components/uart/uart_component_esp8266.cpp +++ b/esphome/components/uart/uart_component_esp8266.cpp @@ -213,14 +213,14 @@ size_t ESP8266UartComponent::available() { return this->sw_serial_->available(); } } -FlushResult ESP8266UartComponent::flush() { +UARTFlushResult ESP8266UartComponent::flush() { ESP_LOGVV(TAG, " Flushing"); if (this->hw_serial_ != nullptr) { this->hw_serial_->flush(); } else { this->sw_serial_->flush(); } - return FlushResult::ASSUMED_SUCCESS; + return UARTFlushResult::UART_FLUSH_RESULT_ASSUMED_SUCCESS; } void ESP8266SoftwareSerial::setup(InternalGPIOPin *tx_pin, InternalGPIOPin *rx_pin, uint32_t baud_rate, uint8_t stop_bits, uint32_t data_bits, UARTParityOptions parity, diff --git a/esphome/components/uart/uart_component_esp8266.h b/esphome/components/uart/uart_component_esp8266.h index ca90dc5964..7f844d9b65 100644 --- a/esphome/components/uart/uart_component_esp8266.h +++ b/esphome/components/uart/uart_component_esp8266.h @@ -58,7 +58,7 @@ class ESP8266UartComponent : public UARTComponent, public Component { bool read_array(uint8_t *data, size_t len) override; size_t available() override; - FlushResult flush() override; + UARTFlushResult flush() override; uint32_t get_config(); diff --git a/esphome/components/uart/uart_component_esp_idf.cpp b/esphome/components/uart/uart_component_esp_idf.cpp index 8168e49805..bd2f915d3a 100644 --- a/esphome/components/uart/uart_component_esp_idf.cpp +++ b/esphome/components/uart/uart_component_esp_idf.cpp @@ -360,15 +360,15 @@ size_t IDFUARTComponent::available() { return available; } -FlushResult IDFUARTComponent::flush() { +UARTFlushResult IDFUARTComponent::flush() { ESP_LOGVV(TAG, " Flushing"); TickType_t ticks = this->flush_timeout_ms_ == 0 ? portMAX_DELAY : pdMS_TO_TICKS(this->flush_timeout_ms_); esp_err_t err = uart_wait_tx_done(this->uart_num_, ticks); if (err == ESP_OK) - return FlushResult::SUCCESS; + return UARTFlushResult::UART_FLUSH_RESULT_SUCCESS; if (err == ESP_ERR_TIMEOUT) - return FlushResult::TIMEOUT; - return FlushResult::FAILED; + return UARTFlushResult::UART_FLUSH_RESULT_TIMEOUT; + return UARTFlushResult::UART_FLUSH_RESULT_FAILED; } void IDFUARTComponent::check_logger_conflict() {} diff --git a/esphome/components/uart/uart_component_esp_idf.h b/esphome/components/uart/uart_component_esp_idf.h index 9fa2013cfd..ec4f2884b2 100644 --- a/esphome/components/uart/uart_component_esp_idf.h +++ b/esphome/components/uart/uart_component_esp_idf.h @@ -31,7 +31,7 @@ class IDFUARTComponent : public UARTComponent, public Component { bool read_array(uint8_t *data, size_t len) override; size_t available() override; - FlushResult flush() override; + UARTFlushResult flush() override; void set_flush_timeout(uint32_t flush_timeout_ms) override { this->flush_timeout_ms_ = flush_timeout_ms; } diff --git a/esphome/components/uart/uart_component_host.cpp b/esphome/components/uart/uart_component_host.cpp index 66026f3ccd..0042ffae23 100644 --- a/esphome/components/uart/uart_component_host.cpp +++ b/esphome/components/uart/uart_component_host.cpp @@ -274,13 +274,13 @@ size_t HostUartComponent::available() { return result; }; -FlushResult HostUartComponent::flush() { +UARTFlushResult HostUartComponent::flush() { if (this->file_descriptor_ == -1) { - return FlushResult::ASSUMED_SUCCESS; + return UARTFlushResult::UART_FLUSH_RESULT_ASSUMED_SUCCESS; } tcflush(this->file_descriptor_, TCIOFLUSH); ESP_LOGV(TAG, " Flushing"); - return FlushResult::ASSUMED_SUCCESS; + return UARTFlushResult::UART_FLUSH_RESULT_ASSUMED_SUCCESS; } void HostUartComponent::update_error_(const std::string &error) { diff --git a/esphome/components/uart/uart_component_host.h b/esphome/components/uart/uart_component_host.h index c22efdcb92..56ff525bc3 100644 --- a/esphome/components/uart/uart_component_host.h +++ b/esphome/components/uart/uart_component_host.h @@ -18,7 +18,7 @@ class HostUartComponent : public UARTComponent, public Component { bool peek_byte(uint8_t *data) override; bool read_array(uint8_t *data, size_t len) override; size_t available() override; - FlushResult flush() override; + UARTFlushResult flush() override; void set_name(std::string port_name) { port_name_ = port_name; }; protected: diff --git a/esphome/components/uart/uart_component_libretiny.cpp b/esphome/components/uart/uart_component_libretiny.cpp index 6a550f296a..4172e7c164 100644 --- a/esphome/components/uart/uart_component_libretiny.cpp +++ b/esphome/components/uart/uart_component_libretiny.cpp @@ -170,10 +170,10 @@ bool LibreTinyUARTComponent::read_array(uint8_t *data, size_t len) { } size_t LibreTinyUARTComponent::available() { return this->serial_->available(); } -FlushResult LibreTinyUARTComponent::flush() { +UARTFlushResult LibreTinyUARTComponent::flush() { ESP_LOGVV(TAG, " Flushing"); this->serial_->flush(); - return FlushResult::ASSUMED_SUCCESS; + return UARTFlushResult::UART_FLUSH_RESULT_ASSUMED_SUCCESS; } void LibreTinyUARTComponent::check_logger_conflict() { diff --git a/esphome/components/uart/uart_component_libretiny.h b/esphome/components/uart/uart_component_libretiny.h index 77df808067..872ea86601 100644 --- a/esphome/components/uart/uart_component_libretiny.h +++ b/esphome/components/uart/uart_component_libretiny.h @@ -22,7 +22,7 @@ class LibreTinyUARTComponent : public UARTComponent, public Component { bool read_array(uint8_t *data, size_t len) override; size_t available() override; - FlushResult flush() override; + UARTFlushResult flush() override; uint16_t get_config(); diff --git a/esphome/components/uart/uart_component_rp2040.cpp b/esphome/components/uart/uart_component_rp2040.cpp index 6f6f1fb96b..1aaf98dc84 100644 --- a/esphome/components/uart/uart_component_rp2040.cpp +++ b/esphome/components/uart/uart_component_rp2040.cpp @@ -208,10 +208,10 @@ bool RP2040UartComponent::read_array(uint8_t *data, size_t len) { return true; } size_t RP2040UartComponent::available() { return this->serial_->available(); } -FlushResult RP2040UartComponent::flush() { +UARTFlushResult RP2040UartComponent::flush() { ESP_LOGVV(TAG, " Flushing"); this->serial_->flush(); - return FlushResult::ASSUMED_SUCCESS; + return UARTFlushResult::UART_FLUSH_RESULT_ASSUMED_SUCCESS; } } // namespace esphome::uart diff --git a/esphome/components/uart/uart_component_rp2040.h b/esphome/components/uart/uart_component_rp2040.h index 891212ca74..198c698af9 100644 --- a/esphome/components/uart/uart_component_rp2040.h +++ b/esphome/components/uart/uart_component_rp2040.h @@ -25,7 +25,7 @@ class RP2040UartComponent : public UARTComponent, public Component { bool read_array(uint8_t *data, size_t len) override; size_t available() override; - FlushResult flush() override; + UARTFlushResult flush() override; uint16_t get_config(); diff --git a/esphome/components/usb_cdc_acm/usb_cdc_acm.h b/esphome/components/usb_cdc_acm/usb_cdc_acm.h index a56abc9ee8..89405ab893 100644 --- a/esphome/components/usb_cdc_acm/usb_cdc_acm.h +++ b/esphome/components/usb_cdc_acm/usb_cdc_acm.h @@ -82,7 +82,7 @@ class USBCDCACMInstance : public uart::UARTComponent, public Parentedhas_peek_ ? 1 : 0); } -uart::FlushResult USBCDCACMInstance::flush() { +uart::UARTFlushResult USBCDCACMInstance::flush() { // Wait for TX ring buffer to be empty if (this->usb_tx_ringbuf_ == nullptr) { - return uart::FlushResult::ASSUMED_SUCCESS; + return uart::UARTFlushResult::UART_FLUSH_RESULT_ASSUMED_SUCCESS; } UBaseType_t waiting = 1; @@ -342,10 +342,10 @@ uart::FlushResult USBCDCACMInstance::flush() { // Also wait for USB to finish transmitting esp_err_t err = tinyusb_cdcacm_write_flush(static_cast(this->itf_), pdMS_TO_TICKS(100)); if (err == ESP_OK) - return uart::FlushResult::SUCCESS; + return uart::UARTFlushResult::UART_FLUSH_RESULT_SUCCESS; if (err == ESP_ERR_TIMEOUT) - return uart::FlushResult::TIMEOUT; - return uart::FlushResult::FAILED; + return uart::UARTFlushResult::UART_FLUSH_RESULT_TIMEOUT; + return uart::UARTFlushResult::UART_FLUSH_RESULT_FAILED; } void USBCDCACMInstance::check_logger_conflict() {} diff --git a/esphome/components/usb_uart/usb_uart.cpp b/esphome/components/usb_uart/usb_uart.cpp index a5d312f191..0b8589f671 100644 --- a/esphome/components/usb_uart/usb_uart.cpp +++ b/esphome/components/usb_uart/usb_uart.cpp @@ -169,7 +169,7 @@ void USBUartChannel::write_array(const uint8_t *data, size_t len) { this->parent_->start_output(this); } -uart::FlushResult USBUartChannel::flush() { +uart::UARTFlushResult USBUartChannel::flush() { // Spin until the output queue is drained and the last USB transfer completes. // Safe to call from the main loop only. // The flush_timeout_ms_ timeout guards against a device that stops responding mid-flush; @@ -181,8 +181,8 @@ uart::FlushResult USBUartChannel::flush() { yield(); } if (!this->output_queue_.empty() || this->output_started_.load()) - return uart::FlushResult::TIMEOUT; - return uart::FlushResult::SUCCESS; + return uart::UARTFlushResult::UART_FLUSH_RESULT_TIMEOUT; + return uart::UARTFlushResult::UART_FLUSH_RESULT_SUCCESS; } bool USBUartChannel::peek_byte(uint8_t *data) { diff --git a/esphome/components/usb_uart/usb_uart.h b/esphome/components/usb_uart/usb_uart.h index 7a06b04f11..8a47f0cf4b 100644 --- a/esphome/components/usb_uart/usb_uart.h +++ b/esphome/components/usb_uart/usb_uart.h @@ -140,7 +140,7 @@ class USBUartChannel : public uart::UARTComponent, public Parentedinput_buffer_.get_available(); } - uart::FlushResult flush() override; + uart::UARTFlushResult flush() override; void check_logger_conflict() override {} void set_parity(UARTParityOptions parity) { this->parity_ = parity; } void set_debug(bool debug) { this->debug_ = debug; } diff --git a/esphome/components/weikai/weikai.cpp b/esphome/components/weikai/weikai.cpp index f01d164e9f..2ec5632691 100644 --- a/esphome/components/weikai/weikai.cpp +++ b/esphome/components/weikai/weikai.cpp @@ -433,16 +433,16 @@ void WeikaiChannel::write_array(const uint8_t *buffer, size_t length) { this->reg(0).write_fifo(const_cast(buffer), length); } -uart::FlushResult WeikaiChannel::flush() { +uart::UARTFlushResult WeikaiChannel::flush() { uint32_t const start_time = millis(); while (this->tx_fifo_is_not_empty_()) { // wait until buffer empty if (millis() - start_time > 200) { ESP_LOGW(TAG, "WARNING flush timeout - still %d bytes not sent after 200 ms", this->tx_in_fifo_()); - return uart::FlushResult::TIMEOUT; + return uart::UARTFlushResult::UART_FLUSH_RESULT_TIMEOUT; } yield(); // reschedule our thread to avoid blocking } - return uart::FlushResult::SUCCESS; + return uart::UARTFlushResult::UART_FLUSH_RESULT_SUCCESS; } size_t WeikaiChannel::xfer_fifo_to_buffer_() { diff --git a/esphome/components/weikai/weikai.h b/esphome/components/weikai/weikai.h index 715b82bfc7..36d8f66265 100644 --- a/esphome/components/weikai/weikai.h +++ b/esphome/components/weikai/weikai.h @@ -380,7 +380,7 @@ class WeikaiChannel : public uart::UARTComponent { /// @details If we refer to Serial.flush() in Arduino it says: ** Waits for the transmission of outgoing serial data /// to complete. (Prior to Arduino 1.0, this the method was removing any buffered incoming serial data.). ** Therefore /// we wait until all bytes are gone with a timeout of 100 ms - uart::FlushResult flush() override; + uart::UARTFlushResult flush() override; protected: friend class WeikaiComponent; diff --git a/tests/components/ld2450/common.h b/tests/components/ld2450/common.h index 9f9e7b3e9f..304634edca 100644 --- a/tests/components/ld2450/common.h +++ b/tests/components/ld2450/common.h @@ -16,7 +16,7 @@ class MockUARTComponent : public uart::UARTComponent { MOCK_METHOD(bool, read_array, (uint8_t * data, size_t len), (override)); MOCK_METHOD(bool, peek_byte, (uint8_t * data), (override)); MOCK_METHOD(size_t, available, (), (override)); - MOCK_METHOD(uart::FlushResult, flush, (), (override)); + MOCK_METHOD(uart::UARTFlushResult, flush, (), (override)); MOCK_METHOD(void, check_logger_conflict, (), (override)); }; diff --git a/tests/components/uart/common.h b/tests/components/uart/common.h index f7e2d8a3f7..de3ea3029e 100644 --- a/tests/components/uart/common.h +++ b/tests/components/uart/common.h @@ -30,7 +30,7 @@ class MockUARTComponent : public UARTComponent { MOCK_METHOD(bool, read_array, (uint8_t * data, size_t len), (override)); MOCK_METHOD(bool, peek_byte, (uint8_t * data), (override)); MOCK_METHOD(size_t, available, (), (override)); - MOCK_METHOD(FlushResult, flush, (), (override)); + MOCK_METHOD(UARTFlushResult, flush, (), (override)); MOCK_METHOD(void, check_logger_conflict, (), (override)); }; diff --git a/tests/integration/fixtures/external_components/uart_mock/uart_mock.cpp b/tests/integration/fixtures/external_components/uart_mock/uart_mock.cpp index 1a15da76d1..d0690e7515 100644 --- a/tests/integration/fixtures/external_components/uart_mock/uart_mock.cpp +++ b/tests/integration/fixtures/external_components/uart_mock/uart_mock.cpp @@ -153,9 +153,9 @@ bool MockUartComponent::read_array(uint8_t *data, size_t len) { size_t MockUartComponent::available() { return this->rx_buffer_.size(); } -uart::FlushResult MockUartComponent::flush() { +uart::UARTFlushResult MockUartComponent::flush() { // Nothing to flush in mock - return uart::FlushResult::ASSUMED_SUCCESS; + return uart::UARTFlushResult::UART_FLUSH_RESULT_ASSUMED_SUCCESS; } void MockUartComponent::set_rx_full_threshold(size_t rx_full_threshold) { diff --git a/tests/integration/fixtures/external_components/uart_mock/uart_mock.h b/tests/integration/fixtures/external_components/uart_mock/uart_mock.h index 82e3b3d563..0b3b49893d 100644 --- a/tests/integration/fixtures/external_components/uart_mock/uart_mock.h +++ b/tests/integration/fixtures/external_components/uart_mock/uart_mock.h @@ -28,7 +28,7 @@ class MockUartComponent : public uart::UARTComponent, public Component { bool peek_byte(uint8_t *data) override; bool read_array(uint8_t *data, size_t len) override; size_t available() override; - uart::FlushResult flush() override; + uart::UARTFlushResult flush() override; void set_rx_full_threshold(size_t rx_full_threshold) override; void set_rx_timeout(size_t rx_timeout) override; From f4097d5a95a37faa963ec2d8ad1588b70b259e3c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Mar 2026 20:57:40 -1000 Subject: [PATCH 101/166] [api] Devirtualize API command dispatch (#15044) --- esphome/components/api/api_connection.cpp | 2 +- esphome/components/api/api_connection.h | 156 ++++++++++++--------- esphome/components/api/api_pb2_service.cpp | 3 +- esphome/components/api/api_pb2_service.h | 134 +++++++++--------- esphome/components/api/proto.h | 23 +-- script/api_protobuf/api_protobuf.py | 13 +- 6 files changed, 164 insertions(+), 167 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 82d7e3f674..d023cd21a8 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -234,7 +234,7 @@ void APIConnection::loop() { this->last_traffic_ = now; } // read a packet - this->read_message(buffer.data_len, buffer.type, buffer.data); + this->read_message_(buffer.data_len, buffer.type, buffer.data); if (this->flags_.remove) return; } diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 85c8e777a9..3d8563b1ae 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -49,11 +49,29 @@ class APIConnection final : public APIServerConnectionBase { friend class APIServer; friend class ListEntitiesIterator; APIConnection(std::unique_ptr socket, APIServer *parent); - virtual ~APIConnection(); + ~APIConnection(); void start(); void loop(); + protected: + // read_message_ is defined here (instead of in APIServerConnectionBase) so the + // compiler can devirtualize and inline on_* handler calls within this final class. + void read_message_(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data); + + // Auth helpers defined here (not in ProtoService) so the compiler can + // devirtualize is_connection_setup()/on_no_setup_connection() calls + // within this final class. + inline bool check_connection_setup_() { + if (!this->is_connection_setup()) { + this->on_no_setup_connection(); + return false; + } + return true; + } + inline bool check_authenticated_() { return this->check_connection_setup_(); } + + public: bool send_list_info_done() { return this->schedule_message_(nullptr, ListEntitiesDoneResponse::MESSAGE_TYPE, ListEntitiesDoneResponse::ESTIMATED_SIZE); @@ -63,72 +81,72 @@ class APIConnection final : public APIServerConnectionBase { #endif #ifdef USE_COVER bool send_cover_state(cover::Cover *cover); - void on_cover_command_request(const CoverCommandRequest &msg) override; + void on_cover_command_request(const CoverCommandRequest &msg); #endif #ifdef USE_FAN bool send_fan_state(fan::Fan *fan); - void on_fan_command_request(const FanCommandRequest &msg) override; + void on_fan_command_request(const FanCommandRequest &msg); #endif #ifdef USE_LIGHT bool send_light_state(light::LightState *light); - void on_light_command_request(const LightCommandRequest &msg) override; + void on_light_command_request(const LightCommandRequest &msg); #endif #ifdef USE_SENSOR bool send_sensor_state(sensor::Sensor *sensor); #endif #ifdef USE_SWITCH bool send_switch_state(switch_::Switch *a_switch); - void on_switch_command_request(const SwitchCommandRequest &msg) override; + void on_switch_command_request(const SwitchCommandRequest &msg); #endif #ifdef USE_TEXT_SENSOR bool send_text_sensor_state(text_sensor::TextSensor *text_sensor); #endif #ifdef USE_CAMERA void set_camera_state(std::shared_ptr image); - void on_camera_image_request(const CameraImageRequest &msg) override; + void on_camera_image_request(const CameraImageRequest &msg); #endif #ifdef USE_CLIMATE bool send_climate_state(climate::Climate *climate); - void on_climate_command_request(const ClimateCommandRequest &msg) override; + void on_climate_command_request(const ClimateCommandRequest &msg); #endif #ifdef USE_NUMBER bool send_number_state(number::Number *number); - void on_number_command_request(const NumberCommandRequest &msg) override; + void on_number_command_request(const NumberCommandRequest &msg); #endif #ifdef USE_DATETIME_DATE bool send_date_state(datetime::DateEntity *date); - void on_date_command_request(const DateCommandRequest &msg) override; + void on_date_command_request(const DateCommandRequest &msg); #endif #ifdef USE_DATETIME_TIME bool send_time_state(datetime::TimeEntity *time); - void on_time_command_request(const TimeCommandRequest &msg) override; + void on_time_command_request(const TimeCommandRequest &msg); #endif #ifdef USE_DATETIME_DATETIME bool send_datetime_state(datetime::DateTimeEntity *datetime); - void on_date_time_command_request(const DateTimeCommandRequest &msg) override; + void on_date_time_command_request(const DateTimeCommandRequest &msg); #endif #ifdef USE_TEXT bool send_text_state(text::Text *text); - void on_text_command_request(const TextCommandRequest &msg) override; + void on_text_command_request(const TextCommandRequest &msg); #endif #ifdef USE_SELECT bool send_select_state(select::Select *select); - void on_select_command_request(const SelectCommandRequest &msg) override; + void on_select_command_request(const SelectCommandRequest &msg); #endif #ifdef USE_BUTTON - void on_button_command_request(const ButtonCommandRequest &msg) override; + void on_button_command_request(const ButtonCommandRequest &msg); #endif #ifdef USE_LOCK bool send_lock_state(lock::Lock *a_lock); - void on_lock_command_request(const LockCommandRequest &msg) override; + void on_lock_command_request(const LockCommandRequest &msg); #endif #ifdef USE_VALVE bool send_valve_state(valve::Valve *valve); - void on_valve_command_request(const ValveCommandRequest &msg) override; + void on_valve_command_request(const ValveCommandRequest &msg); #endif #ifdef USE_MEDIA_PLAYER bool send_media_player_state(media_player::MediaPlayer *media_player); - void on_media_player_command_request(const MediaPlayerCommandRequest &msg) override; + void on_media_player_command_request(const MediaPlayerCommandRequest &msg); #endif bool try_send_log_message(int level, const char *tag, const char *line, size_t message_len); #ifdef USE_API_HOMEASSISTANT_SERVICES @@ -138,23 +156,23 @@ class APIConnection final : public APIServerConnectionBase { this->send_message(call); } #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES - void on_homeassistant_action_response(const HomeassistantActionResponse &msg) override; + void on_homeassistant_action_response(const HomeassistantActionResponse &msg); #endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES #endif // USE_API_HOMEASSISTANT_SERVICES #ifdef USE_BLUETOOTH_PROXY - void on_subscribe_bluetooth_le_advertisements_request(const SubscribeBluetoothLEAdvertisementsRequest &msg) override; - void on_unsubscribe_bluetooth_le_advertisements_request() override; + void on_subscribe_bluetooth_le_advertisements_request(const SubscribeBluetoothLEAdvertisementsRequest &msg); + void on_unsubscribe_bluetooth_le_advertisements_request(); - void on_bluetooth_device_request(const BluetoothDeviceRequest &msg) override; - void on_bluetooth_gatt_read_request(const BluetoothGATTReadRequest &msg) override; - void on_bluetooth_gatt_write_request(const BluetoothGATTWriteRequest &msg) override; - void on_bluetooth_gatt_read_descriptor_request(const BluetoothGATTReadDescriptorRequest &msg) override; - void on_bluetooth_gatt_write_descriptor_request(const BluetoothGATTWriteDescriptorRequest &msg) override; - void on_bluetooth_gatt_get_services_request(const BluetoothGATTGetServicesRequest &msg) override; - void on_bluetooth_gatt_notify_request(const BluetoothGATTNotifyRequest &msg) override; - void on_subscribe_bluetooth_connections_free_request() override; - void on_bluetooth_scanner_set_mode_request(const BluetoothScannerSetModeRequest &msg) override; - void on_bluetooth_set_connection_params_request(const BluetoothSetConnectionParamsRequest &msg) override; + void on_bluetooth_device_request(const BluetoothDeviceRequest &msg); + void on_bluetooth_gatt_read_request(const BluetoothGATTReadRequest &msg); + void on_bluetooth_gatt_write_request(const BluetoothGATTWriteRequest &msg); + void on_bluetooth_gatt_read_descriptor_request(const BluetoothGATTReadDescriptorRequest &msg); + void on_bluetooth_gatt_write_descriptor_request(const BluetoothGATTWriteDescriptorRequest &msg); + void on_bluetooth_gatt_get_services_request(const BluetoothGATTGetServicesRequest &msg); + void on_bluetooth_gatt_notify_request(const BluetoothGATTNotifyRequest &msg); + void on_subscribe_bluetooth_connections_free_request(); + void on_bluetooth_scanner_set_mode_request(const BluetoothScannerSetModeRequest &msg); + void on_bluetooth_set_connection_params_request(const BluetoothSetConnectionParamsRequest &msg); #endif #ifdef USE_HOMEASSISTANT_TIME @@ -165,42 +183,42 @@ class APIConnection final : public APIServerConnectionBase { #endif #ifdef USE_VOICE_ASSISTANT - void on_subscribe_voice_assistant_request(const SubscribeVoiceAssistantRequest &msg) override; - void on_voice_assistant_response(const VoiceAssistantResponse &msg) override; - void on_voice_assistant_event_response(const VoiceAssistantEventResponse &msg) override; - void on_voice_assistant_audio(const VoiceAssistantAudio &msg) override; - void on_voice_assistant_timer_event_response(const VoiceAssistantTimerEventResponse &msg) override; - void on_voice_assistant_announce_request(const VoiceAssistantAnnounceRequest &msg) override; - void on_voice_assistant_configuration_request(const VoiceAssistantConfigurationRequest &msg) override; - void on_voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) override; + void on_subscribe_voice_assistant_request(const SubscribeVoiceAssistantRequest &msg); + void on_voice_assistant_response(const VoiceAssistantResponse &msg); + void on_voice_assistant_event_response(const VoiceAssistantEventResponse &msg); + void on_voice_assistant_audio(const VoiceAssistantAudio &msg); + void on_voice_assistant_timer_event_response(const VoiceAssistantTimerEventResponse &msg); + void on_voice_assistant_announce_request(const VoiceAssistantAnnounceRequest &msg); + void on_voice_assistant_configuration_request(const VoiceAssistantConfigurationRequest &msg); + void on_voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg); #endif #ifdef USE_ZWAVE_PROXY - void on_z_wave_proxy_frame(const ZWaveProxyFrame &msg) override; - void on_z_wave_proxy_request(const ZWaveProxyRequest &msg) override; + void on_z_wave_proxy_frame(const ZWaveProxyFrame &msg); + void on_z_wave_proxy_request(const ZWaveProxyRequest &msg); #endif #ifdef USE_ALARM_CONTROL_PANEL bool send_alarm_control_panel_state(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel); - void on_alarm_control_panel_command_request(const AlarmControlPanelCommandRequest &msg) override; + void on_alarm_control_panel_command_request(const AlarmControlPanelCommandRequest &msg); #endif #ifdef USE_WATER_HEATER bool send_water_heater_state(water_heater::WaterHeater *water_heater); - void on_water_heater_command_request(const WaterHeaterCommandRequest &msg) override; + void on_water_heater_command_request(const WaterHeaterCommandRequest &msg); #endif #ifdef USE_IR_RF - void on_infrared_rf_transmit_raw_timings_request(const InfraredRFTransmitRawTimingsRequest &msg) override; + void on_infrared_rf_transmit_raw_timings_request(const InfraredRFTransmitRawTimingsRequest &msg); void send_infrared_rf_receive_event(const InfraredRFReceiveEvent &msg); #endif #ifdef USE_SERIAL_PROXY - void on_serial_proxy_configure_request(const SerialProxyConfigureRequest &msg) override; - void on_serial_proxy_write_request(const SerialProxyWriteRequest &msg) override; - void on_serial_proxy_set_modem_pins_request(const SerialProxySetModemPinsRequest &msg) override; - void on_serial_proxy_get_modem_pins_request(const SerialProxyGetModemPinsRequest &msg) override; - void on_serial_proxy_request(const SerialProxyRequest &msg) override; + void on_serial_proxy_configure_request(const SerialProxyConfigureRequest &msg); + void on_serial_proxy_write_request(const SerialProxyWriteRequest &msg); + void on_serial_proxy_set_modem_pins_request(const SerialProxySetModemPinsRequest &msg); + void on_serial_proxy_get_modem_pins_request(const SerialProxyGetModemPinsRequest &msg); + void on_serial_proxy_request(const SerialProxyRequest &msg); void send_serial_proxy_data(const SerialProxyDataReceived &msg); #endif @@ -210,26 +228,26 @@ class APIConnection final : public APIServerConnectionBase { #ifdef USE_UPDATE bool send_update_state(update::UpdateEntity *update); - void on_update_command_request(const UpdateCommandRequest &msg) override; + void on_update_command_request(const UpdateCommandRequest &msg); #endif - void on_disconnect_response() override; - void on_ping_response() override { + void on_disconnect_response(); + void on_ping_response() { // we initiated ping this->flags_.sent_ping = false; } #ifdef USE_API_HOMEASSISTANT_STATES - void on_home_assistant_state_response(const HomeAssistantStateResponse &msg) override; + void on_home_assistant_state_response(const HomeAssistantStateResponse &msg); #endif #ifdef USE_HOMEASSISTANT_TIME - void on_get_time_response(const GetTimeResponse &value) override; + void on_get_time_response(const GetTimeResponse &value); #endif - void on_hello_request(const HelloRequest &msg) override; - void on_disconnect_request() override; - void on_ping_request() override; - void on_device_info_request() override; - void on_list_entities_request() override { this->begin_iterator_(ActiveIterator::LIST_ENTITIES); } - void on_subscribe_states_request() override { + void on_hello_request(const HelloRequest &msg); + void on_disconnect_request(); + void on_ping_request(); + void on_device_info_request(); + void on_list_entities_request() { this->begin_iterator_(ActiveIterator::LIST_ENTITIES); } + void on_subscribe_states_request() { this->flags_.state_subscription = true; // Start initial state iterator only if no iterator is active // If list_entities is running, we'll start initial_state when it completes @@ -237,7 +255,7 @@ class APIConnection final : public APIServerConnectionBase { this->begin_iterator_(ActiveIterator::INITIAL_STATE); } } - void on_subscribe_logs_request(const SubscribeLogsRequest &msg) override { + void on_subscribe_logs_request(const SubscribeLogsRequest &msg) { this->flags_.log_subscription = msg.level; if (msg.dump_config) App.schedule_dump_config(); @@ -249,13 +267,13 @@ class APIConnection final : public APIServerConnectionBase { #endif } #ifdef USE_API_HOMEASSISTANT_SERVICES - void on_subscribe_homeassistant_services_request() override { this->flags_.service_call_subscription = true; } + void on_subscribe_homeassistant_services_request() { this->flags_.service_call_subscription = true; } #endif #ifdef USE_API_HOMEASSISTANT_STATES - void on_subscribe_home_assistant_states_request() override; + void on_subscribe_home_assistant_states_request(); #endif #ifdef USE_API_USER_DEFINED_ACTIONS - void on_execute_service_request(const ExecuteServiceRequest &msg) override; + void on_execute_service_request(const ExecuteServiceRequest &msg); #ifdef USE_API_USER_DEFINED_ACTION_RESPONSES void send_execute_service_response(uint32_t call_id, bool success, StringRef error_message); #ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON @@ -265,13 +283,13 @@ class APIConnection final : public APIServerConnectionBase { #endif // USE_API_USER_DEFINED_ACTION_RESPONSES #endif #ifdef USE_API_NOISE - void on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &msg) override; + void on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &msg); #endif - bool is_authenticated() override { + bool is_authenticated() { return static_cast(this->flags_.connection_state) == ConnectionState::AUTHENTICATED; } - bool is_connection_setup() override { + bool is_connection_setup() { return static_cast(this->flags_.connection_state) == ConnectionState::CONNECTED || this->is_authenticated(); } @@ -284,8 +302,8 @@ class APIConnection final : public APIServerConnectionBase { (this->client_api_version_major_ == major && this->client_api_version_minor_ >= minor); } - void on_fatal_error() override; - void on_no_setup_connection() override; + void on_fatal_error(); + void on_no_setup_connection(); // Function pointer type for type-erased message encoding using MessageEncodeFn = void (*)(const void *, ProtoWriteBuffer &); @@ -324,7 +342,7 @@ class APIConnection final : public APIServerConnectionBase { return true; return this->try_to_clear_buffer_slow_(log_out_of_space); } - bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) override; + bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type); const char *get_name() const { return this->helper_->get_client_name(); } /// Get peer name (IP address) into caller-provided buffer, returns buf for convenience diff --git a/esphome/components/api/api_pb2_service.cpp b/esphome/components/api/api_pb2_service.cpp index f2f7fa5238..d86cf912db 100644 --- a/esphome/components/api/api_pb2_service.cpp +++ b/esphome/components/api/api_pb2_service.cpp @@ -1,6 +1,7 @@ // This file was automatically generated with a tool. // See script/api_protobuf/api_protobuf.py #include "api_pb2_service.h" +#include "api_connection.h" #include "esphome/core/log.h" namespace esphome::api { @@ -20,7 +21,7 @@ void APIServerConnectionBase::log_receive_message_(const LogString *name) { } #endif -void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) { +void APIConnection::read_message_(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) { // Check authentication/connection requirements 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 10fd88d8e1..4925a6497a 100644 --- a/esphome/components/api/api_pb2_service.h +++ b/esphome/components/api/api_pb2_service.h @@ -8,7 +8,7 @@ namespace esphome::api { -class APIServerConnectionBase : public ProtoService { +class APIServerConnectionBase { public: #ifdef HAS_PROTO_MESSAGE_DUMP protected: @@ -19,227 +19,223 @@ class APIServerConnectionBase : public ProtoService { public: #endif - virtual void on_hello_request(const HelloRequest &value){}; + void on_hello_request(const HelloRequest &value){}; - virtual void on_disconnect_request(){}; - virtual void on_disconnect_response(){}; - virtual void on_ping_request(){}; - virtual void on_ping_response(){}; - virtual void on_device_info_request(){}; + void on_disconnect_request(){}; + void on_disconnect_response(){}; + void on_ping_request(){}; + void on_ping_response(){}; + void on_device_info_request(){}; - virtual void on_list_entities_request(){}; + void on_list_entities_request(){}; - virtual void on_subscribe_states_request(){}; + void on_subscribe_states_request(){}; #ifdef USE_COVER - virtual void on_cover_command_request(const CoverCommandRequest &value){}; + void on_cover_command_request(const CoverCommandRequest &value){}; #endif #ifdef USE_FAN - virtual void on_fan_command_request(const FanCommandRequest &value){}; + void on_fan_command_request(const FanCommandRequest &value){}; #endif #ifdef USE_LIGHT - virtual void on_light_command_request(const LightCommandRequest &value){}; + void on_light_command_request(const LightCommandRequest &value){}; #endif #ifdef USE_SWITCH - virtual void on_switch_command_request(const SwitchCommandRequest &value){}; + void on_switch_command_request(const SwitchCommandRequest &value){}; #endif - virtual void on_subscribe_logs_request(const SubscribeLogsRequest &value){}; + void on_subscribe_logs_request(const SubscribeLogsRequest &value){}; #ifdef USE_API_NOISE - virtual void on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &value){}; + void on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &value){}; #endif #ifdef USE_API_HOMEASSISTANT_SERVICES - virtual void on_subscribe_homeassistant_services_request(){}; + void on_subscribe_homeassistant_services_request(){}; #endif #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES - virtual void on_homeassistant_action_response(const HomeassistantActionResponse &value){}; + void on_homeassistant_action_response(const HomeassistantActionResponse &value){}; #endif #ifdef USE_API_HOMEASSISTANT_STATES - virtual void on_subscribe_home_assistant_states_request(){}; + void on_subscribe_home_assistant_states_request(){}; #endif #ifdef USE_API_HOMEASSISTANT_STATES - virtual void on_home_assistant_state_response(const HomeAssistantStateResponse &value){}; + void on_home_assistant_state_response(const HomeAssistantStateResponse &value){}; #endif - virtual void on_get_time_response(const GetTimeResponse &value){}; + void on_get_time_response(const GetTimeResponse &value){}; #ifdef USE_API_USER_DEFINED_ACTIONS - virtual void on_execute_service_request(const ExecuteServiceRequest &value){}; + void on_execute_service_request(const ExecuteServiceRequest &value){}; #endif #ifdef USE_CAMERA - virtual void on_camera_image_request(const CameraImageRequest &value){}; + void on_camera_image_request(const CameraImageRequest &value){}; #endif #ifdef USE_CLIMATE - virtual void on_climate_command_request(const ClimateCommandRequest &value){}; + void on_climate_command_request(const ClimateCommandRequest &value){}; #endif #ifdef USE_WATER_HEATER - virtual void on_water_heater_command_request(const WaterHeaterCommandRequest &value){}; + void on_water_heater_command_request(const WaterHeaterCommandRequest &value){}; #endif #ifdef USE_NUMBER - virtual void on_number_command_request(const NumberCommandRequest &value){}; + void on_number_command_request(const NumberCommandRequest &value){}; #endif #ifdef USE_SELECT - virtual void on_select_command_request(const SelectCommandRequest &value){}; + void on_select_command_request(const SelectCommandRequest &value){}; #endif #ifdef USE_SIREN - virtual void on_siren_command_request(const SirenCommandRequest &value){}; + void on_siren_command_request(const SirenCommandRequest &value){}; #endif #ifdef USE_LOCK - virtual void on_lock_command_request(const LockCommandRequest &value){}; + void on_lock_command_request(const LockCommandRequest &value){}; #endif #ifdef USE_BUTTON - virtual void on_button_command_request(const ButtonCommandRequest &value){}; + void on_button_command_request(const ButtonCommandRequest &value){}; #endif #ifdef USE_MEDIA_PLAYER - virtual void on_media_player_command_request(const MediaPlayerCommandRequest &value){}; + void on_media_player_command_request(const MediaPlayerCommandRequest &value){}; #endif #ifdef USE_BLUETOOTH_PROXY - virtual void on_subscribe_bluetooth_le_advertisements_request( - const SubscribeBluetoothLEAdvertisementsRequest &value){}; + void on_subscribe_bluetooth_le_advertisements_request(const SubscribeBluetoothLEAdvertisementsRequest &value){}; #endif #ifdef USE_BLUETOOTH_PROXY - virtual void on_bluetooth_device_request(const BluetoothDeviceRequest &value){}; + void on_bluetooth_device_request(const BluetoothDeviceRequest &value){}; #endif #ifdef USE_BLUETOOTH_PROXY - virtual void on_bluetooth_gatt_get_services_request(const BluetoothGATTGetServicesRequest &value){}; + void on_bluetooth_gatt_get_services_request(const BluetoothGATTGetServicesRequest &value){}; #endif #ifdef USE_BLUETOOTH_PROXY - virtual void on_bluetooth_gatt_read_request(const BluetoothGATTReadRequest &value){}; + void on_bluetooth_gatt_read_request(const BluetoothGATTReadRequest &value){}; #endif #ifdef USE_BLUETOOTH_PROXY - virtual void on_bluetooth_gatt_write_request(const BluetoothGATTWriteRequest &value){}; + void on_bluetooth_gatt_write_request(const BluetoothGATTWriteRequest &value){}; #endif #ifdef USE_BLUETOOTH_PROXY - virtual void on_bluetooth_gatt_read_descriptor_request(const BluetoothGATTReadDescriptorRequest &value){}; + void on_bluetooth_gatt_read_descriptor_request(const BluetoothGATTReadDescriptorRequest &value){}; #endif #ifdef USE_BLUETOOTH_PROXY - virtual void on_bluetooth_gatt_write_descriptor_request(const BluetoothGATTWriteDescriptorRequest &value){}; + void on_bluetooth_gatt_write_descriptor_request(const BluetoothGATTWriteDescriptorRequest &value){}; #endif #ifdef USE_BLUETOOTH_PROXY - virtual void on_bluetooth_gatt_notify_request(const BluetoothGATTNotifyRequest &value){}; + void on_bluetooth_gatt_notify_request(const BluetoothGATTNotifyRequest &value){}; #endif #ifdef USE_BLUETOOTH_PROXY - virtual void on_subscribe_bluetooth_connections_free_request(){}; + void on_subscribe_bluetooth_connections_free_request(){}; #endif #ifdef USE_BLUETOOTH_PROXY - virtual void on_unsubscribe_bluetooth_le_advertisements_request(){}; + void on_unsubscribe_bluetooth_le_advertisements_request(){}; #endif #ifdef USE_BLUETOOTH_PROXY - virtual void on_bluetooth_scanner_set_mode_request(const BluetoothScannerSetModeRequest &value){}; + void on_bluetooth_scanner_set_mode_request(const BluetoothScannerSetModeRequest &value){}; #endif #ifdef USE_VOICE_ASSISTANT - virtual void on_subscribe_voice_assistant_request(const SubscribeVoiceAssistantRequest &value){}; + void on_subscribe_voice_assistant_request(const SubscribeVoiceAssistantRequest &value){}; #endif #ifdef USE_VOICE_ASSISTANT - virtual void on_voice_assistant_response(const VoiceAssistantResponse &value){}; + void on_voice_assistant_response(const VoiceAssistantResponse &value){}; #endif #ifdef USE_VOICE_ASSISTANT - virtual void on_voice_assistant_event_response(const VoiceAssistantEventResponse &value){}; + void on_voice_assistant_event_response(const VoiceAssistantEventResponse &value){}; #endif #ifdef USE_VOICE_ASSISTANT - virtual void on_voice_assistant_audio(const VoiceAssistantAudio &value){}; + void on_voice_assistant_audio(const VoiceAssistantAudio &value){}; #endif #ifdef USE_VOICE_ASSISTANT - virtual void on_voice_assistant_timer_event_response(const VoiceAssistantTimerEventResponse &value){}; + void on_voice_assistant_timer_event_response(const VoiceAssistantTimerEventResponse &value){}; #endif #ifdef USE_VOICE_ASSISTANT - virtual void on_voice_assistant_announce_request(const VoiceAssistantAnnounceRequest &value){}; + void on_voice_assistant_announce_request(const VoiceAssistantAnnounceRequest &value){}; #endif #ifdef USE_VOICE_ASSISTANT - virtual void on_voice_assistant_configuration_request(const VoiceAssistantConfigurationRequest &value){}; + void on_voice_assistant_configuration_request(const VoiceAssistantConfigurationRequest &value){}; #endif #ifdef USE_VOICE_ASSISTANT - virtual void on_voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &value){}; + void on_voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &value){}; #endif #ifdef USE_ALARM_CONTROL_PANEL - virtual void on_alarm_control_panel_command_request(const AlarmControlPanelCommandRequest &value){}; + void on_alarm_control_panel_command_request(const AlarmControlPanelCommandRequest &value){}; #endif #ifdef USE_TEXT - virtual void on_text_command_request(const TextCommandRequest &value){}; + void on_text_command_request(const TextCommandRequest &value){}; #endif #ifdef USE_DATETIME_DATE - virtual void on_date_command_request(const DateCommandRequest &value){}; + void on_date_command_request(const DateCommandRequest &value){}; #endif #ifdef USE_DATETIME_TIME - virtual void on_time_command_request(const TimeCommandRequest &value){}; + void on_time_command_request(const TimeCommandRequest &value){}; #endif #ifdef USE_VALVE - virtual void on_valve_command_request(const ValveCommandRequest &value){}; + void on_valve_command_request(const ValveCommandRequest &value){}; #endif #ifdef USE_DATETIME_DATETIME - virtual void on_date_time_command_request(const DateTimeCommandRequest &value){}; + void on_date_time_command_request(const DateTimeCommandRequest &value){}; #endif #ifdef USE_UPDATE - virtual void on_update_command_request(const UpdateCommandRequest &value){}; + void on_update_command_request(const UpdateCommandRequest &value){}; #endif #ifdef USE_ZWAVE_PROXY - virtual void on_z_wave_proxy_frame(const ZWaveProxyFrame &value){}; + void on_z_wave_proxy_frame(const ZWaveProxyFrame &value){}; #endif #ifdef USE_ZWAVE_PROXY - virtual void on_z_wave_proxy_request(const ZWaveProxyRequest &value){}; + void on_z_wave_proxy_request(const ZWaveProxyRequest &value){}; #endif #ifdef USE_IR_RF - virtual void on_infrared_rf_transmit_raw_timings_request(const InfraredRFTransmitRawTimingsRequest &value){}; + void on_infrared_rf_transmit_raw_timings_request(const InfraredRFTransmitRawTimingsRequest &value){}; #endif #ifdef USE_SERIAL_PROXY - virtual void on_serial_proxy_configure_request(const SerialProxyConfigureRequest &value){}; + void on_serial_proxy_configure_request(const SerialProxyConfigureRequest &value){}; #endif #ifdef USE_SERIAL_PROXY - virtual void on_serial_proxy_write_request(const SerialProxyWriteRequest &value){}; + void on_serial_proxy_write_request(const SerialProxyWriteRequest &value){}; #endif #ifdef USE_SERIAL_PROXY - virtual void on_serial_proxy_set_modem_pins_request(const SerialProxySetModemPinsRequest &value){}; + void on_serial_proxy_set_modem_pins_request(const SerialProxySetModemPinsRequest &value){}; #endif #ifdef USE_SERIAL_PROXY - virtual void on_serial_proxy_get_modem_pins_request(const SerialProxyGetModemPinsRequest &value){}; + void on_serial_proxy_get_modem_pins_request(const SerialProxyGetModemPinsRequest &value){}; #endif #ifdef USE_SERIAL_PROXY - virtual void on_serial_proxy_request(const SerialProxyRequest &value){}; + void on_serial_proxy_request(const SerialProxyRequest &value){}; #endif #ifdef USE_BLUETOOTH_PROXY - virtual void on_bluetooth_set_connection_params_request(const BluetoothSetConnectionParamsRequest &value){}; + void on_bluetooth_set_connection_params_request(const BluetoothSetConnectionParamsRequest &value){}; #endif - - protected: - 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 cd22915703..a1d825ead9 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -711,26 +711,7 @@ inline void ProtoLengthDelimited::decode_to_message(ProtoDecodableMessage &msg) template const char *proto_enum_to_string(T value); -class ProtoService { - public: - protected: - virtual bool is_authenticated() = 0; - virtual bool is_connection_setup() = 0; - virtual void on_fatal_error() = 0; - virtual void on_no_setup_connection() = 0; - virtual bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) = 0; - virtual void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) = 0; - - // Authentication helper methods - inline bool check_connection_setup_() { - if (!this->is_connection_setup()) { - this->on_no_setup_connection(); - return false; - } - return true; - } - - inline bool check_authenticated_() { return this->check_connection_setup_(); } -}; +// ProtoService removed — its methods were inlined into APIConnection. +// APIConnection is the concrete server-side implementation; the extra virtual layer was unnecessary. } // namespace esphome::api diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index aca31e49c8..221ccc8d69 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -2608,7 +2608,7 @@ def build_service_message_type( is_empty = not has_fields if is_empty: EMPTY_MESSAGES.add(mt.name) - hout += f"virtual void {func}({'' if is_empty else f'const {mt.name} &value'}){{}};\n" + hout += f"void {func}({'' if is_empty else f'const {mt.name} &value'}){{}};\n" case = "" if not is_empty: case += f"{mt.name} msg;\n" @@ -2960,6 +2960,7 @@ namespace esphome::api { cpp = FILE_HEADER cpp += """\ #include "api_pb2_service.h" +#include "api_connection.h" #include "esphome/core/log.h" namespace esphome::api { @@ -2970,7 +2971,7 @@ static const char *const TAG = "api.service"; class_name = "APIServerConnectionBase" - hpp += f"class {class_name} : public ProtoService {{\n" + hpp += f"class {class_name} {{\n" hpp += " public:\n" # Add logging helper method declarations @@ -3063,11 +3064,11 @@ static const char *const TAG = "api.service"; result += "#endif\n" return result - # Generate read_message with auth check before dispatch - hpp += " protected:\n" - hpp += " void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) override;\n" + # Generate read_message_ as APIConnection method (not base class) so the compiler + # can devirtualize and inline the on_* handler calls within the same class. + # APIConnection declares this method in api_connection.h. - out = f"void {class_name}::read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) {{\n" + out = "void APIConnection::read_message_(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) {\n" # Auth check block before dispatch switch out += " // Check authentication/connection requirements\n" From 5560c9eef78e2a7be9fc5d728e767c34a23630c1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Mar 2026 21:10:51 -1000 Subject: [PATCH 102/166] [test] Fix flakey ld2412 integration test race condition (#15100) --- tests/integration/test_uart_mock_ld2412.py | 33 +++++++++------------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/tests/integration/test_uart_mock_ld2412.py b/tests/integration/test_uart_mock_ld2412.py index 9b928ef14f..12aa3f8397 100644 --- a/tests/integration/test_uart_mock_ld2412.py +++ b/tests/integration/test_uart_mock_ld2412.py @@ -79,9 +79,15 @@ async def test_uart_mock_ld2412( ], ) - # Signal when we see recovery frame values + # Signal when we see all recovery frame values recovery_received = collector.add_waiter( - lambda: pytest.approx(50.0) in collector.sensor_states["moving_distance"] + lambda: ( + pytest.approx(50.0) in collector.sensor_states["moving_distance"] + and pytest.approx(75.0) in collector.sensor_states["still_distance"] + and pytest.approx(100.0) in collector.sensor_states["moving_energy"] + and pytest.approx(80.0) in collector.sensor_states["still_energy"] + and pytest.approx(50.0) in collector.sensor_states["detection_distance"] + ) ) async with ( @@ -150,23 +156,12 @@ async def test_uart_mock_ld2412( ) # Recovery frame: moving=50, still=75, energy=100/80, detect=50 - recovery_idx = next( - i - for i, v in enumerate(collector.sensor_states["moving_distance"]) - if v == pytest.approx(50.0) - ) - assert collector.sensor_states["still_distance"][recovery_idx] == pytest.approx( - 75.0 - ) - assert collector.sensor_states["moving_energy"][recovery_idx] == pytest.approx( - 100.0 - ) - assert collector.sensor_states["still_energy"][recovery_idx] == pytest.approx( - 80.0 - ) - assert collector.sensor_states["detection_distance"][ - recovery_idx - ] == pytest.approx(50.0) + # Check values exist (waiter already ensured all are present) + assert pytest.approx(50.0) in collector.sensor_states["moving_distance"] + assert pytest.approx(75.0) in collector.sensor_states["still_distance"] + assert pytest.approx(100.0) in collector.sensor_states["moving_energy"] + assert pytest.approx(80.0) in collector.sensor_states["still_energy"] + assert pytest.approx(50.0) in collector.sensor_states["detection_distance"] # Verify binary sensors detected targets (from Phase 1 frame) assert collector.binary_states["has_target"][0] is True From 43879964bd3ceff183ff4e113b6b5f4d05b34d77 Mon Sep 17 00:00:00 2001 From: Simone Rossetto Date: Mon, 23 Mar 2026 10:03:19 +0100 Subject: [PATCH 103/166] [wireguard] bump esp_wireguard to 0.4.4 for mbedtls 4.0+ compatibility (#15104) Co-authored-by: J. Nick Koston --- .clang-tidy.hash | 2 +- esphome/components/wireguard/__init__.py | 2 +- platformio.ini | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.clang-tidy.hash b/.clang-tidy.hash index c32978d411..5c7eab517b 100644 --- a/.clang-tidy.hash +++ b/.clang-tidy.hash @@ -1 +1 @@ -9f5d763f95ff720024f3fdddba2fad3801e2bfe00b7cc2124e6d68c17d3504c6 +f31f13994768b5b07e29624406c9b053bf4bb26e1623ac2bc1e9d4a9477502d6 diff --git a/esphome/components/wireguard/__init__.py b/esphome/components/wireguard/__init__.py index e2ea61a542..1b54391376 100644 --- a/esphome/components/wireguard/__init__.py +++ b/esphome/components/wireguard/__init__.py @@ -137,7 +137,7 @@ async def to_code(config): # the '+1' modifier is relative to the device's own address that will # be automatically added to the provided list. cg.add_build_flag(f"-DCONFIG_WIREGUARD_MAX_SRC_IPS={len(allowed_ips) + 1}") - cg.add_library("droscy/esp_wireguard", "0.4.2") + cg.add_library("droscy/esp_wireguard", "0.4.4") await cg.register_component(var, config) diff --git a/platformio.ini b/platformio.ini index d3a482b652..e0f7c7d443 100644 --- a/platformio.ini +++ b/platformio.ini @@ -118,7 +118,7 @@ lib_deps = ESP8266HTTPClient ; http_request (Arduino built-in) ESP8266mDNS ; mdns (Arduino built-in) DNSServer ; captive_portal (Arduino built-in) - droscy/esp_wireguard@0.4.2 ; wireguard + droscy/esp_wireguard@0.4.4 ; wireguard lvgl/lvgl@9.5.0 ; lvgl build_flags = @@ -154,7 +154,7 @@ lib_deps = DNSServer ; captive_portal (Arduino built-in) makuna/NeoPixelBus@2.8.0 ; neopixelbus esphome/ESP32-audioI2S@2.3.0 ; i2s_audio - droscy/esp_wireguard@0.4.2 ; wireguard + droscy/esp_wireguard@0.4.4 ; wireguard kahrendt/ESPMicroSpeechFeatures@1.1.0 ; micro_wake_word build_flags = @@ -176,7 +176,7 @@ platform_packages = framework = espidf lib_deps = ${common:idf.lib_deps} - droscy/esp_wireguard@0.4.2 ; wireguard + droscy/esp_wireguard@0.4.4 ; wireguard kahrendt/ESPMicroSpeechFeatures@1.1.0 ; micro_wake_word tonia/HeatpumpIR@1.0.40 ; heatpumpir build_flags = @@ -221,7 +221,7 @@ lib_compat_mode = soft lib_deps = bblanchon/ArduinoJson@7.4.2 ; json ESP32Async/ESPAsyncWebServer@3.9.6 ; web_server_base - droscy/esp_wireguard@0.4.2 ; wireguard + droscy/esp_wireguard@0.4.4 ; wireguard lvgl/lvgl@9.5.0 ; lvgl build_flags = ${common:arduino.build_flags} From 5a984b54cfce79fbd6ad93365cf8ccc148942d6f Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Mon, 23 Mar 2026 07:31:05 -0500 Subject: [PATCH 104/166] [audio] Bump microOpus to avoid creating an extra opus-staged directory (#14974) --- esphome/components/audio/__init__.py | 2 +- esphome/idf_component.yml | 2 +- script/analyze_component_buses.py | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/esphome/components/audio/__init__.py b/esphome/components/audio/__init__.py index b28c2ed3d8..9cc80b9b33 100644 --- a/esphome/components/audio/__init__.py +++ b/esphome/components/audio/__init__.py @@ -214,4 +214,4 @@ async def to_code(config): cg.add_define("USE_AUDIO_MP3_SUPPORT") if data.opus_support: cg.add_define("USE_AUDIO_OPUS_SUPPORT") - add_idf_component(name="esphome/micro-opus", ref="0.3.5") + add_idf_component(name="esphome/micro-opus", ref="0.3.6") diff --git a/esphome/idf_component.yml b/esphome/idf_component.yml index 5cfa8532ff..4148147a3b 100644 --- a/esphome/idf_component.yml +++ b/esphome/idf_component.yml @@ -4,7 +4,7 @@ dependencies: esphome/esp-audio-libs: version: 2.0.3 esphome/micro-opus: - version: 0.3.5 + version: 0.3.6 espressif/esp-dsp: version: "1.7.1" espressif/esp-tflite-micro: diff --git a/script/analyze_component_buses.py b/script/analyze_component_buses.py index 427602dff2..17af7af577 100755 --- a/script/analyze_component_buses.py +++ b/script/analyze_component_buses.py @@ -83,6 +83,7 @@ ISOLATED_COMPONENTS = { "openthread": "Conflicts with wifi: used by most components", "openthread_info": "Conflicts with wifi: used by most components", "matrix_keypad": "Needs isolation due to keypad", + "microphone": "Defines PDM microphone requiring I2S port 0 - conflicts with micro_wake_word PDM mic when merged", "modbus_controller": "Defines multiple modbus buses for testing client/server functionality - conflicts with package modbus bus", "neopixelbus": "RMT type conflict with ESP32 Arduino/ESP-IDF headers (enum vs struct rmt_channel_t)", "packages": "cannot merge packages", From e8c5dfca3ecc959f17722bdafb13471ad4e3f1a0 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Tue, 24 Mar 2026 02:09:30 +1000 Subject: [PATCH 105/166] [lvgl] Various fixes (#15098) --- esphome/components/lvgl/__init__.py | 8 +++++++- esphome/components/lvgl/lvgl_esphome.h | 2 +- esphome/components/lvgl/widgets/textarea.py | 4 ++++ 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/esphome/components/lvgl/__init__.py b/esphome/components/lvgl/__init__.py index c37a32ecca..a6afa12afa 100644 --- a/esphome/components/lvgl/__init__.py +++ b/esphome/components/lvgl/__init__.py @@ -226,6 +226,9 @@ async def to_code(configs): config_0 = configs[0] # Global configuration if CORE.is_esp32: + # Skip compiling lvgl examples + add_idf_sdkconfig_option("CONFIG_LV_BUILD_EXAMPLES", False) + add_idf_sdkconfig_option("CONFIG_LV_BUILD_DEMOS", False) if get_esp32_variant() == VARIANT_ESP32P4: add_idf_sdkconfig_option("CONFIG_LV_DRAW_BUF_ALIGN", 64) # disable use of PPA for fills until upstream bugs fixed @@ -406,7 +409,10 @@ async def to_code(configs): lv_conf_h_file = CORE.relative_src_path(LV_CONF_FILENAME) write_file_if_changed(lv_conf_h_file, generate_lv_conf_h()) cg.add_build_flag("-DLV_CONF_H=1") - cg.add_build_flag(f'-DLV_CONF_PATH=\\"{LV_CONF_FILENAME}\\"') + # handle windows paths in a way that doesn't break the generated C++ + lv_conf_h_path = Path(lv_conf_h_file).as_posix() + cg.add_build_flag(f'-DLV_CONF_PATH=\\"{lv_conf_h_path}\\"') + cg.add_build_flag("-DLV_KCONFIG_IGNORE") for prop in df.get_remapped_uses(): df.LOGGER.warning( diff --git a/esphome/components/lvgl/lvgl_esphome.h b/esphome/components/lvgl/lvgl_esphome.h index 8e34f16c98..66f823d549 100644 --- a/esphome/components/lvgl/lvgl_esphome.h +++ b/esphome/components/lvgl/lvgl_esphome.h @@ -71,7 +71,7 @@ inline void lv_style_set_text_font(lv_style_t *style, const font::Font *font) { lv_style_set_text_font(style, font->get_lv_font()); } #endif -#ifdef USE_IMAGE +#if defined(USE_LVGL_IMAGE) && defined(USE_IMAGE) // Shortcut / overload, so that the source of an image can easily be updated // from within a lambda. inline void lv_image_set_src(lv_obj_t *obj, esphome::image::Image *image) { diff --git a/esphome/components/lvgl/widgets/textarea.py b/esphome/components/lvgl/widgets/textarea.py index e5ab884685..baa40ee2c6 100644 --- a/esphome/components/lvgl/widgets/textarea.py +++ b/esphome/components/lvgl/widgets/textarea.py @@ -16,6 +16,7 @@ from ..lv_validation import lv_bool, lv_int, lv_text from ..schemas import TEXT_SCHEMA from ..types import LvText from . import Widget, WidgetType +from .label import CONF_LABEL CONF_TEXTAREA = "textarea" @@ -46,6 +47,9 @@ class TextareaType(WidgetType): TEXTAREA_SCHEMA, ) + def get_uses(self): + return (CONF_LABEL,) + async def to_code(self, w: Widget, config: dict): for prop in (CONF_TEXT, CONF_PLACEHOLDER_TEXT, CONF_ACCEPTED_CHARS): if (value := config.get(prop)) is not None: From 3b5b51b4f0f918fef64e4996dcf0a551389afc4d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Mar 2026 08:22:25 -1000 Subject: [PATCH 106/166] [time] Point to valid IANA timezone list on validation failure (#15110) --- esphome/components/time/__init__.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/esphome/components/time/__init__.py b/esphome/components/time/__init__.py index 9821046a73..c31ccbc7ea 100644 --- a/esphome/components/time/__init__.py +++ b/esphome/components/time/__init__.py @@ -284,13 +284,23 @@ def validate_tz(value: str) -> str: tzfile = _load_tzdata(value) if tzfile is not None: value = _extract_tz_string(tzfile) + is_iana = True + else: + is_iana = False # Validate that the POSIX TZ string is parseable (skip empty strings) if value: try: parse_posix_tz_python(value) except ValueError as e: - raise cv.Invalid(f"Invalid POSIX timezone string '{value}': {e}") from e + if is_iana: + raise cv.Invalid(f"Invalid POSIX timezone string '{value}': {e}") from e + raise cv.Invalid( + f"Invalid POSIX timezone string '{value}': {e}. " + f"If you meant to use an IANA timezone, check the list of valid " + f"timezones at " + f"https://en.wikipedia.org/wiki/List_of_tz_database_time_zones" + ) from e return value From 03d6b36fe03176ea2bca019f701f8a706dc3f3c0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Mar 2026 08:22:38 -1000 Subject: [PATCH 107/166] [gpio] Compile out interlock fields when unused (#15111) --- esphome/components/gpio/switch/__init__.py | 1 + esphome/components/gpio/switch/gpio_switch.cpp | 9 +++++++++ esphome/components/gpio/switch/gpio_switch.h | 4 ++++ esphome/core/defines.h | 1 + 4 files changed, 15 insertions(+) diff --git a/esphome/components/gpio/switch/__init__.py b/esphome/components/gpio/switch/__init__.py index 604de6d809..9462cd0161 100644 --- a/esphome/components/gpio/switch/__init__.py +++ b/esphome/components/gpio/switch/__init__.py @@ -32,6 +32,7 @@ async def to_code(config): cg.add(var.set_pin(pin)) if CONF_INTERLOCK in config: + cg.add_define("USE_GPIO_SWITCH_INTERLOCK") interlock = [] for it in config[CONF_INTERLOCK]: lock = await cg.get_variable(it) diff --git a/esphome/components/gpio/switch/gpio_switch.cpp b/esphome/components/gpio/switch/gpio_switch.cpp index d461fab051..9c6464815a 100644 --- a/esphome/components/gpio/switch/gpio_switch.cpp +++ b/esphome/components/gpio/switch/gpio_switch.cpp @@ -5,7 +5,9 @@ namespace esphome { namespace gpio { static const char *const TAG = "switch.gpio"; +#ifdef USE_GPIO_SWITCH_INTERLOCK static constexpr uint32_t INTERLOCK_TIMEOUT_ID = 0; +#endif float GPIOSwitch::get_setup_priority() const { return setup_priority::HARDWARE; } void GPIOSwitch::setup() { @@ -28,6 +30,7 @@ void GPIOSwitch::setup() { void GPIOSwitch::dump_config() { LOG_SWITCH("", "GPIO Switch", this); LOG_PIN(" Pin: ", this->pin_); +#ifdef USE_GPIO_SWITCH_INTERLOCK if (!this->interlock_.empty()) { ESP_LOGCONFIG(TAG, " Interlocks:"); for (auto *lock : this->interlock_) { @@ -36,8 +39,10 @@ void GPIOSwitch::dump_config() { ESP_LOGCONFIG(TAG, " %s", lock->get_name().c_str()); } } +#endif } void GPIOSwitch::write_state(bool state) { +#ifdef USE_GPIO_SWITCH_INTERLOCK if (state != this->inverted_) { // Turning ON, check interlocking @@ -64,11 +69,15 @@ void GPIOSwitch::write_state(bool state) { // re-activations this->cancel_timeout(INTERLOCK_TIMEOUT_ID); } +#endif this->pin_->digital_write(state); this->publish_state(state); } + +#ifdef USE_GPIO_SWITCH_INTERLOCK void GPIOSwitch::set_interlock(const std::initializer_list &interlock) { this->interlock_ = interlock; } +#endif } // namespace gpio } // namespace esphome diff --git a/esphome/components/gpio/switch/gpio_switch.h b/esphome/components/gpio/switch/gpio_switch.h index a73fb9e18c..f7415d1dba 100644 --- a/esphome/components/gpio/switch/gpio_switch.h +++ b/esphome/components/gpio/switch/gpio_switch.h @@ -18,15 +18,19 @@ class GPIOSwitch final : public switch_::Switch, public Component { void setup() override; void dump_config() override; +#ifdef USE_GPIO_SWITCH_INTERLOCK void set_interlock(const std::initializer_list &interlock); void set_interlock_wait_time(uint32_t interlock_wait_time) { interlock_wait_time_ = interlock_wait_time; } +#endif protected: void write_state(bool state) override; GPIOPin *pin_; +#ifdef USE_GPIO_SWITCH_INTERLOCK FixedVector interlock_; uint32_t interlock_wait_time_{0}; +#endif }; } // namespace gpio diff --git a/esphome/core/defines.h b/esphome/core/defines.h index d94b7e9f5d..f437e30a95 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -53,6 +53,7 @@ #define USE_ESP32_IMPROV_STATE_CALLBACK #define USE_EVENT #define USE_FAN +#define USE_GPIO_SWITCH_INTERLOCK #define USE_GRAPH #define USE_GRAPHICAL_DISPLAY_MENU #define USE_HOMEASSISTANT_TIME From 36d2e58b11836420fff50447c9c2a9ee70cdf432 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Mar 2026 08:23:08 -1000 Subject: [PATCH 108/166] [api] Make ProtoDecodableMessage::decode() non-virtual (#15076) --- esphome/components/api/api_pb2.h | 4 ++-- esphome/components/api/proto.h | 22 +++++++--------------- script/api_protobuf/api_protobuf.py | 2 +- 3 files changed, 10 insertions(+), 18 deletions(-) diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index a4ee0adb8b..86289a28d6 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -1253,7 +1253,7 @@ class ExecuteServiceArgument final : public ProtoDecodableMessage { FixedVector int_array{}; FixedVector float_array{}; FixedVector string_array{}; - void decode(const uint8_t *buffer, size_t length) override; + void decode(const uint8_t *buffer, size_t length); #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1278,7 +1278,7 @@ class ExecuteServiceRequest final : public ProtoDecodableMessage { #ifdef USE_API_USER_DEFINED_ACTION_RESPONSES bool return_response{false}; #endif - void decode(const uint8_t *buffer, size_t length) override; + void decode(const uint8_t *buffer, size_t length); #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index a1d825ead9..d6f9c947d7 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -152,8 +152,7 @@ class ProtoVarInt { #endif }; -// Forward declarations for decode_to_message and related encoding helpers -class ProtoDecodableMessage; +// Forward declarations for encoding helpers class ProtoMessage; class ProtoSize; @@ -166,16 +165,9 @@ class ProtoLengthDelimited { const uint8_t *data() const { return this->value_; } size_t size() const { return this->length_; } - /** - * Decode the length-delimited data into an existing ProtoDecodableMessage instance. - * - * This method allows decoding without templates, enabling use in contexts - * where the message type is not known at compile time. The ProtoDecodableMessage's - * decode() method will be called with the raw data and length. - * - * @param msg The ProtoDecodableMessage instance to decode into - */ - void decode_to_message(ProtoDecodableMessage &msg) const; + /// Decode the length-delimited data into a message instance. + /// Template preserves concrete type so decode() resolves statically. + template void decode_to_message(T &msg) const; protected: const uint8_t *const value_; @@ -468,7 +460,7 @@ class ProtoMessage { // Base class for messages that support decoding class ProtoDecodableMessage : public ProtoMessage { public: - virtual void decode(const uint8_t *buffer, size_t length); + void decode(const uint8_t *buffer, size_t length); /** * Count occurrences of a repeated field in a protobuf buffer. @@ -704,8 +696,8 @@ template inline void ProtoWriteBuffer::encode_optional_sub_message(u this->encode_optional_sub_message(field_id, value.calculate_size(), &value, &proto_encode_msg); } -// Implementation of decode_to_message - must be after ProtoDecodableMessage is defined -inline void ProtoLengthDelimited::decode_to_message(ProtoDecodableMessage &msg) const { +// Template decode_to_message - preserves concrete type so decode() resolves statically +template void ProtoLengthDelimited::decode_to_message(T &msg) const { msg.decode(this->value_, this->length_); } diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 221ccc8d69..0bb569fdb5 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -2275,7 +2275,7 @@ def build_message_type( o += "}\n" cpp += o # Generate the decode() declaration in header (public method) - prot = "void decode(const uint8_t *buffer, size_t length) override;" + prot = "void decode(const uint8_t *buffer, size_t length);" public_content.append(prot) # Only generate encode method if this message needs encoding and has fields From 9385f1612820541019f3685810d07f352b9b5ee1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Mar 2026 08:23:22 -1000 Subject: [PATCH 109/166] [text_sensor] Guard raw_callback_ behind USE_TEXT_SENSOR_FILTER, save 4 bytes per instance (#15097) --- esphome/components/text_sensor/text_sensor.cpp | 2 ++ esphome/components/text_sensor/text_sensor.h | 10 +++++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/esphome/components/text_sensor/text_sensor.cpp b/esphome/components/text_sensor/text_sensor.cpp index aa49a85d26..0dc29f9a94 100644 --- a/esphome/components/text_sensor/text_sensor.cpp +++ b/esphome/components/text_sensor/text_sensor.cpp @@ -31,7 +31,9 @@ void TextSensor::publish_state(const char *state, size_t len) { if (len != this->state.size() || memcmp(state, this->state.data(), len) != 0) { this->state.assign(state, len); } +#ifdef USE_TEXT_SENSOR_FILTER this->raw_callback_.call(this->state); +#endif ESP_LOGV(TAG, "'%s': Received new state %s", this->name_.c_str(), this->state.c_str()); this->notify_frontend_(); #ifdef USE_TEXT_SENSOR_FILTER diff --git a/esphome/components/text_sensor/text_sensor.h b/esphome/components/text_sensor/text_sensor.h index 8941790e7c..3f69e91c8d 100644 --- a/esphome/components/text_sensor/text_sensor.h +++ b/esphome/components/text_sensor/text_sensor.h @@ -64,8 +64,14 @@ class TextSensor : public EntityBase { template void add_on_state_callback(F &&callback) { this->callback_.add(std::forward(callback)); } /// Add a callback that will be called every time the sensor sends a raw value. + /// When USE_TEXT_SENSOR_FILTER is not enabled, delegates to the regular callback + /// since raw state equals filtered state without filter support compiled in. template void add_on_raw_state_callback(F &&callback) { +#ifdef USE_TEXT_SENSOR_FILTER this->raw_callback_.add(std::forward(callback)); +#else + this->callback_.add(std::forward(callback)); +#endif } // ========== INTERNAL METHODS ========== @@ -77,8 +83,10 @@ class TextSensor : public EntityBase { protected: /// Notify frontend that state has changed (assumes this->state is already set) void notify_frontend_(); +#ifdef USE_TEXT_SENSOR_FILTER LazyCallbackManager raw_callback_; ///< Storage for raw state callbacks. - LazyCallbackManager callback_; ///< Storage for filtered state callbacks. +#endif + LazyCallbackManager callback_; ///< Storage for filtered state callbacks. #ifdef USE_TEXT_SENSOR_FILTER Filter *filter_list_{nullptr}; ///< Store all active filters. From 4b0c711f7743495db05f97168d09bf991f60e204 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Mar 2026 08:23:35 -1000 Subject: [PATCH 110/166] [ci] Ban std::bind in new C++ code (#14969) --- script/ci-custom.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/script/ci-custom.py b/script/ci-custom.py index 06fcdadb8c..1da923b095 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -890,6 +890,22 @@ def lint_no_powf_in_core(fname, match): ) +@lint_re_check( + r"[^\w]std\s*::\s*bind\s*\(" + CPP_RE_EOL, + include=cpp_include, +) +def lint_no_std_bind(fname, match): + return ( + f"{highlight('std::bind()')} is not allowed in new ESPHome code. " + f"Lambdas are clearer, produce smaller binaries, and are more likely to fit within " + f"the {highlight('std::function')} small-buffer optimization (avoiding heap allocation).\n" + f"Please use a lambda instead.\n" + f" Before: {highlight('std::bind(&Class::method, this, std::placeholders::_1)')}\n" + f" After: {highlight('[this](auto arg) { this->method(arg); }')}\n" + f"(If strictly necessary, add `// NOLINT` to the end of the line)" + ) + + LOG_MULTILINE_RE = re.compile(r"ESP_LOG\w+\s*\(.*?;", re.DOTALL) LOG_BAD_CONTINUATION_RE = re.compile(r'\\n(?:[^ \\"\r\n\t]|"\s*\n\s*"[^ \\])') LOG_PERCENT_S_CONTINUATION_RE = re.compile(r'\\n(?:%s|"\s*\n\s*"%s)') From 9da0c5bc857b13a1a6387670a19629bcd5ab20fc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Mar 2026 08:47:15 -1000 Subject: [PATCH 111/166] [wifi] Fix roaming attempt counter reset on disconnect during scan (#15099) --- esphome/components/wifi/wifi_component.cpp | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 0fd1385258..e1d4b07471 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -269,11 +269,11 @@ bool CompactString::operator==(const StringRef &other) const { /// │ │ │ /// │ ┌──────────────┼──────────────┐ │ /// │ ↓ ↓ ↓ │ -/// │ scan error no better AP +10 dB better AP │ +/// │ disconnect no better AP +10 dB better AP │ /// │ │ │ │ │ /// │ ↓ ↓ ↓ │ /// │ ┌──────────────────────────────┐ ┌──────────────────────────┐ │ -/// │ │ → IDLE │ │ CONNECTING │ │ +/// │ │ → RECONNECTING │ │ CONNECTING │ │ /// │ │ (counter preserved) │ │ (process_roaming_scan_) │ │ /// │ └──────────────────────────────┘ └────────────┬─────────────┘ │ /// │ │ │ @@ -296,7 +296,7 @@ bool CompactString::operator==(const StringRef &other) const { /// │ Key behaviors: │ /// │ - After 3 checks: attempts >= 3, stop checking │ /// │ - Non-roaming disconnect: clear_roaming_state_() resets counter │ -/// │ - Scan error (SCANNING→IDLE): counter preserved │ +/// │ - Disconnect during scan (SCANNING→RECONNECTING): counter preserved │ /// │ - Roaming success (CONNECTING→IDLE): counter reset (can roam again) │ /// │ - Roaming fail (RECONNECTING→IDLE): counter preserved (ping-pong) │ /// └──────────────────────────────────────────────────────────────────────┘ @@ -2068,9 +2068,10 @@ void WiFiComponent::retry_connect() { ESP_LOGD(TAG, "Roam failed, reconnecting (attempt %u/%u)", this->roaming_attempts_, ROAMING_MAX_ATTEMPTS); this->roaming_state_ = RoamingState::RECONNECTING; } else if (this->roaming_state_ == RoamingState::SCANNING) { - // Roam scan failed (e.g., scan error on ESP8266) - go back to idle, keep counter - ESP_LOGD(TAG, "Roam scan failed (attempt %u/%u)", this->roaming_attempts_, ROAMING_MAX_ATTEMPTS); - this->roaming_state_ = RoamingState::IDLE; + // Disconnected during roam scan - transition to RECONNECTING so the attempts + // counter is preserved when reconnection succeeds (IDLE would reset it) + ESP_LOGD(TAG, "Disconnected during roam scan (attempt %u/%u)", this->roaming_attempts_, ROAMING_MAX_ATTEMPTS); + this->roaming_state_ = RoamingState::RECONNECTING; } else if (this->roaming_state_ == RoamingState::IDLE) { // Not a roaming-triggered reconnect, reset state this->clear_roaming_state_(); From 4c1363b104f198aa837a11bd1ba1ecad28371ec4 Mon Sep 17 00:00:00 2001 From: Daniel Kent <129895318+danielkent-net@users.noreply.github.com> Date: Mon, 23 Mar 2026 15:07:40 -0400 Subject: [PATCH 112/166] [spi] Add LOG_SPI_DEVICE macro (#15118) --- esphome/components/spi/spi.h | 2 ++ script/ci-custom.py | 1 + 2 files changed, 3 insertions(+) diff --git a/esphome/components/spi/spi.h b/esphome/components/spi/spi.h index e237cf44f4..84c8bca267 100644 --- a/esphome/components/spi/spi.h +++ b/esphome/components/spi/spi.h @@ -34,6 +34,8 @@ using SPIInterface = void *; // Stub for platforms without SPI (e.g., Zephyr) */ namespace esphome::spi { +#define LOG_SPI_DEVICE(this) ESP_LOGCONFIG(TAG, " CS Pin: %d", esphome::spi::Utility::get_pin_no(this->cs_)); + /// The bit-order for SPI devices. This defines how the data read from and written to the device is interpreted. enum SPIBitOrder { /// The least significant bit is transmitted/received first. diff --git a/script/ci-custom.py b/script/ci-custom.py index 1da923b095..25a0cf2127 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -963,6 +963,7 @@ def lint_log_multiline_continuation(fname, content): "esphome/components/nextion/nextion_base.h", "esphome/components/select/select.h", "esphome/components/sensor/sensor.h", + "esphome/components/spi/spi.h", "esphome/components/stepper/stepper.h", "esphome/components/switch/switch.h", "esphome/components/text/text.h", From 1e16b30380a4703142318a5a1a93f6ad3626f6f2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Mar 2026 09:18:58 -1000 Subject: [PATCH 113/166] [ethernet] Add ENC28J60 SPI Ethernet support (#14945) --- esphome/components/ethernet/__init__.py | 44 ++++++++++++++----- .../components/ethernet/ethernet_component.h | 13 ++++++ .../ethernet/ethernet_component_esp32.cpp | 41 +++++++++++------ .../ethernet/ethernet_component_rp2040.cpp | 27 +++++++++--- .../ethernet/common-enc28j60-rp2040.yaml | 18 ++++++++ .../components/ethernet/common-enc28j60.yaml | 19 ++++++++ .../ethernet/test-enc28j60.esp32-idf.yaml | 1 + .../ethernet/test-enc28j60.rp2040-ard.yaml | 1 + 8 files changed, 134 insertions(+), 30 deletions(-) create mode 100644 tests/components/ethernet/common-enc28j60-rp2040.yaml create mode 100644 tests/components/ethernet/common-enc28j60.yaml create mode 100644 tests/components/ethernet/test-enc28j60.esp32-idf.yaml create mode 100644 tests/components/ethernet/test-enc28j60.rp2040-ard.yaml diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py index f519d79aa1..e17abfcc93 100644 --- a/esphome/components/ethernet/__init__.py +++ b/esphome/components/ethernet/__init__.py @@ -119,6 +119,7 @@ ETHERNET_TYPES = { "OPENETH": EthernetType.ETHERNET_TYPE_OPENETH, "DM9051": EthernetType.ETHERNET_TYPE_DM9051, "LAN8670": EthernetType.ETHERNET_TYPE_LAN8670, + "ENC28J60": EthernetType.ETHERNET_TYPE_ENC28J60, } # PHY types that need compile-time defines for conditional compilation @@ -134,6 +135,7 @@ _PHY_TYPE_TO_DEFINE = { "W5500": "USE_ETHERNET_W5500", "DM9051": "USE_ETHERNET_DM9051", "LAN8670": "USE_ETHERNET_LAN8670", + "ENC28J60": "USE_ETHERNET_ENC28J60", } @@ -155,11 +157,16 @@ _IDF6_ETHERNET_COMPONENTS: dict[str, IDFRegistryComponent] = { "KSZ8081RNA": IDFRegistryComponent("espressif/ksz80xx", "1.0.0"), "W5500": IDFRegistryComponent("espressif/w5500", "1.0.1"), "DM9051": IDFRegistryComponent("espressif/dm9051", "1.0.0"), + "ENC28J60": IDFRegistryComponent("espressif/enc28j60", "1.0.1"), + "LAN8670": IDFRegistryComponent("espressif/lan867x", "2.0.0"), } -SPI_ETHERNET_TYPES = ["W5500", "DM9051"] +# These types are always external IDF components (never built-in to ESP-IDF) +_ALWAYS_EXTERNAL_IDF_COMPONENTS = {"LAN8670", "ENC28J60"} + +SPI_ETHERNET_TYPES = ["W5500", "DM9051", "ENC28J60"] # RP2040-supported SPI ethernet types -RP2040_SPI_ETHERNET_TYPES = ["W5500"] +RP2040_SPI_ETHERNET_TYPES = ["W5500", "ENC28J60"] SPI_ETHERNET_DEFAULT_POLLING_INTERVAL = TimePeriodMilliseconds(milliseconds=10) emac_rmii_clock_mode_t = cg.global_ns.enum("emac_rmii_clock_mode_t") @@ -220,7 +227,18 @@ def _validate(config): if CORE.is_esp32: if config[CONF_TYPE] in SPI_ETHERNET_TYPES: - if _is_framework_spi_polling_mode_supported(): + # ENC28J60 driver does not support polling mode - interrupt is required + if config[CONF_TYPE] == "ENC28J60": + if CONF_POLLING_INTERVAL in config: + raise cv.Invalid( + f"'{CONF_POLLING_INTERVAL}' is not supported for ENC28J60. " + f"'{CONF_INTERRUPT_PIN}' is required." + ) + if CONF_INTERRUPT_PIN not in config: + raise cv.Invalid( + f"'{CONF_INTERRUPT_PIN}' is a required option for ENC28J60." + ) + elif _is_framework_spi_polling_mode_supported(): if CONF_POLLING_INTERVAL in config and CONF_INTERRUPT_PIN in config: raise cv.Invalid( f"Cannot specify more than one of {CONF_INTERRUPT_PIN}, {CONF_POLLING_INTERVAL}" @@ -367,6 +385,7 @@ CONFIG_SCHEMA = cv.All( "W5500": SPI_SCHEMA, "OPENETH": cv.All(BASE_SCHEMA, cv.only_on([Platform.ESP32])), "DM9051": SPI_SCHEMA, + "ENC28J60": SPI_SCHEMA, "LAN8670": RMII_SCHEMA, }, upper=True, @@ -502,7 +521,8 @@ async def _to_code_esp32(var: cg.Pvariable, config: ConfigType) -> None: cg.add_define("USE_ETHERNET_SPI") add_idf_sdkconfig_option("CONFIG_ETH_USE_SPI_ETHERNET", True) # CONFIG_ETH_SPI_ETHERNET_{TYPE} Kconfig options were removed in IDF 6.0 - if idf_version() < cv.Version(6, 0, 0): + # ENC28J60 was never built-in to IDF, so it has no Kconfig option + if idf_version() < cv.Version(6, 0, 0) and config[CONF_TYPE] != "ENC28J60": add_idf_sdkconfig_option( f"CONFIG_ETH_SPI_ETHERNET_{config[CONF_TYPE]}", True ) @@ -533,12 +553,11 @@ async def _to_code_esp32(var: cg.Pvariable, config: ConfigType) -> None: # Re-enable ESP-IDF's Ethernet driver (excluded by default to save compile time) include_builtin_idf_component("esp_eth") - if config[CONF_TYPE] == "LAN8670": - # Add LAN867x 10BASE-T1S PHY support component - add_idf_component(name="espressif/lan867x", ref="2.0.0") - - # IDF 6.0 moved per-chip PHY/MAC drivers to the Espressif Component Registry - if idf_version() >= cv.Version(6, 0, 0) and ( + if config[CONF_TYPE] in _ALWAYS_EXTERNAL_IDF_COMPONENTS: + component = _IDF6_ETHERNET_COMPONENTS[config[CONF_TYPE]] + add_idf_component(name=component.name, ref=component.version) + elif idf_version() >= cv.Version(6, 0, 0) and ( + # IDF 6.0 moved per-chip PHY/MAC drivers to the Espressif Component Registry component := _IDF6_ETHERNET_COMPONENTS.get(config[CONF_TYPE]) ): add_idf_component(name=component.name, ref=component.version) @@ -555,7 +574,10 @@ async def _to_code_rp2040(var: cg.Pvariable, config: ConfigType) -> None: cg.add(var.set_reset_pin(config[CONF_RESET_PIN])) cg.add_define("USE_ETHERNET_SPI") - cg.add_library("lwIP_w5500", None) + if config[CONF_TYPE] == "ENC28J60": + cg.add_library("lwIP_enc28j60", None) + else: + cg.add_library("lwIP_w5500", None) def _final_validate_rmii_pins(config: ConfigType) -> None: diff --git a/esphome/components/ethernet/ethernet_component.h b/esphome/components/ethernet/ethernet_component.h index b6699e8020..4c85c39eb8 100644 --- a/esphome/components/ethernet/ethernet_component.h +++ b/esphome/components/ethernet/ethernet_component.h @@ -23,7 +23,13 @@ extern "C" eth_esp32_emac_config_t eth_esp32_emac_default_config(void); #endif // USE_ESP32 #ifdef USE_RP2040 +#if defined(USE_ETHERNET_W5500) #include +#elif defined(USE_ETHERNET_ENC28J60) +#include +#else +#error "Unsupported RP2040 SPI Ethernet type" +#endif #endif namespace esphome::ethernet { @@ -57,6 +63,7 @@ enum EthernetType : uint8_t { ETHERNET_TYPE_OPENETH, ETHERNET_TYPE_DM9051, ETHERNET_TYPE_LAN8670, + ETHERNET_TYPE_ENC28J60, }; struct ManualIP { @@ -215,7 +222,13 @@ class EthernetComponent final : public Component { #ifdef USE_RP2040 static constexpr uint32_t LINK_CHECK_INTERVAL = 500; // ms between link/IP polls +#if defined(USE_ETHERNET_W5500) Wiznet5500lwIP *eth_{nullptr}; +#elif defined(USE_ETHERNET_ENC28J60) + ENC28J60lwIP *eth_{nullptr}; +#else +#error "Unsupported RP2040 SPI Ethernet type" +#endif uint32_t last_link_check_{0}; uint8_t clk_pin_; uint8_t miso_pin_; diff --git a/esphome/components/ethernet/ethernet_component_esp32.cpp b/esphome/components/ethernet/ethernet_component_esp32.cpp index fb69a901aa..a170239e03 100644 --- a/esphome/components/ethernet/ethernet_component_esp32.cpp +++ b/esphome/components/ethernet/ethernet_component_esp32.cpp @@ -44,6 +44,11 @@ #include "esp_eth_phy_lan867x.h" #endif +// ENC28J60 header exists on all IDF versions (always an external component) +#ifdef USE_ETHERNET_ENC28J60 +#include "esp_eth_enc28j60.h" +#endif + #ifdef USE_ETHERNET_SPI #include #include @@ -194,25 +199,27 @@ void EthernetComponent::setup() { .post_cb = nullptr, }; -#ifdef USE_ETHERNET_W5500 +#if defined(USE_ETHERNET_W5500) eth_w5500_config_t w5500_config = ETH_W5500_DEFAULT_CONFIG(host, &devcfg); -#endif -#ifdef USE_ETHERNET_DM9051 +#elif defined(USE_ETHERNET_DM9051) eth_dm9051_config_t dm9051_config = ETH_DM9051_DEFAULT_CONFIG(host, &devcfg); +#elif defined(USE_ETHERNET_ENC28J60) + eth_enc28j60_config_t enc28j60_config = ETH_ENC28J60_DEFAULT_CONFIG(host, &devcfg); #endif -#ifdef USE_ETHERNET_W5500 +#if defined(USE_ETHERNET_W5500) w5500_config.int_gpio_num = this->interrupt_pin_; #ifdef USE_ETHERNET_SPI_POLLING_SUPPORT w5500_config.poll_period_ms = this->polling_interval_; #endif -#endif - -#ifdef USE_ETHERNET_DM9051 +#elif defined(USE_ETHERNET_DM9051) dm9051_config.int_gpio_num = this->interrupt_pin_; #ifdef USE_ETHERNET_SPI_POLLING_SUPPORT dm9051_config.poll_period_ms = this->polling_interval_; #endif +#elif defined(USE_ETHERNET_ENC28J60) + enc28j60_config.int_gpio_num = this->interrupt_pin_; + // ENC28J60 does not support poll_period_ms #endif phy_config.phy_addr = this->phy_addr_spi_; @@ -300,19 +307,24 @@ void EthernetComponent::setup() { #endif #endif #ifdef USE_ETHERNET_SPI -#ifdef USE_ETHERNET_W5500 +#if defined(USE_ETHERNET_W5500) case ETHERNET_TYPE_W5500: { mac = esp_eth_mac_new_w5500(&w5500_config, &mac_config); this->phy_ = esp_eth_phy_new_w5500(&phy_config); break; } -#endif -#ifdef USE_ETHERNET_DM9051 +#elif defined(USE_ETHERNET_DM9051) case ETHERNET_TYPE_DM9051: { mac = esp_eth_mac_new_dm9051(&dm9051_config, &mac_config); this->phy_ = esp_eth_phy_new_dm9051(&phy_config); break; } +#elif defined(USE_ETHERNET_ENC28J60) + case ETHERNET_TYPE_ENC28J60: { + mac = esp_eth_mac_new_enc28j60(&enc28j60_config, &mac_config); + this->phy_ = esp_eth_phy_new_enc28j60(&phy_config); + break; + } #endif #endif default: { @@ -405,15 +417,18 @@ void EthernetComponent::dump_config() { eth_type = "KSZ8081RNA"; break; #endif -#ifdef USE_ETHERNET_W5500 +#if defined(USE_ETHERNET_W5500) case ETHERNET_TYPE_W5500: eth_type = "W5500"; break; -#endif -#ifdef USE_ETHERNET_DM9051 +#elif defined(USE_ETHERNET_DM9051) case ETHERNET_TYPE_DM9051: eth_type = "DM9051"; break; +#elif defined(USE_ETHERNET_ENC28J60) + case ETHERNET_TYPE_ENC28J60: + eth_type = "ENC28J60"; + break; #endif #ifdef USE_ETHERNET_OPENETH case ETHERNET_TYPE_OPENETH: diff --git a/esphome/components/ethernet/ethernet_component_rp2040.cpp b/esphome/components/ethernet/ethernet_component_rp2040.cpp index 77b1a22d66..bd8c458985 100644 --- a/esphome/components/ethernet/ethernet_component_rp2040.cpp +++ b/esphome/components/ethernet/ethernet_component_rp2040.cpp @@ -31,11 +31,15 @@ void EthernetComponent::setup() { reset_pin.digital_write(false); delay(1); // NOLINT reset_pin.digital_write(true); - delay(10); // NOLINT - wait for W5500 to initialize after reset + delay(10); // NOLINT - wait for chip to initialize after reset } - // Create the W5500 device instance + // Create the SPI Ethernet device instance +#if defined(USE_ETHERNET_W5500) this->eth_ = new Wiznet5500lwIP(this->cs_pin_, SPI, this->interrupt_pin_); // NOLINT +#elif defined(USE_ETHERNET_ENC28J60) + this->eth_ = new ENC28J60lwIP(this->cs_pin_, SPI, this->interrupt_pin_); // NOLINT +#endif // Set hostname before begin() so the LWIP netif gets it this->eth_->hostname(App.get_name().c_str()); @@ -61,7 +65,7 @@ void EthernetComponent::setup() { } if (!success) { - ESP_LOGE(TAG, "Failed to initialize W5500 Ethernet"); + ESP_LOGE(TAG, "Failed to initialize Ethernet"); delete this->eth_; // NOLINT(cppcoreguidelines-owning-memory) this->eth_ = nullptr; this->mark_failed(); @@ -164,9 +168,15 @@ void EthernetComponent::loop() { } void EthernetComponent::dump_config() { + const char *type_str = "Unknown"; +#if defined(USE_ETHERNET_W5500) + type_str = "W5500"; +#elif defined(USE_ETHERNET_ENC28J60) + type_str = "ENC28J60"; +#endif ESP_LOGCONFIG(TAG, "Ethernet:\n" - " Type: W5500\n" + " Type: %s\n" " Connected: %s\n" " CLK Pin: %u\n" " MISO Pin: %u\n" @@ -174,7 +184,7 @@ void EthernetComponent::dump_config() { " CS Pin: %u\n" " IRQ Pin: %d\n" " Reset Pin: %d", - YESNO(this->is_connected()), this->clk_pin_, this->miso_pin_, this->mosi_pin_, this->cs_pin_, + type_str, YESNO(this->is_connected()), this->clk_pin_, this->miso_pin_, this->mosi_pin_, this->cs_pin_, this->interrupt_pin_, this->reset_pin_); this->dump_connect_params_(); } @@ -216,13 +226,18 @@ const char *EthernetComponent::get_eth_mac_address_pretty_into_buffer( } eth_duplex_t EthernetComponent::get_duplex_mode() { - // W5500 is always full duplex + // Both W5500 and ENC28J60 are full-duplex on RP2040 return ETH_DUPLEX_FULL; } eth_speed_t EthernetComponent::get_link_speed() { +#ifdef USE_ETHERNET_ENC28J60 + // ENC28J60 is 10Mbps only + return ETH_SPEED_10M; +#else // W5500 is always 100Mbps return ETH_SPEED_100M; +#endif } bool EthernetComponent::powerdown() { diff --git a/tests/components/ethernet/common-enc28j60-rp2040.yaml b/tests/components/ethernet/common-enc28j60-rp2040.yaml new file mode 100644 index 0000000000..0718723d8a --- /dev/null +++ b/tests/components/ethernet/common-enc28j60-rp2040.yaml @@ -0,0 +1,18 @@ +ethernet: + type: ENC28J60 + clk_pin: 18 + mosi_pin: 19 + miso_pin: 16 + cs_pin: 17 + interrupt_pin: 21 + reset_pin: 20 + manual_ip: + static_ip: 192.168.178.56 + gateway: 192.168.178.1 + subnet: 255.255.255.0 + domain: .local + mac_address: "02:AA:BB:CC:DD:01" + on_connect: + - logger.log: "Ethernet connected!" + on_disconnect: + - logger.log: "Ethernet disconnected!" diff --git a/tests/components/ethernet/common-enc28j60.yaml b/tests/components/ethernet/common-enc28j60.yaml new file mode 100644 index 0000000000..6b288bed19 --- /dev/null +++ b/tests/components/ethernet/common-enc28j60.yaml @@ -0,0 +1,19 @@ +ethernet: + type: ENC28J60 + clk_pin: 19 + mosi_pin: 21 + miso_pin: 23 + cs_pin: 18 + interrupt_pin: 36 + reset_pin: 22 + clock_speed: 10Mhz + manual_ip: + static_ip: 192.168.178.56 + gateway: 192.168.178.1 + subnet: 255.255.255.0 + domain: .local + mac_address: "02:AA:BB:CC:DD:01" + on_connect: + - logger.log: "Ethernet connected!" + on_disconnect: + - logger.log: "Ethernet disconnected!" diff --git a/tests/components/ethernet/test-enc28j60.esp32-idf.yaml b/tests/components/ethernet/test-enc28j60.esp32-idf.yaml new file mode 100644 index 0000000000..b7c1b7f5aa --- /dev/null +++ b/tests/components/ethernet/test-enc28j60.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common-enc28j60.yaml diff --git a/tests/components/ethernet/test-enc28j60.rp2040-ard.yaml b/tests/components/ethernet/test-enc28j60.rp2040-ard.yaml new file mode 100644 index 0000000000..61d1cd86f5 --- /dev/null +++ b/tests/components/ethernet/test-enc28j60.rp2040-ard.yaml @@ -0,0 +1 @@ +<<: !include common-enc28j60-rp2040.yaml From 11b829dda17b8cca2174272069caf36d0dca2628 Mon Sep 17 00:00:00 2001 From: Daniel Kent <129895318+danielkent-net@users.noreply.github.com> Date: Mon, 23 Mar 2026 15:59:17 -0400 Subject: [PATCH 114/166] [spa06_spi] Add SPA06-003 Temperature and Pressure Sensor - SPI support (Part 3 of 3) (#14523) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- CODEOWNERS | 1 + esphome/components/spa06_spi/__init__.py | 0 esphome/components/spa06_spi/sensor.py | 41 +++++++++++ esphome/components/spa06_spi/spa06_spi.cpp | 72 +++++++++++++++++++ esphome/components/spa06_spi/spa06_spi.h | 22 ++++++ tests/components/spa06_spi/common.yaml | 15 ++++ .../components/spa06_spi/test.esp32-idf.yaml | 7 ++ .../spa06_spi/test.esp8266-ard.yaml | 7 ++ .../components/spa06_spi/test.rp2040-ard.yaml | 7 ++ 9 files changed, 172 insertions(+) create mode 100644 esphome/components/spa06_spi/__init__.py create mode 100644 esphome/components/spa06_spi/sensor.py create mode 100644 esphome/components/spa06_spi/spa06_spi.cpp create mode 100644 esphome/components/spa06_spi/spa06_spi.h create mode 100644 tests/components/spa06_spi/common.yaml create mode 100644 tests/components/spa06_spi/test.esp32-idf.yaml create mode 100644 tests/components/spa06_spi/test.esp8266-ard.yaml create mode 100644 tests/components/spa06_spi/test.rp2040-ard.yaml diff --git a/CODEOWNERS b/CODEOWNERS index e3e09cbc11..afe4cdb871 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -459,6 +459,7 @@ esphome/components/sonoff_d1/* @anatoly-savchenkov esphome/components/sound_level/* @kahrendt esphome/components/spa06_base/* @danielkent-net esphome/components/spa06_i2c/* @danielkent-net +esphome/components/spa06_spi/* @danielkent-net esphome/components/speaker/* @jesserockz @kahrendt esphome/components/speaker/media_player/* @kahrendt @synesthesiam esphome/components/speaker_source/* @kahrendt diff --git a/esphome/components/spa06_spi/__init__.py b/esphome/components/spa06_spi/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/spa06_spi/sensor.py b/esphome/components/spa06_spi/sensor.py new file mode 100644 index 0000000000..b82186ec21 --- /dev/null +++ b/esphome/components/spa06_spi/sensor.py @@ -0,0 +1,41 @@ +import logging + +import esphome.codegen as cg +from esphome.components import spi +from esphome.components.spi import CONF_SPI_MODE +import esphome.config_validation as cv + +from ..spa06_base import CONFIG_SCHEMA_BASE, to_code_base + +AUTO_LOAD = ["spa06_base"] +CODEOWNERS = ["@danielkent-net"] +DEPENDENCIES = ["spi"] + +spa06_ns = cg.esphome_ns.namespace("spa06_spi") +SPA06SPIComponent = spa06_ns.class_( + "SPA06SPIComponent", cg.PollingComponent, spi.SPIDevice +) + +_LOGGER = logging.getLogger(__name__) + +VALID_SPI_MODES = {3: "MODE3", "3": "MODE3", "MODE3": "MODE3"} + + +def check_spi_mode(config): + spi_mode = config.get(CONF_SPI_MODE) + if spi_mode not in VALID_SPI_MODES: + raise cv.Invalid("SPA06 only supports SPI mode 3") + return config + + +CONFIG_SCHEMA = cv.All( + CONFIG_SCHEMA_BASE.extend(spi.spi_device_schema(default_mode="mode3")).extend( + {cv.GenerateID(): cv.declare_id(SPA06SPIComponent)} + ), + check_spi_mode, +) + + +async def to_code(config): + var = await to_code_base(config) + await spi.register_spi_device(var, config) diff --git a/esphome/components/spa06_spi/spa06_spi.cpp b/esphome/components/spa06_spi/spa06_spi.cpp new file mode 100644 index 0000000000..9f3683d219 --- /dev/null +++ b/esphome/components/spa06_spi/spa06_spi.cpp @@ -0,0 +1,72 @@ +#include +#include + +#include "spa06_spi.h" +#include "esphome/components/spa06_base/spa06_base.h" +#include "esphome/components/spi/spi.h" + +// OR (|) register with SPA06_SPI_READ for read. +inline constexpr uint8_t SPA06_SPI_READ = 0x80; + +// AND (&) register with SPA06_SPI_WRITE for write. +inline constexpr uint8_t SPA06_SPI_WRITE = 0x7F; + +namespace esphome::spa06_spi { + +static const char *const TAG = "spa06_spi"; + +void SPA06SPIComponent::dump_config() { + SPA06Component::dump_config(); + LOG_SPI_DEVICE(this) +} + +void SPA06SPIComponent::setup() { + this->spi_setup(); + SPA06Component::setup(); +} + +void SPA06SPIComponent::protocol_reset() { + // Forces the device into SPI mode using a dummy read + uint8_t dummy_read = 0; + this->spa_read_byte(spa06_base::SPA06_ID, &dummy_read); +} + +// In SPI mode, only 7 bits of the register addresses are used; the MSB of register address +// is not used and replaced by a read/write bit (RW = ‘0’ for write and RW = ‘1’ for read). +// Example: address 0xF7 is accessed by using SPI register address 0x77. For write access, +// the byte 0x77 is transferred, for read access, the byte 0xF7 is transferred. +// The expressions SPA06_SPI_READ (| with register) and SPA06_SPI_WRITE (& with register) +// are defined for readability. + +bool SPA06SPIComponent::spa_read_byte(uint8_t a_register, uint8_t *data) { + this->enable(); + this->transfer_byte(a_register | SPA06_SPI_READ); + *data = this->transfer_byte(0); + this->disable(); + return true; +} + +bool SPA06SPIComponent::spa_write_byte(uint8_t a_register, uint8_t data) { + this->enable(); + this->transfer_byte(a_register & SPA06_SPI_WRITE); + this->transfer_byte(data); + this->disable(); + return true; +} + +bool SPA06SPIComponent::spa_read_bytes(uint8_t a_register, uint8_t *data, size_t len) { + this->enable(); + this->transfer_byte(a_register | SPA06_SPI_READ); + this->read_array(data, len); + this->disable(); + return true; +} + +bool SPA06SPIComponent::spa_write_bytes(uint8_t a_register, uint8_t *data, size_t len) { + this->enable(); + this->transfer_byte(a_register & SPA06_SPI_WRITE); + this->write_array(data, len); + this->disable(); + return true; +} +} // namespace esphome::spa06_spi diff --git a/esphome/components/spa06_spi/spa06_spi.h b/esphome/components/spa06_spi/spa06_spi.h new file mode 100644 index 0000000000..ffbc162d6f --- /dev/null +++ b/esphome/components/spa06_spi/spa06_spi.h @@ -0,0 +1,22 @@ +#pragma once + +#include "esphome/components/spa06_base/spa06_base.h" +#include "esphome/components/spi/spi.h" + +namespace esphome::spa06_spi { + +class SPA06SPIComponent : public spa06_base::SPA06Component, + public spi::SPIDevice { + void setup() override; + bool spa_read_byte(uint8_t a_register, uint8_t *data) override; + bool spa_write_byte(uint8_t a_register, uint8_t data) override; + bool spa_read_bytes(uint8_t a_register, uint8_t *data, size_t len) override; + bool spa_write_bytes(uint8_t a_register, uint8_t *data, size_t len) override; + void dump_config() override; + + protected: + void protocol_reset() override; +}; + +} // namespace esphome::spa06_spi diff --git a/tests/components/spa06_spi/common.yaml b/tests/components/spa06_spi/common.yaml new file mode 100644 index 0000000000..9263202ab4 --- /dev/null +++ b/tests/components/spa06_spi/common.yaml @@ -0,0 +1,15 @@ +sensor: + - platform: spa06_spi + spi_id: spi_bus + cs_pin: ${cs_pin} + temperature: + id: spa06_spi_temperature + name: Outside Temperature + sample_rate: 1 + oversampling: NONE + pressure: + name: Outside Pressure + id: spa06_spi_pressure + sample_rate: 25p4 + oversampling: 16X + update_interval: 15s diff --git a/tests/components/spa06_spi/test.esp32-idf.yaml b/tests/components/spa06_spi/test.esp32-idf.yaml new file mode 100644 index 0000000000..a3352cf880 --- /dev/null +++ b/tests/components/spa06_spi/test.esp32-idf.yaml @@ -0,0 +1,7 @@ +substitutions: + cs_pin: GPIO5 + +packages: + spi: !include ../../test_build_components/common/spi/esp32-idf.yaml + +<<: !include common.yaml diff --git a/tests/components/spa06_spi/test.esp8266-ard.yaml b/tests/components/spa06_spi/test.esp8266-ard.yaml new file mode 100644 index 0000000000..595f31046a --- /dev/null +++ b/tests/components/spa06_spi/test.esp8266-ard.yaml @@ -0,0 +1,7 @@ +substitutions: + cs_pin: GPIO15 + +packages: + spi: !include ../../test_build_components/common/spi/esp8266-ard.yaml + +<<: !include common.yaml diff --git a/tests/components/spa06_spi/test.rp2040-ard.yaml b/tests/components/spa06_spi/test.rp2040-ard.yaml new file mode 100644 index 0000000000..93e19cfea4 --- /dev/null +++ b/tests/components/spa06_spi/test.rp2040-ard.yaml @@ -0,0 +1,7 @@ +substitutions: + cs_pin: GPIO17 + +packages: + spi: !include ../../test_build_components/common/spi/rp2040-ard.yaml + +<<: !include common.yaml From 6956bf7e539589ee06282d8560b97f91989397c3 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Tue, 24 Mar 2026 07:24:25 +1000 Subject: [PATCH 115/166] [text] Add text_sensor for read-only view of text component (#15090) --- .../components/text/text_sensor/__init__.py | 25 +++++++++++++++++++ .../text/text_sensor/text_text_sensor.cpp | 16 ++++++++++++ .../text/text_sensor/text_text_sensor.h | 19 ++++++++++++++ tests/components/text/common.yaml | 5 ++++ 4 files changed, 65 insertions(+) create mode 100644 esphome/components/text/text_sensor/__init__.py create mode 100644 esphome/components/text/text_sensor/text_text_sensor.cpp create mode 100644 esphome/components/text/text_sensor/text_text_sensor.h diff --git a/esphome/components/text/text_sensor/__init__.py b/esphome/components/text/text_sensor/__init__.py new file mode 100644 index 0000000000..5e45f10193 --- /dev/null +++ b/esphome/components/text/text_sensor/__init__.py @@ -0,0 +1,25 @@ +import esphome.codegen as cg +from esphome.components import text_sensor +import esphome.config_validation as cv +from esphome.const import CONF_SOURCE_ID + +from .. import Text, text_ns + +TextTextSensor = text_ns.class_("TextTextSensor", text_sensor.TextSensor, cg.Component) + + +CONFIG_SCHEMA = ( + text_sensor.text_sensor_schema(TextTextSensor) + .extend( + { + cv.Required(CONF_SOURCE_ID): cv.use_id(Text), + } + ) + .extend(cv.COMPONENT_SCHEMA) +) + + +async def to_code(config): + source = await cg.get_variable(config[CONF_SOURCE_ID]) + var = await text_sensor.new_text_sensor(config, source) + await cg.register_component(var, config) diff --git a/esphome/components/text/text_sensor/text_text_sensor.cpp b/esphome/components/text/text_sensor/text_text_sensor.cpp new file mode 100644 index 0000000000..50504c605e --- /dev/null +++ b/esphome/components/text/text_sensor/text_text_sensor.cpp @@ -0,0 +1,16 @@ +#include "text_text_sensor.h" +#include "esphome/core/log.h" + +namespace esphome::text { + +static const char *const TAG = "text.text_sensor"; + +void TextTextSensor::setup() { + this->source_->add_on_state_callback([this](const std::string &value) { this->publish_state(value); }); + if (this->source_->has_state()) + this->publish_state(this->source_->state); +} + +void TextTextSensor::dump_config() { LOG_TEXT_SENSOR("", "Text Text Sensor", this); } + +} // namespace esphome::text diff --git a/esphome/components/text/text_sensor/text_text_sensor.h b/esphome/components/text/text_sensor/text_text_sensor.h new file mode 100644 index 0000000000..fd70ea3451 --- /dev/null +++ b/esphome/components/text/text_sensor/text_text_sensor.h @@ -0,0 +1,19 @@ +#pragma once + +#include "../text.h" +#include "esphome/core/component.h" +#include "esphome/components/text_sensor/text_sensor.h" + +namespace esphome::text { + +class TextTextSensor : public text_sensor::TextSensor, public Component { + public: + explicit TextTextSensor(Text *source) : source_(source) {} + void setup() override; + void dump_config() override; + + protected: + Text *source_; +}; + +} // namespace esphome::text diff --git a/tests/components/text/common.yaml b/tests/components/text/common.yaml index 26618be03a..561d17143f 100644 --- a/tests/components/text/common.yaml +++ b/tests/components/text/common.yaml @@ -23,3 +23,8 @@ text: min_length: 8 max_length: 32 mode: password + +text_sensor: + - platform: text + name: "Test Text State" + source_id: test_text From 332118db5644b161c09c90f64e07f2289d6f86a9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Mar 2026 11:44:26 -1000 Subject: [PATCH 116/166] Bump pytest-cov from 7.0.0 to 7.1.0 (#15123) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 0d3f0671f5..1440b20333 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -6,7 +6,7 @@ pre-commit # Unit tests pytest==9.0.2 -pytest-cov==7.0.0 +pytest-cov==7.1.0 pytest-mock==3.15.1 pytest-asyncio==1.3.0 pytest-xdist==3.8.0 From bf6000ef3d38bf663d4fc2d69a220d5a64889e78 Mon Sep 17 00:00:00 2001 From: Javier Peletier Date: Mon, 23 Mar 2026 23:50:28 +0100 Subject: [PATCH 117/166] [substitutions] substitutions pass and !include redesign (package refactor part 2b) (#14918) Co-authored-by: J. Nick Koston Co-authored-by: J. Nick Koston --- esphome/components/packages/__init__.py | 9 +- esphome/components/substitutions/__init__.py | 444 +++++++++++++----- esphome/components/substitutions/jinja.py | 94 +--- esphome/config.py | 30 +- esphome/yaml_util.py | 41 +- .../component_tests/packages/test_packages.py | 2 + .../substitutions/00-simple_var.approved.yaml | 17 + .../substitutions/00-simple_var.input.yaml | 10 + .../02-expressions.approved.yaml | 6 + .../substitutions/02-expressions.input.yaml | 6 + .../07-package_merging.approved.yaml | 46 ++ .../07-package_merging.input.yaml | 63 +++ ...-include_vars_without_substs.approved.yaml | 5 + .../09-include_vars_without_substs.input.yaml | 7 + tests/unit_tests/test_substitutions.py | 244 +++++++++- tests/unit_tests/test_yaml_util.py | 2 +- 16 files changed, 753 insertions(+), 273 deletions(-) create mode 100644 tests/unit_tests/fixtures/substitutions/07-package_merging.approved.yaml create mode 100644 tests/unit_tests/fixtures/substitutions/07-package_merging.input.yaml create mode 100644 tests/unit_tests/fixtures/substitutions/09-include_vars_without_substs.approved.yaml create mode 100644 tests/unit_tests/fixtures/substitutions/09-include_vars_without_substs.input.yaml diff --git a/esphome/components/packages/__init__.py b/esphome/components/packages/__init__.py index 6d353ccf11..793cb946dd 100644 --- a/esphome/components/packages/__init__.py +++ b/esphome/components/packages/__init__.py @@ -226,7 +226,7 @@ def _process_remote_package(config: dict, skip_update: bool = False) -> dict: raise cv.Invalid( f"Current ESPHome Version is too old to use this package: {ESPHOME_VERSION} < {min_version}" ) - new_yaml = yaml_util.substitute_vars(new_yaml, vars) + new_yaml = yaml_util.add_context(new_yaml, vars or None) packages[f"{filename}{idx}"] = new_yaml except EsphomeError as e: raise cv.Invalid( @@ -296,6 +296,13 @@ def do_packages_pass(config: dict, skip_update: bool = False) -> dict: def process_package_callback(package_config: dict) -> dict: """This will be called for each package found in the config.""" + if isinstance(package_config, yaml_util.ConfigContext): + context_vars = package_config.vars + if CONF_PACKAGES in package_config or CONF_URL in package_config: + # Remote package definition: eagerly resolve before PACKAGE_SCHEMA validation. + from esphome.components.substitutions import substitute_context_vars + + substitute_context_vars(package_config, context_vars) package_config = PACKAGE_SCHEMA(package_config) if isinstance(package_config, str): return package_config # Jinja string, skip processing diff --git a/esphome/components/substitutions/__init__.py b/esphome/components/substitutions/__init__.py index 7e15f714f7..ecee816ce9 100644 --- a/esphome/components/substitutions/__init__.py +++ b/esphome/components/substitutions/__init__.py @@ -1,31 +1,50 @@ +from collections import ChainMap import logging -from re import Match from typing import Any from esphome import core from esphome.config_helpers import Extend, Remove, merge_config, merge_dicts_ordered import esphome.config_validation as cv from esphome.const import CONF_SUBSTITUTIONS, VALID_SUBSTITUTIONS_CHARACTERS -from esphome.yaml_util import ESPHomeDataBase, ESPLiteralValue, make_data_base +from esphome.types import ConfigType +from esphome.util import OrderedDict +from esphome.yaml_util import ( + ConfigContext, + ESPHomeDataBase, + ESPLiteralValue, + make_data_base, +) -from .jinja import Jinja, JinjaError, JinjaStr, has_jinja +from .jinja import Jinja, JinjaError, Missing, Resolver, UndefinedError, has_jinja CODEOWNERS = ["@esphome/core"] _LOGGER = logging.getLogger(__name__) +ContextVars = ChainMap[str, Any] +SubstitutionPath = list[int | str] +ErrList = list[tuple[UndefinedError, SubstitutionPath, Any]] +# Module-level instance is safe: context_vars is passed per-call, and context_trace +# is stack-saved/restored within expand(). Not thread-safe — only use from one thread. +jinja = Jinja() -def validate_substitution_key(value): + +def validate_substitution_key(value: Any) -> str: + """Validate and normalize a substitution key, stripping a leading ``$`` if present.""" value = cv.string(value) if not value: raise cv.Invalid("Substitution key must not be empty") if value[0] == "$": value = value[1:] + if not value: + raise cv.Invalid("Substitution key must not be empty") if value[0].isdigit(): raise cv.Invalid("First character in substitutions cannot be a digit.") for char in value: if char not in VALID_SUBSTITUTIONS_CHARACTERS: raise cv.Invalid( - f"Substitution must only consist of upper/lowercase characters, the underscore and numbers. The character '{char}' cannot be used" + f"Substitution must only consist of upper/lowercase characters," + f" the underscore and numbers." + f" The character '{char}' cannot be used" ) return value @@ -37,8 +56,8 @@ CONFIG_SCHEMA = cv.Schema( ) -async def to_code(config): - pass +async def to_code(config: ConfigType) -> None: + """No runtime code generation needed — substitutions are resolved at config time.""" def _restore_data_base(value: Any, orig_value: ESPHomeDataBase) -> ESPHomeDataBase: @@ -62,91 +81,122 @@ def _restore_data_base(value: Any, orig_value: ESPHomeDataBase) -> ESPHomeDataBa return value -def _expand_jinja( - value: str | JinjaStr, - orig_value: str | JinjaStr, - path, - jinja: Jinja, - ignore_missing: bool, -) -> Any: - if has_jinja(value): - # If the original value passed in to this function is a JinjaStr, it means it contains an unresolved - # Jinja expression from a previous pass. - if isinstance(orig_value, JinjaStr): - # Rebuild the JinjaStr in case it was lost while replacing substitutions. - value = JinjaStr(value, orig_value.upvalues) - try: - # Invoke the jinja engine to evaluate the expression. - value, err = jinja.expand(value) - if err is not None and not ignore_missing and "password" not in path: - _LOGGER.warning( - "Found '%s' (see %s) which looks like an expression," - " but could not resolve all the variables: %s", - value, - "->".join(str(x) for x in path), - err.message, - ) - except JinjaError as err: - raise cv.Invalid( - f"{err.error_name()} Error evaluating jinja expression '{value}': {str(err.parent())}." - f"\nEvaluation stack: (most recent evaluation last)\n{err.stack_trace_str()}" - f"\nRelevant context:\n{err.context_trace_str()}" - f"\nSee {'->'.join(str(x) for x in path)}", - path, - ) - # If the original, unexpanded string, contained document metadata (ESPHomeDatabase), - # assign this same document metadata to the resulting value. - if isinstance(orig_value, ESPHomeDataBase): - value = _restore_data_base(value, orig_value) +def _try_substitute(value: Any, context: ContextVars) -> Any: + """Substitute variables in value, returning the result or the original if unchanged.""" + result = _substitute_item(value, [], context, strict_undefined=True) + return result if result is not None else value - return value + +def _resolve_var(name: str, context_vars: ContextVars) -> Any: + """Look up a substitution variable, falling back to the resolver callback.""" + sub = context_vars.get(name, Missing) + if sub is Missing: + resolver = context_vars.get(Resolver) + if resolver: + sub = resolver(name) + return sub + + +def _handle_undefined( + err: UndefinedError, + path: SubstitutionPath, + value: Any, + strict_undefined: bool, + errors: ErrList | None, +) -> None: + """Handle an undefined variable. + + In strict mode, raises immediately. Otherwise, appends to the errors + list for deferred warning at the end of the substitution pass. + """ + if strict_undefined: + raise err + if errors is not None: + errors.append((err, path, value)) def _expand_substitutions( - substitutions: dict, value: str, path, jinja: Jinja, ignore_missing: bool + value: str, + path: SubstitutionPath, + context_vars: ContextVars, + strict_undefined: bool, + errors: ErrList | None, ) -> Any: + """Expand ``$var``, ``${var}``, and Jinja expressions in a string. + + Works in two phases: + + 1. **Simple substitution** — scan for ``$name`` / ``${name}`` tokens + and replace them with the value from *context_vars*. If the token + spans the entire string, return the raw value (preserving type). + 2. **Jinja evaluation** — if the result still contains Jinja syntax + (e.g. ``${a * b}``), render it through the Jinja engine with the + full *context_vars* as template variables. + + Returns the expanded value (may be a non-string type) or the + original *value* unchanged if there is nothing to substitute. + """ if "$" not in value: return value orig_value = value - i = 0 - while True: - m: Match[str] = cv.VARIABLE_PROG.search(value, i) - if not m: - # No more variable substitutions found. See if the remainder looks like a jinja template - value = _expand_jinja(value, orig_value, path, jinja, ignore_missing) - break - - i, j = m.span(0) + # Phase 1: Replace $var and ${var} references + search_pos = 0 + while (m := cv.VARIABLE_PROG.search(value, search_pos)) is not None: + match_start, match_end = m.span(0) name: str = m.group(1) if name.startswith("{") and name.endswith("}"): name = name[1:-1] - if name not in substitutions: - if not ignore_missing and "password" not in path: - _LOGGER.warning( - "Found '%s' (see %s) which looks like a substitution, but '%s' was " - "not declared", - orig_value, - "->".join(str(x) for x in path), - name, - ) - i = j + sub = _resolve_var(name, context_vars) + if sub is Missing: + _handle_undefined( + err=UndefinedError(f"'{name}' is undefined"), + path=path, + value=value, + strict_undefined=strict_undefined, + errors=errors, + ) + search_pos = match_end continue - sub: Any = substitutions[name] - - if i == 0 and j == len(value): - # The variable spans the whole expression, e.g., "${varName}". Return its resolved value directly - # to conserve its type. + if match_start == 0 and match_end == len(value): + # The variable spans the whole expression, e.g., "${varName}". + # Return its resolved value directly to conserve its type. value = sub break - tail = value[j:] - value = value[:i] + str(sub) - i = len(value) + tail = value[match_end:] + value = value[:match_start] + str(sub) + search_pos = len(value) value += tail + # Phase 2: Evaluate any remaining jinja expressions (e.g., "${a * b}") + if isinstance(value, str) and has_jinja(value): + try: + value = jinja.expand(value, context_vars) + except UndefinedError as err: + _handle_undefined( + err=err, + path=path, + value=value, + strict_undefined=strict_undefined, + errors=errors, + ) + except JinjaError as err: + raise cv.Invalid( + f"{err.error_name()} Error evaluating jinja expression" + f" '{value}': {str(err.parent())}." + f"\nEvaluation stack: (most recent evaluation last)" + f"\n{err.stack_trace_str()}" + f"\nRelevant context:\n{err.context_trace_str()}" + f"\nSee {'->'.join(str(x) for x in path)}", + path, + ) + else: + if isinstance(orig_value, ESPHomeDataBase): + value = _restore_data_base(value, orig_value) + # orig_value can also already be a lambda with esp_range info, and only # a plain string is sent in orig_value if isinstance(orig_value, ESPHomeDataBase): @@ -157,83 +207,221 @@ def _expand_substitutions( return value +def _push_context( + local_vars: dict[str, Any], + parent_context: ContextVars, + errors: ErrList | None = None, +) -> tuple[ContextVars, dict[str, Any]]: + """Resolve local_vars and layer them on top of parent_context. + + Returns ``(child_context, resolved_vars)`` where *child_context* is a + new :class:`ChainMap` whose front map is *resolved_vars* (an + :class:`OrderedDict` of successfully-resolved variables). + + Variables may reference each other (e.g. ``b: ${a + 1}``). + Dependencies are resolved recursively via a *resolver* callback + that Jinja invokes on cache-miss. If vars are already in + dependency order, the loop iterates exactly once per variable. + + The ChainMap stack used during resolution is:: + + resolver_context → resolved_vars → parent maps … + ↑ ↑ + holds Resolver filled as vars + callback are resolved + """ + # Vars still waiting to be resolved — popped one-by-one by resolve(). + unresolved_vars = local_vars.copy() + # Accumulates resolved values in dependency order; becomes the front + # map of the returned child context so later lookups find them first. + resolved_vars = OrderedDict() + # The context callees will search: resolved_vars (initially empty) + # shadowing whatever the parent already provides. + context_vars = parent_context.new_child(resolved_vars) + + # Vars that failed resolution (missing or circular references). + # Maps name → (original_value, cause_error) for deferred warnings. + unresolvables: dict[str, tuple[Any, UndefinedError]] = {} + + # One extra child layer so the Resolver callback lives in its own + # map and doesn't pollute resolved_vars. + resolver_context = context_vars.new_child() + + def resolve(key: str) -> Any: + """Resolve a variable, recursively resolving any dependencies it references.""" + value = unresolved_vars.pop(key, Missing) + if value is Missing: + return Missing + try: + value = _try_substitute(value, resolver_context) + except UndefinedError as err: + unresolvables[key] = (value, err) + return Missing + resolved_vars[key] = value + return value + + # Set up the resolver for use during substitution + resolver_context[Resolver] = resolve + + # Resolve all variables, recursively resolving dependencies as needed. + # Each call to resolve() resolves that variable and any variables it depends on. + while unresolved_vars: + resolve(next(iter(unresolved_vars))) + + for name, (value, cause) in unresolvables.items(): + resolved_vars[name] = value + if errors is not None: + _handle_undefined( + err=UndefinedError( + f"Could not resolve substitution variable '{name}': {cause}" + ), + path=["substitutions", name], + value=value, + strict_undefined=False, + errors=errors, + ) + + return context_vars, resolved_vars + + +def push_context( + config_node: Any, + parent_context: ContextVars, + errors: ErrList | None = None, +) -> ContextVars: + """Returns the context vars this config node must be evaluated with.""" + if isinstance(config_node, ConfigContext): + return _push_context(config_node.vars, parent_context, errors)[0] + + # This node does not define any vars itself, so just return parent context + return parent_context + + def _substitute_item( - substitutions: dict, item: Any, - path: list[int | str], - jinja: Jinja, - ignore_missing: bool, + path: SubstitutionPath, + parent_context: ContextVars, + strict_undefined: bool, + errors: ErrList | None = None, ) -> Any | None: - if isinstance(item, ESPLiteralValue): - return None # do not substitute inside literal blocks - if isinstance(item, list): - for i, it in enumerate(item): - sub = _substitute_item(substitutions, it, path + [i], jinja, ignore_missing) - if sub is not None: - item[i] = sub - elif isinstance(item, dict): - replace_keys = [] - for k, v in item.items(): - if path or k != CONF_SUBSTITUTIONS: - sub = _substitute_item( - substitutions, k, path + [k], jinja, ignore_missing - ) + """Recursively substitute variables in a config item. + + Walks dicts, lists, strings, Lambdas, Extend, and Remove nodes, + replacing variable references with values from context_vars. + Mutates containers in-place; returns a replacement value for + strings/scalars, or None if the item was unchanged. + """ + + def _walk(item: Any, path: SubstitutionPath, parent_ctx: ContextVars) -> Any | None: + if isinstance(item, ESPLiteralValue): + return None # do not substitute inside literal blocks + + ctx = push_context(item, parent_ctx, errors) + + if isinstance(item, list): + for idx, it in enumerate(item): + sub = _walk(it, path + [idx], ctx) if sub is not None: - replace_keys.append((k, sub)) - sub = _substitute_item(substitutions, v, path + [k], jinja, ignore_missing) - if sub is not None: - item[k] = sub - for old, new in replace_keys: - if str(new) == str(old): - item[new] = item[old] - else: - item[new] = merge_config(item.get(old), item.get(new)) - del item[old] - elif isinstance(item, str): - sub = _expand_substitutions(substitutions, item, path, jinja, ignore_missing) - if isinstance(sub, JinjaStr) or sub != item: - return sub - elif isinstance(item, (core.Lambda, Extend, Remove)): - sub = _expand_substitutions( - substitutions, item.value, path, jinja, ignore_missing + item[idx] = sub + elif isinstance(item, dict): + replace_keys: list[tuple[str, Any]] = [] + for k, v in item.items(): + if path or k != CONF_SUBSTITUTIONS: + sub = _walk(k, path + [k], ctx) + if sub is not None: + replace_keys.append((k, sub)) + sub = _walk(v, path + [k], ctx) + if sub is not None: + item[k] = sub + for old, new in replace_keys: + if str(new) == str(old): + item[new] = item[old] + else: + item[new] = merge_config(item.get(new), item.get(old)) + del item[old] + elif isinstance(item, str): + sub = _expand_substitutions(item, path, ctx, strict_undefined, errors) + if not isinstance(sub, str) or sub != item: + return sub + elif isinstance(item, (core.Lambda, Extend, Remove)) and item.value: + sub = _expand_substitutions(item.value, path, ctx, strict_undefined, errors) + if sub != item.value: + item.value = sub + return None + + return _walk(item, path, parent_context) + + +def substitute_context_vars(node: Any, context_vars: dict[str, Any]) -> None: + """Eagerly substitute context vars into a config node in-place. + + Undefined variables are silently ignored — this is used before + the main substitution pass when not all variables are visible yet. + """ + _substitute_item(node, [], ContextVars(context_vars), strict_undefined=False) + + +def _warn_unresolved_variables(errors: ErrList) -> None: + """Log warnings for unresolved substitution variables, skipping password fields.""" + for err, path, expression in errors: + if "password" in path: + continue + location: str = "->".join(str(x) for x in path) + if isinstance(expression, ESPHomeDataBase) and expression.esp_range is not None: + location += f" in {str(expression.esp_range.start_mark)}" + + _LOGGER.warning( + "The string '%s' looks like an expression," + " but could not resolve all the variables: %s (see %s)", + expression, + err.message, + location, ) - if sub != item: - item.value = sub - return None def do_substitution_pass( - config: dict, command_line_substitutions: dict, ignore_missing: bool = False -) -> None: - if CONF_SUBSTITUTIONS not in config and not command_line_substitutions: - return + config: OrderedDict, command_line_substitutions: dict[str, Any] | None = None +) -> OrderedDict: + """Run the substitution pass over the entire config. - # Merge substitutions in config, overriding with substitutions coming from command line: + Extracts the ``substitutions:`` block, merges in any command-line + overrides, resolves inter-variable dependencies, then walks the + config tree replacing all ``$var`` / ``${expr}`` references. + Returns the (mutated) config dict with resolved substitutions + restored at the front. + """ + # Extract substitutions from config, overriding with substitutions coming from command line: # Use merge_dicts_ordered to preserve OrderedDict type for move_to_end() - substitutions = merge_dicts_ordered( - config.get(CONF_SUBSTITUTIONS, {}), command_line_substitutions or {} - ) - with cv.prepend_path("substitutions"): + substitutions = config.pop(CONF_SUBSTITUTIONS, {}) + with cv.prepend_path(CONF_SUBSTITUTIONS): if not isinstance(substitutions, dict): raise cv.Invalid( f"Substitutions must be a key to value mapping, got {type(substitutions)}" ) + substitutions = merge_dicts_ordered( + substitutions, command_line_substitutions or {} + ) - replace_keys = [] - for key, value in substitutions.items(): + replace_keys: list[tuple[str, str]] = [] + for key in substitutions: with cv.prepend_path(key): sub = validate_substitution_key(key) if sub != key: replace_keys.append((key, sub)) - substitutions[key] = value for old, new in replace_keys: substitutions[new] = substitutions[old] del substitutions[old] - config[CONF_SUBSTITUTIONS] = substitutions - # Move substitutions to the first place to replace substitutions in them correctly - config.move_to_end(CONF_SUBSTITUTIONS, False) + errors: ErrList = [] # Collect undefined errors during substitution + parent_context, substitutions = _push_context(substitutions, ContextVars(), errors) - # Create a Jinja environment that will consider substitutions in scope: - jinja = Jinja(substitutions) - _substitute_item(substitutions, config, [], jinja, ignore_missing) + _substitute_item(config, [], parent_context, False, errors) + + if errors: + _warn_unresolved_variables(errors) + + # Restore substitutions to front of dict for readability + if substitutions: + config[CONF_SUBSTITUTIONS] = substitutions + config.move_to_end(CONF_SUBSTITUTIONS, last=False) + return config diff --git a/esphome/components/substitutions/jinja.py b/esphome/components/substitutions/jinja.py index fb9f843da2..37e9fa4d2d 100644 --- a/esphome/components/substitutions/jinja.py +++ b/esphome/components/substitutions/jinja.py @@ -1,7 +1,6 @@ from ast import literal_eval -from collections.abc import Iterator +from collections.abc import Iterator, Mapping from itertools import chain, islice -import logging import math import re from types import GeneratorType @@ -9,16 +8,17 @@ from typing import Any import jinja2 as jinja from jinja2.nativetypes import NativeCodeGenerator, NativeTemplate - -from esphome.yaml_util import ESPLiteralValue +from jinja2.runtime import missing as Missing TemplateError = jinja.TemplateError TemplateSyntaxError = jinja.TemplateSyntaxError TemplateRuntimeError = jinja.TemplateRuntimeError UndefinedError = jinja.UndefinedError Undefined = jinja.Undefined +# Sentinel key for resolver callback in ContextVars. +# Dots are invalid in substitution names so this can never collide with user keys. +Resolver = ".resolver" -_LOGGER = logging.getLogger(__name__) DETECT_JINJA = r"(\$\{)" detect_jinja_re = re.compile( @@ -52,33 +52,6 @@ SAFE_GLOBALS = { } -class JinjaStr(str): - """ - Wraps a string containing an unresolved Jinja expression, - storing the variables visible to it when it failed to resolve. - For example, an expression inside a package, `${ A * B }` may fail - to resolve at package parsing time if `A` is a local package var - but `B` is a substitution defined in the root yaml. - Therefore, we store the value of `A` as an upvalue bound - to the original string so we may be able to resolve `${ A * B }` - later in the main substitutions pass. - """ - - Undefined = object() - - def __new__(cls, value: str, upvalues=None): - if isinstance(value, JinjaStr): - base = str(value) - merged = {**value.upvalues, **(upvalues or {})} - else: - base = value - merged = dict(upvalues or {}) - obj = super().__new__(cls, base) - obj.upvalues = merged - obj.result = JinjaStr.Undefined - return obj - - class JinjaError(Exception): def __init__(self, context_trace: dict, expr: str): self.context_trace = context_trace @@ -106,9 +79,13 @@ class JinjaError(Exception): class TrackerContext(jinja.runtime.Context): def resolve_or_missing(self, key): val = super().resolve_or_missing(key) - if isinstance(val, JinjaStr): - self.environment.context_trace[key] = val - val, _ = self.environment.expand(val) + if val is Missing: + # Variable not in the template context — check if a resolver callback + # was registered (by _push_context) to lazily resolve dependencies + # between substitution variables in the same block. + resolver = super().resolve_or_missing(Resolver) + if resolver is not Missing: + val = resolver(key) self.environment.context_trace[key] = val return val @@ -160,15 +137,13 @@ def _concat_nodes_override(values: Iterator[Any]) -> Any: class Jinja(jinja.Environment): - """ - Wraps a Jinja environment - """ + """Jinja environment configured for ESPHome substitution expressions.""" # jinja environment customization overrides code_generator_class = NativeCodeGenerator concat = staticmethod(_concat_nodes_override) - def __init__(self, context_vars: dict): + def __init__(self) -> None: super().__init__( trim_blocks=True, lstrip_blocks=True, @@ -183,49 +158,25 @@ class Jinja(jinja.Environment): self.context_class = TrackerContext self.add_extension("jinja2.ext.do") self.context_trace = {} - self.context_vars = {**context_vars} - for k, v in self.context_vars.items(): - if isinstance(v, ESPLiteralValue): - continue - if isinstance(v, str) and not isinstance(v, JinjaStr) and has_jinja(v): - self.context_vars[k] = JinjaStr(v, self.context_vars) - self.globals = { - **self.globals, - **self.context_vars, - **SAFE_GLOBALS, - } + self.globals = {**self.globals, **SAFE_GLOBALS} - def expand(self, content_str: str | JinjaStr) -> Any: + def expand(self, content_str: str, context_vars: Mapping[str, Any]) -> Any: """ Renders a string that may contain Jinja expressions or statements Returns the resulting value if all variables and expressions could be resolved. - Otherwise, it returns a tagged (JinjaStr) string that captures variables - in scope (upvalues), like a closure for later evaluation. """ result = None - override_vars = {} - if isinstance(content_str, JinjaStr): - if content_str.result is not JinjaStr.Undefined: - return content_str.result, None - # If `value` is already a JinjaStr, it means we are trying to evaluate it again - # in a parent pass. - # Hopefully, all required variables are visible now. - override_vars = content_str.upvalues old_trace = self.context_trace self.context_trace = {} try: template = self.from_string(content_str) - result = template.render(override_vars) + result = template.render(context_vars) if isinstance(result, Undefined): - print("" + result) # force a UndefinedError exception - except (TemplateSyntaxError, UndefinedError) as err: - # `content_str` contains a Jinja expression that refers to a variable that is undefined - # in this scope. Perhaps it refers to a root substitution that is not visible yet. - # Therefore, return `content_str` as a JinjaStr, which contains the variables - # that are actually visible to it at this point to postpone evaluation. - return JinjaStr(content_str, {**self.context_vars, **override_vars}), err + str(result) # force a UndefinedError exception + except UndefinedError as err: + raise err except JinjaError as err: err.context_trace = {**self.context_trace, **err.context_trace} err.eval_stack.append(content_str) @@ -242,10 +193,7 @@ class Jinja(jinja.Environment): finally: self.context_trace = old_trace - if isinstance(content_str, JinjaStr): - content_str.result = result - - return result, None + return result class JinjaTemplate(NativeTemplate): diff --git a/esphome/config.py b/esphome/config.py index 6f6ad4886b..b80aaf3700 100644 --- a/esphome/config.py +++ b/esphome/config.py @@ -12,7 +12,8 @@ from typing import Any import voluptuous as vol from esphome import core, loader, pins, yaml_util -from esphome.config_helpers import Extend, Remove, merge_config, merge_dicts_ordered +from esphome.components.substitutions import do_substitution_pass +from esphome.config_helpers import Extend, Remove, merge_config import esphome.config_validation as cv from esphome.const import ( CONF_ESPHOME, @@ -974,7 +975,7 @@ class PinUseValidationCheck(ConfigValidationStep): def validate_config( config: dict[str, Any], - command_line_substitutions: dict[str, Any], + command_line_substitutions: dict[str, Any] | None, skip_external_update: bool = False, ) -> Config: result = Config() @@ -994,21 +995,15 @@ def validate_config( result.add_error(err) return result - CORE.raw_config = config - # 1. Load substitutions if CONF_SUBSTITUTIONS in config or command_line_substitutions: - from esphome.components import substitutions - - result[CONF_SUBSTITUTIONS] = merge_dicts_ordered( - config.get(CONF_SUBSTITUTIONS) or {}, command_line_substitutions - ) result.add_output_path([CONF_SUBSTITUTIONS], CONF_SUBSTITUTIONS) - try: - substitutions.do_substitution_pass(config, command_line_substitutions) - except vol.Invalid as err: - result.add_error(err) - return result + try: + config = do_substitution_pass(config, command_line_substitutions) + except vol.Invalid as err: + CORE.raw_config = config + result.add_error(err) + return result # 1.1. Merge packages if CONF_PACKAGES in config: @@ -1016,6 +1011,9 @@ def validate_config( config = merge_packages(config) + # Remove substitutions from config during validation to prevent + # re-substitution. Re-added to result at the end of this function. + substitutions = config.pop(CONF_SUBSTITUTIONS, None) CORE.raw_config = config # 1.2. Resolve !extend and !remove and check for REPLACEME @@ -1089,6 +1087,10 @@ def validate_config( result.run_validation_steps() + if substitutions is not None: + result[CONF_SUBSTITUTIONS] = substitutions + result.move_to_end(CONF_SUBSTITUTIONS, last=False) + return result diff --git a/esphome/yaml_util.py b/esphome/yaml_util.py index d0eab4e44e..e001316a22 100644 --- a/esphome/yaml_util.py +++ b/esphome/yaml_util.py @@ -325,9 +325,7 @@ class ESPHomeLoaderMixin: return val @_add_data_ref - def construct_include( - self, node: yaml.Node - ) -> dict[str, Any] | OrderedDict[str, Any]: + def construct_include(self, node: yaml.Node) -> Any: from esphome.const import CONF_VARS def extract_file_vars(node): @@ -344,9 +342,7 @@ class ESPHomeLoaderMixin: file, vars = node.value, None result = self.yaml_loader(self._rel_path(file)) - if not vars: - vars = {} - return substitute_vars(result, vars) + return add_context(result, vars) @_add_data_ref def construct_include_dir_list(self, node: yaml.Node) -> list[dict[str, Any]]: @@ -495,39 +491,6 @@ def parse_yaml( ) -def substitute_vars(config, vars): - from esphome.components import substitutions - from esphome.const import CONF_SUBSTITUTIONS - - org_subs = None - result = config - if not isinstance(config, dict): - # when the included yaml contains a list or a scalar - # wrap it into an OrderedDict because do_substitution_pass expects it - result = OrderedDict([("yaml", config)]) - elif CONF_SUBSTITUTIONS in result: - org_subs = result.pop(CONF_SUBSTITUTIONS) - - defaults = {} - if CONF_DEFAULTS in result: - defaults = result.pop(CONF_DEFAULTS) - - result[CONF_SUBSTITUTIONS] = vars - for k, v in defaults.items(): - if k not in result[CONF_SUBSTITUTIONS]: - result[CONF_SUBSTITUTIONS][k] = v - - # Ignore missing vars that refer to the top level substitutions - substitutions.do_substitution_pass(result, None, ignore_missing=True) - result.pop(CONF_SUBSTITUTIONS) - - if not isinstance(config, dict): - result = result["yaml"] # unwrap the result - elif org_subs: - result[CONF_SUBSTITUTIONS] = org_subs - return result - - def _load_yaml_internal_with_type( loader_type: type[ESPHomeLoader] | type[ESPHomePurePythonLoader], fname: Path, diff --git a/tests/component_tests/packages/test_packages.py b/tests/component_tests/packages/test_packages.py index 22fb2c4e32..60dc0dccda 100644 --- a/tests/component_tests/packages/test_packages.py +++ b/tests/component_tests/packages/test_packages.py @@ -6,6 +6,7 @@ from unittest.mock import MagicMock, patch import pytest from esphome.components.packages import CONFIG_SCHEMA, do_packages_pass, merge_packages +from esphome.components.substitutions import do_substitution_pass import esphome.config as config_module from esphome.config import resolve_extend_remove from esphome.config_helpers import Extend, Remove @@ -71,6 +72,7 @@ def fixture_basic_esphome(): def packages_pass(config): """Wrapper around packages_pass that also resolves Extend and Remove.""" config = do_packages_pass(config) + config = do_substitution_pass(config) config = merge_packages(config) resolve_extend_remove(config) return config diff --git a/tests/unit_tests/fixtures/substitutions/00-simple_var.approved.yaml b/tests/unit_tests/fixtures/substitutions/00-simple_var.approved.yaml index 9ed9b99c49..87f0e3fa21 100644 --- a/tests/unit_tests/fixtures/substitutions/00-simple_var.approved.yaml +++ b/tests/unit_tests/fixtures/substitutions/00-simple_var.approved.yaml @@ -38,3 +38,20 @@ test_list: - '{ 79, 82 }' - a: 15 should be 15, overridden from command line b: 20 should stay as 20, not overridden + - aa: + - 1 + - 2 + - 3 + - 4 + - 5 + - 6 + bb: + - 7 + - 8 + - 9 + - aa: + x: 1 + y: 3 + z: 4 + bb: + w: 5 diff --git a/tests/unit_tests/fixtures/substitutions/00-simple_var.input.yaml b/tests/unit_tests/fixtures/substitutions/00-simple_var.input.yaml index 64701c03dd..d70372f280 100644 --- a/tests/unit_tests/fixtures/substitutions/00-simple_var.input.yaml +++ b/tests/unit_tests/fixtures/substitutions/00-simple_var.input.yaml @@ -44,3 +44,13 @@ test_list: - '{ ${position.x}, ${position.y} }' - a: ${a} should be 15, overridden from command line b: ${b} should stay as 20, not overridden + + # Test merging lists when substituted keys resolve to an existing key + - ${ "aa" }: [1, 2, 3] + ${ "a" + "a" }: [4, 5, 6] + ${ "bb" }: [7, 8, 9] + + # Test merging dicts when substituted keys resolve to an existing key + - ${ "aa" }: {"x": 1, "y": 2} + ${ "a" + "a" }: {"y": 3, "z": 4} + ${ "bb" }: {"w": 5} diff --git a/tests/unit_tests/fixtures/substitutions/02-expressions.approved.yaml b/tests/unit_tests/fixtures/substitutions/02-expressions.approved.yaml index 1a51fc44cf..b8c76fbf52 100644 --- a/tests/unit_tests/fixtures/substitutions/02-expressions.approved.yaml +++ b/tests/unit_tests/fixtures/substitutions/02-expressions.approved.yaml @@ -9,6 +9,11 @@ substitutions: numberOne: 1 var1: 79 double_width: 14 + double_height: 16 + y: ${x} + x: ${y} + b: 79 + c: 80 test_list: - The area is 56 - 56 @@ -27,3 +32,4 @@ test_list: - chr(97) = a - len([1,2,3]) = 3 - width = 7, double_width = 14 + - a = ${a} diff --git a/tests/unit_tests/fixtures/substitutions/02-expressions.input.yaml b/tests/unit_tests/fixtures/substitutions/02-expressions.input.yaml index 4612f581b5..9593867f49 100644 --- a/tests/unit_tests/fixtures/substitutions/02-expressions.input.yaml +++ b/tests/unit_tests/fixtures/substitutions/02-expressions.input.yaml @@ -1,4 +1,7 @@ substitutions: + y: ${x} # Circular reference, expect to pass unresolved. + x: ${y} # Circular reference, expect to pass unresolved. + double_height: ${height * 2} width: 7 height: 8 enabled: true @@ -9,6 +12,8 @@ substitutions: numberOne: 1 var1: 79 double_width: ${width * 2} + c: ${b+1} + b: ${undefined_variable | default(79) } test_list: - "The area is ${width * height}" @@ -25,3 +30,4 @@ test_list: - chr(97) = ${ chr(97) } - len([1,2,3]) = ${ len([1,2,3]) } - width = ${width}, double_width = ${double_width} + - a = ${a} diff --git a/tests/unit_tests/fixtures/substitutions/07-package_merging.approved.yaml b/tests/unit_tests/fixtures/substitutions/07-package_merging.approved.yaml new file mode 100644 index 0000000000..867889b7bc --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/07-package_merging.approved.yaml @@ -0,0 +1,46 @@ +fancy_component: &id001 + - id: component9 + value: 9 +some_component: + - id: component1 + value: 1 + - id: component2 + value: 2 + - id: component3 + value: 3 + - id: component4 + value: 4 + - id: component5 + value: 79 + power: 200 + - id: component6 + value: 6 + - id: component7 + value: 7 +switch: &id002 + - platform: gpio + id: switch1 + pin: 12 + - platform: gpio + id: switch2 + pin: 13 +display: + - platform: ili9xxx + dimensions: + width: 100 + height: 480 +substitutions: + extended_component: component5 + package_options: + alternative_package: + alternative_component: + - id: component8 + value: 8 + fancy_package: + substitutions: + fancy_subst: 42 + fancy_component: *id001 + pin: 12 + some_switches: *id002 + package_selection: fancy_package + fancy_subst: 42 diff --git a/tests/unit_tests/fixtures/substitutions/07-package_merging.input.yaml b/tests/unit_tests/fixtures/substitutions/07-package_merging.input.yaml new file mode 100644 index 0000000000..cc7b841aba --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/07-package_merging.input.yaml @@ -0,0 +1,63 @@ +substitutions: + package_options: + alternative_package: + alternative_component: + - id: component8 + value: 8 + fancy_package: + substitutions: + fancy_subst: 42 + fancy_component: + - id: component9 + value: 9 + + pin: 12 + some_switches: + - platform: gpio + id: switch1 + pin: ${pin} + - platform: gpio + id: switch2 + pin: ${pin+1} + + package_selection: fancy_package + +packages: + - ${ package_options[package_selection] } + - some_component: + - id: component1 + value: 1 + - some_component: + - id: component2 + value: 2 + - switch: ${ some_switches } + - packages: + package_with_defaults: !include + file: display.yaml + vars: + native_width: 100 + high_dpi: false + my_package: + packages: + - packages: + special_package: + substitutions: + extended_component: component5 + some_component: + - id: component3 + value: 3 + some_component: + - id: component4 + value: 4 + - id: !extend ${ extended_component } + power: 200 + value: 79 + some_component: + - id: component5 + value: 5 + +some_component: + - id: component6 + value: 6 + - id: component7 + value: 7 diff --git a/tests/unit_tests/fixtures/substitutions/09-include_vars_without_substs.approved.yaml b/tests/unit_tests/fixtures/substitutions/09-include_vars_without_substs.approved.yaml new file mode 100644 index 0000000000..4abaf4471d --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/09-include_vars_without_substs.approved.yaml @@ -0,0 +1,5 @@ +values: + - var1: $var1 + - a: 10 + - b: B-default + - c: The value of C is 79 diff --git a/tests/unit_tests/fixtures/substitutions/09-include_vars_without_substs.input.yaml b/tests/unit_tests/fixtures/substitutions/09-include_vars_without_substs.input.yaml new file mode 100644 index 0000000000..91eb0e9a3f --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/09-include_vars_without_substs.input.yaml @@ -0,0 +1,7 @@ +# Test that include_vars with vars works even when there are no substitutions key defined. +packages: + - !include + file: inc1.yaml + vars: + a: 10 + c: 79 diff --git a/tests/unit_tests/test_substitutions.py b/tests/unit_tests/test_substitutions.py index 1d8cb7631d..db46a27dfb 100644 --- a/tests/unit_tests/test_substitutions.py +++ b/tests/unit_tests/test_substitutions.py @@ -10,9 +10,10 @@ from esphome import config as config_module, yaml_util from esphome.components import substitutions from esphome.components.packages import do_packages_pass, merge_packages from esphome.config import resolve_extend_remove -from esphome.config_helpers import merge_config +from esphome.config_helpers import Extend, merge_config +import esphome.config_validation as cv from esphome.const import CONF_SUBSTITUTIONS -from esphome.core import CORE +from esphome.core import CORE, Lambda from esphome.util import OrderedDict _LOGGER = logging.getLogger(__name__) @@ -144,7 +145,7 @@ def test_substitutions_fixtures( config = do_packages_pass(config) - substitutions.do_substitution_pass(config, command_line_substitutions) + config = substitutions.do_substitution_pass(config, command_line_substitutions) config = merge_packages(config) @@ -206,7 +207,7 @@ def test_substitutions_with_command_line_maintains_ordered_dict() -> None: command_line_subs = {"var2": "override", "var3": "new_value"} # Call do_substitution_pass with command line substitutions - substitutions.do_substitution_pass(config, command_line_subs) + config = substitutions.do_substitution_pass(config, command_line_subs) # Verify that config is still an OrderedDict assert isinstance(config, OrderedDict), "Config should remain an OrderedDict" @@ -234,7 +235,7 @@ def test_substitutions_without_command_line_maintains_ordered_dict() -> None: config["other_key"] = "other_value" # Call without command line substitutions - substitutions.do_substitution_pass(config, None) + config = substitutions.do_substitution_pass(config, None) # Verify that config is still an OrderedDict assert isinstance(config, OrderedDict), "Config should remain an OrderedDict" @@ -268,7 +269,7 @@ def test_substitutions_after_merge_config_maintains_ordered_dict() -> None: ) # Now try to run substitution pass on the merged config - substitutions.do_substitution_pass(merged_config, None) + merged_config = substitutions.do_substitution_pass(merged_config, None) # Should not raise AttributeError assert isinstance(merged_config, OrderedDict), ( @@ -279,7 +280,7 @@ def test_substitutions_after_merge_config_maintains_ordered_dict() -> None: def test_validate_config_with_command_line_substitutions_maintains_ordered_dict( - tmp_path, + tmp_path: Path, ) -> None: """Test that validate_config preserves OrderedDict when merging command-line substitutions. @@ -288,7 +289,7 @@ def test_validate_config_with_command_line_substitutions_maintains_ordered_dict( """ # Create a minimal valid config test_config = OrderedDict() - test_config["esphome"] = {"name": "test_device", "platform": "ESP32"} + test_config["esphome"] = {"name": "test_device"} test_config[CONF_SUBSTITUTIONS] = OrderedDict({"var1": "value1", "var2": "value2"}) test_config["esp32"] = {"board": "esp32dev"} @@ -314,17 +315,11 @@ def test_validate_config_with_command_line_substitutions_maintains_ordered_dict( assert result[CONF_SUBSTITUTIONS]["var3"] == "new_value" -def test_validate_config_without_command_line_substitutions_maintains_ordered_dict( - tmp_path, -) -> None: - """Test that validate_config preserves OrderedDict without command-line substitutions. - - This tests the code path in config.py where result[CONF_SUBSTITUTIONS] is set - using merge_dicts_ordered() when command_line_substitutions is None. - """ +def _get_test_minimal_valid_config(tmp_path: Path) -> OrderedDict: + """Helper to create a minimal valid config for testing.""" # Create a minimal valid config test_config = OrderedDict() - test_config["esphome"] = {"name": "test_device", "platform": "ESP32"} + test_config["esphome"] = {"name": "test_device"} test_config[CONF_SUBSTITUTIONS] = OrderedDict({"var1": "value1", "var2": "value2"}) test_config["esp32"] = {"board": "esp32dev"} @@ -332,6 +327,19 @@ def test_validate_config_without_command_line_substitutions_maintains_ordered_di test_yaml = tmp_path / "test.yaml" test_yaml.write_text("# test config") CORE.config_path = test_yaml + return test_config + + +def test_validate_config_without_command_line_substitutions_maintains_ordered_dict( + tmp_path: Path, +) -> None: + """Test that validate_config preserves OrderedDict without command-line substitutions. + + This tests the code path in config.py where result[CONF_SUBSTITUTIONS] is set + using merge_dicts_ordered() when command_line_substitutions is None. + """ + + test_config = _get_test_minimal_valid_config(tmp_path) # Call validate_config without command line substitutions result = config_module.validate_config(test_config, None) @@ -384,3 +392,205 @@ def test_merge_config_preserves_ordered_dict() -> None: assert not isinstance(result, OrderedDict), ( "dict + dict should not return OrderedDict" ) + + +def test_substitution_pass_error_gets_captured( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + """vol.Invalid from do_substitution_pass is captured by validate_config.""" + + # Patch the target: in config_module.do_substitution_pass (NOT where it's defined) + def fake_do_substitution_pass(*args, **kwargs): + raise cv.Invalid("Error in do_substitutions_pass!!") + + monkeypatch.setattr( + config_module, "do_substitution_pass", fake_do_substitution_pass + ) + + # Prepare minimal config + no CLI substitutions + config = _get_test_minimal_valid_config(tmp_path) + + # Call the function under test + result = config_module.validate_config(config, None) + + # Now assert that add_error was called with the vol.Invalid + + assert "Error in do_substitutions_pass!!" in str(result.get_error_for_path([])) + + +@pytest.mark.parametrize( + "value", ["", " ", "1foo", "9VAR", "0abc", "$1foo", "$9VAR", "$0abc"] +) +def test_validate_substitution_key_empty_raises(value: str) -> None: + """Empty (or all-whitespace) substitution keys are rejected.""" + with pytest.raises(cv.Invalid): + substitutions.validate_substitution_key(value) + + +@pytest.mark.parametrize( + "input_value, expected_output", + [ + ("$FOO_bar9", "FOO_bar9"), # Valid key with leading '$' + ("Foo_bar9", "Foo_bar9"), # Normal valid key + ], +) +def test_validate_substitution_key_valid( + input_value: str, expected_output: str +) -> None: + """Valid substitution keys are accepted with optional leading '$'.""" + result = substitutions.validate_substitution_key(input_value) + assert result == expected_output + + +def test_circular_dependency_warnings( + caplog: pytest.LogCaptureFixture, +) -> None: + """Circular substitution references produce warnings naming the cause.""" + config = OrderedDict( + { + CONF_SUBSTITUTIONS: OrderedDict({"x": "${y}", "y": "${x}"}), + "key": "value", + } + ) + with caplog.at_level(logging.WARNING): + substitutions.do_substitution_pass(config) + + assert "Could not resolve substitution variable 'x'" in caplog.text + assert "'y' is undefined" in caplog.text + assert "Could not resolve substitution variable 'y'" in caplog.text + assert "'x' is undefined" in caplog.text + # Verify path includes location + assert "substitutions->x" in caplog.text + assert "substitutions->y" in caplog.text + + +def test_missing_dependency_warning( + caplog: pytest.LogCaptureFixture, +) -> None: + """A substitution referencing an undefined variable warns with the cause.""" + config = OrderedDict( + { + CONF_SUBSTITUTIONS: OrderedDict({"a": "${missing}"}), + "key": "value", + } + ) + with caplog.at_level(logging.WARNING): + substitutions.do_substitution_pass(config) + + assert "Could not resolve substitution variable 'a'" in caplog.text + assert "'missing' is undefined" in caplog.text + assert "substitutions->a" in caplog.text + + +def test_undefined_variable_warning( + caplog: pytest.LogCaptureFixture, +) -> None: + """A reference to an undefined variable in config values produces a warning.""" + config = OrderedDict( + { + "key": "${undefined_var}", + } + ) + with caplog.at_level(logging.WARNING): + substitutions.do_substitution_pass(config) + + assert "'undefined_var' is undefined" in caplog.text + + +def test_password_field_warnings_suppressed( + caplog: pytest.LogCaptureFixture, +) -> None: + """Undefined variables in password fields should not produce warnings.""" + config = OrderedDict( + { + "password": "${undefined_var}", + } + ) + with caplog.at_level(logging.WARNING): + substitutions.do_substitution_pass(config) + + assert caplog.text == "" + + +def test_config_context_unresolvable_warns( + caplog: pytest.LogCaptureFixture, +) -> None: + """Unresolvable vars in a ConfigContext produce warnings via push_context.""" + inner = OrderedDict({"key": "${a}"}) + yaml_util.add_context(inner, {"a": "${undefined}"}) + config = OrderedDict({"items": [inner]}) + with caplog.at_level(logging.WARNING): + substitutions.do_substitution_pass(config) + + assert "Could not resolve substitution variable 'a'" in caplog.text + assert "'undefined' is undefined" in caplog.text + + +def test_non_string_substitution_value_warning( + caplog: pytest.LogCaptureFixture, +) -> None: + """Undefined vars in non-string contexts (e.g. dict keys) produce warnings.""" + config = OrderedDict( + { + "items": {"${undefined_key}": "value"}, + } + ) + with caplog.at_level(logging.WARNING): + substitutions.do_substitution_pass(config) + + assert "'undefined_key' is undefined" in caplog.text + + +def test_lambda_substitution() -> None: + """Substitution inside a Lambda value should be expanded.""" + lam = Lambda("return ${var};") + config = OrderedDict( + { + CONF_SUBSTITUTIONS: OrderedDict({"var": "42"}), + "lambda": lam, + } + ) + substitutions.do_substitution_pass(config) + assert lam.value == "return 42;" + + +def test_lambda_no_substitution_unchanged() -> None: + """A Lambda with no variable references should not be mutated.""" + lam = Lambda("return 1;") + original_value = lam.value + config = OrderedDict( + { + CONF_SUBSTITUTIONS: OrderedDict({"var": "42"}), + "lambda": lam, + } + ) + substitutions.do_substitution_pass(config) + assert lam.value is original_value + + +def test_extend_substitution() -> None: + """Substitution inside an Extend value should be expanded.""" + ext = Extend("${component_id}") + config = OrderedDict( + { + CONF_SUBSTITUTIONS: OrderedDict({"component_id": "my_sensor"}), + "sensor": ext, + } + ) + substitutions.do_substitution_pass(config) + assert ext.value == "my_sensor" + + +def test_do_substitution_pass_substitutions_must_be_mapping_from_config() -> None: + """Non-mapping substitutions raises cv.Invalid.""" + config = OrderedDict( + { + CONF_SUBSTITUTIONS: ["not", "a", "mapping"], + "other": "value", + } + ) + + with pytest.raises( + cv.Invalid, match="Substitutions must be a key to value mapping" + ): + substitutions.do_substitution_pass(config) diff --git a/tests/unit_tests/test_yaml_util.py b/tests/unit_tests/test_yaml_util.py index adb7658bfd..35a4bc3707 100644 --- a/tests/unit_tests/test_yaml_util.py +++ b/tests/unit_tests/test_yaml_util.py @@ -25,7 +25,7 @@ def test_include_with_vars(fixture_path: Path) -> None: yaml_file = fixture_path / "yaml_util" / "includetest.yaml" actual = yaml_util.load_yaml(yaml_file) - substitutions.do_substitution_pass(actual, None) + actual = substitutions.do_substitution_pass(actual, None) assert actual["esphome"]["name"] == "original" assert actual["esphome"]["libraries"][0] == "Wire" assert actual["esp8266"]["board"] == "nodemcu" From e6a73cab8f1e090244d63d4d63766a570a45ec62 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Tue, 24 Mar 2026 09:04:53 +1000 Subject: [PATCH 118/166] [number] Add sensor platform (#15125) --- esphome/components/number/sensor/__init__.py | 25 +++++++++++++++++++ .../number/sensor/number_sensor.cpp | 16 ++++++++++++ .../components/number/sensor/number_sensor.h | 19 ++++++++++++++ tests/components/number/common.yaml | 13 ++++++++++ tests/components/number/test.esp32-idf.yaml | 2 ++ tests/components/number/test.esp8266-ard.yaml | 2 ++ 6 files changed, 77 insertions(+) create mode 100644 esphome/components/number/sensor/__init__.py create mode 100644 esphome/components/number/sensor/number_sensor.cpp create mode 100644 esphome/components/number/sensor/number_sensor.h create mode 100644 tests/components/number/common.yaml create mode 100644 tests/components/number/test.esp32-idf.yaml create mode 100644 tests/components/number/test.esp8266-ard.yaml diff --git a/esphome/components/number/sensor/__init__.py b/esphome/components/number/sensor/__init__.py new file mode 100644 index 0000000000..0d4b580d7e --- /dev/null +++ b/esphome/components/number/sensor/__init__.py @@ -0,0 +1,25 @@ +import esphome.codegen as cg +from esphome.components import sensor +import esphome.config_validation as cv +from esphome.const import CONF_SOURCE_ID + +from .. import Number, number_ns + +NumberSensor = number_ns.class_("NumberSensor", sensor.Sensor, cg.Component) + + +CONFIG_SCHEMA = ( + sensor.sensor_schema(NumberSensor) + .extend( + { + cv.Required(CONF_SOURCE_ID): cv.use_id(Number), + } + ) + .extend(cv.COMPONENT_SCHEMA) +) + + +async def to_code(config): + source = await cg.get_variable(config[CONF_SOURCE_ID]) + var = await sensor.new_sensor(config, source) + await cg.register_component(var, config) diff --git a/esphome/components/number/sensor/number_sensor.cpp b/esphome/components/number/sensor/number_sensor.cpp new file mode 100644 index 0000000000..227202622a --- /dev/null +++ b/esphome/components/number/sensor/number_sensor.cpp @@ -0,0 +1,16 @@ +#include "number_sensor.h" +#include "esphome/core/log.h" + +namespace esphome::number { + +static const char *const TAG = "number.sensor"; + +void NumberSensor::setup() { + this->source_->add_on_state_callback([this](float value) { this->publish_state(value); }); + if (this->source_->has_state()) + this->publish_state(this->source_->state); +} + +void NumberSensor::dump_config() { LOG_SENSOR("", "Number Sensor", this); } + +} // namespace esphome::number diff --git a/esphome/components/number/sensor/number_sensor.h b/esphome/components/number/sensor/number_sensor.h new file mode 100644 index 0000000000..2d6825a298 --- /dev/null +++ b/esphome/components/number/sensor/number_sensor.h @@ -0,0 +1,19 @@ +#pragma once + +#include "../number.h" +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" + +namespace esphome::number { + +class NumberSensor : public sensor::Sensor, public Component { + public: + explicit NumberSensor(Number *source) : source_(source) {} + void setup() override; + void dump_config() override; + + protected: + Number *source_; +}; + +} // namespace esphome::number diff --git a/tests/components/number/common.yaml b/tests/components/number/common.yaml new file mode 100644 index 0000000000..c17c2dd5f8 --- /dev/null +++ b/tests/components/number/common.yaml @@ -0,0 +1,13 @@ +number: + - platform: template + name: "Test Number" + id: test_number + optimistic: true + min_value: 0 + max_value: 100 + step: 1 + +sensor: + - platform: number + name: "Test Number Value" + source_id: test_number diff --git a/tests/components/number/test.esp32-idf.yaml b/tests/components/number/test.esp32-idf.yaml new file mode 100644 index 0000000000..25cb37a0b4 --- /dev/null +++ b/tests/components/number/test.esp32-idf.yaml @@ -0,0 +1,2 @@ +packages: + common: !include common.yaml diff --git a/tests/components/number/test.esp8266-ard.yaml b/tests/components/number/test.esp8266-ard.yaml new file mode 100644 index 0000000000..25cb37a0b4 --- /dev/null +++ b/tests/components/number/test.esp8266-ard.yaml @@ -0,0 +1,2 @@ +packages: + common: !include common.yaml From 0fb31726f69b304581caec41614a080774feda8a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Mar 2026 13:39:29 -1000 Subject: [PATCH 119/166] [esp32] Add sram1_as_iram option and bootloader version detection (#14874) --- esphome/components/esp32/__init__.py | 19 ++++++++ esphome/core/application.cpp | 50 ++++++++++++++++++---- esphome/core/defines.h | 1 + tests/components/esp32/test.esp32-idf.yaml | 1 + 4 files changed, 62 insertions(+), 9 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index f85f13fe73..1ecc270fd1 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -97,6 +97,7 @@ CONF_ENABLE_LWIP_ASSERT = "enable_lwip_assert" CONF_EXECUTE_FROM_PSRAM = "execute_from_psram" CONF_MINIMUM_CHIP_REVISION = "minimum_chip_revision" CONF_RELEASE = "release" +CONF_SRAM1_AS_IRAM = "sram1_as_iram" CONF_SUBTYPE = "subtype" ARDUINO_FRAMEWORK_NAME = "framework-arduinoespressif32" @@ -884,6 +885,13 @@ def final_validate(config): path=[CONF_FRAMEWORK, CONF_ADVANCED, CONF_MINIMUM_CHIP_REVISION], ) ) + if config[CONF_VARIANT] != VARIANT_ESP32 and advanced[CONF_SRAM1_AS_IRAM]: + errs.append( + cv.Invalid( + f"'{CONF_SRAM1_AS_IRAM}' is only supported on {VARIANT_ESP32}", + path=[CONF_FRAMEWORK, CONF_ADVANCED, CONF_SRAM1_AS_IRAM], + ) + ) if ( config[CONF_VARIANT] != VARIANT_ESP32P4 and config.get(CONF_ENGINEERING_SAMPLE) is not None @@ -1131,6 +1139,7 @@ FRAMEWORK_SCHEMA = cv.Schema( cv.Optional(CONF_MINIMUM_CHIP_REVISION): cv.one_of( *ESP32_CHIP_REVISIONS ), + cv.Optional(CONF_SRAM1_AS_IRAM, default=False): cv.boolean, # DHCP server is needed for WiFi AP mode. When WiFi component is used, # it will handle disabling DHCP server when AP is not configured. # Default to false (disabled) when WiFi is not used. @@ -1655,6 +1664,16 @@ async def to_code(config): for rev, flag in ESP32_CHIP_REVISIONS.items(): add_idf_sdkconfig_option(flag, rev == min_rev) cg.add_define("USE_ESP32_MIN_CHIP_REVISION_SET") + + # Use SRAM1 region as IRAM on ESP32 (original) variant + # This provides an additional 40KB of IRAM by using SRAM1 memory that was previously + # reserved for bootloader DRAM. Requires a bootloader from ESP-IDF v5.1 or later. + # WARNING: If the device has an old bootloader (pre-v5.1), the app will fail to boot. + # A USB flash will update the bootloader automatically. OTA updates do not. + # See: https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-guides/performance/ram-usage.html + if variant == VARIANT_ESP32 and conf[CONF_ADVANCED][CONF_SRAM1_AS_IRAM]: + add_idf_sdkconfig_option("CONFIG_ESP_SYSTEM_ESP32_SRAM1_REGION_AS_IRAM", True) + cg.add_define("USE_ESP32_SRAM1_AS_IRAM") add_idf_sdkconfig_option("CONFIG_PARTITION_TABLE_SINGLE_APP", False) add_idf_sdkconfig_option("CONFIG_PARTITION_TABLE_CUSTOM", True) add_idf_sdkconfig_option("CONFIG_PARTITION_TABLE_CUSTOM_FILENAME", "partitions.csv") diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index c020a8ed58..ce15aed1e2 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -9,6 +9,8 @@ #endif #ifdef USE_ESP32 #include +#include +#include #endif #ifdef USE_LWIP_FAST_SELECT #include "esphome/core/lwip_fast_select.h" @@ -167,19 +169,49 @@ void Application::process_dump_config_() { esp_chip_info(&chip_info); ESP_LOGI(TAG, "ESP32 Chip: %s rev%d.%d, %d core(s)", ESPHOME_VARIANT, chip_info.revision / 100, chip_info.revision % 100, chip_info.cores); -#if defined(USE_ESP32_VARIANT_ESP32) && !defined(USE_ESP32_MIN_CHIP_REVISION_SET) - // Suggest optimization for chips that don't need the PSRAM cache workaround - if (chip_info.revision >= 300) { -#ifdef USE_PSRAM - ESP_LOGW(TAG, "Set minimum_chip_revision: \"%d.%d\" to save ~10KB IRAM", chip_info.revision / 100, - chip_info.revision % 100); -#else - ESP_LOGW(TAG, "Set minimum_chip_revision: \"%d.%d\" to reduce binary size", chip_info.revision / 100, - chip_info.revision % 100); +#if defined(USE_ESP32_VARIANT_ESP32) && (!defined(USE_ESP32_MIN_CHIP_REVISION_SET) || !defined(USE_ESP32_SRAM1_AS_IRAM)) + static const char *const ESP32_ADVANCED_PATH = "under esp32 > framework > advanced"; #endif +#if defined(USE_ESP32_VARIANT_ESP32) && !defined(USE_ESP32_MIN_CHIP_REVISION_SET) + { + // Suggest optimization for chips that don't need the PSRAM cache workaround + if (chip_info.revision >= 300) { +#ifdef USE_PSRAM + ESP_LOGW(TAG, "Chip rev >= 3.0 detected. Set minimum_chip_revision: \"%d.%d\" %s to save ~10KB IRAM", + chip_info.revision / 100, chip_info.revision % 100, ESP32_ADVANCED_PATH); +#else + ESP_LOGW(TAG, "Chip rev >= 3.0 detected. Set minimum_chip_revision: \"%d.%d\" %s to reduce binary size", + chip_info.revision / 100, chip_info.revision % 100, ESP32_ADVANCED_PATH); +#endif + } } #endif + { + // esp_bootloader_desc_t is available in ESP-IDF >= 5.2; if readable the bootloader is modern. + // + // Design decision: We intentionally do NOT mention sram1_as_iram when the bootloader is too old. + // Enabling sram1_as_iram with an old bootloader causes a hard brick (device fails to boot, + // requires USB reflash to recover). Users don't always read warnings carefully, so we only + // suggest the option once we've confirmed the bootloader can handle it. In practice this + // means a user with an old bootloader may need to flash twice: once via USB to update the + // bootloader (they'll see the suggestion on next boot), then OTA with sram1_as_iram: true. + // Two flashes is a better outcome than a bricked device. + esp_bootloader_desc_t boot_desc; + if (esp_ota_get_bootloader_description(nullptr, &boot_desc) != ESP_OK) { +#ifdef USE_ESP32_VARIANT_ESP32 + ESP_LOGW(TAG, "Bootloader too old for OTA rollback and SRAM1 as IRAM (+40KB). " + "Flash via USB once to update the bootloader"); +#else + ESP_LOGW(TAG, "Bootloader too old for OTA rollback. Flash via USB once to update the bootloader"); #endif + } +#if defined(USE_ESP32_VARIANT_ESP32) && !defined(USE_ESP32_SRAM1_AS_IRAM) + else { + ESP_LOGW(TAG, "Bootloader supports SRAM1 as IRAM (+40KB). Set sram1_as_iram: true %s", ESP32_ADVANCED_PATH); + } +#endif + } +#endif // USE_ESP32 } this->components_[this->dump_config_at_]->call_dump_config_(); diff --git a/esphome/core/defines.h b/esphome/core/defines.h index f437e30a95..996818c2e6 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -202,6 +202,7 @@ #define USE_ESPHOME_TASK_LOG_BUFFER #define USE_OTA_ROLLBACK #define USE_ESP32_MIN_CHIP_REVISION_SET +#define USE_ESP32_SRAM1_AS_IRAM #define USE_BLUETOOTH_PROXY #define BLUETOOTH_PROXY_MAX_CONNECTIONS 3 diff --git a/tests/components/esp32/test.esp32-idf.yaml b/tests/components/esp32/test.esp32-idf.yaml index da85aa3b0f..b999f23e1c 100644 --- a/tests/components/esp32/test.esp32-idf.yaml +++ b/tests/components/esp32/test.esp32-idf.yaml @@ -19,6 +19,7 @@ esp32: disable_mbedtls_pkcs7: true disable_regi2c_in_iram: true disable_fatfs: true + sram1_as_iram: true wifi: ssid: MySSID From a0d0516b22e7ea8cd29c718ef510e9d33d3de5a7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Mar 2026 13:40:41 -1000 Subject: [PATCH 120/166] [benchmark] Add noise handshake benchmark (#15039) --- .../components/api/bench_noise_encrypt.cpp | 129 ++++++++++++++++++ 1 file changed, 129 insertions(+) diff --git a/tests/benchmarks/components/api/bench_noise_encrypt.cpp b/tests/benchmarks/components/api/bench_noise_encrypt.cpp index 223e6ada0d..9ef928192c 100644 --- a/tests/benchmarks/components/api/bench_noise_encrypt.cpp +++ b/tests/benchmarks/components/api/bench_noise_encrypt.cpp @@ -172,6 +172,135 @@ BENCHMARK(NoiseDecrypt_MediumMessage); static void NoiseDecrypt_LargeMessage(benchmark::State &state) { noise_decrypt_bench(state, 1024); } BENCHMARK(NoiseDecrypt_LargeMessage); +// --- Full Noise_NNpsk0 handshake benchmark --- +// Measures the complete handshake between initiator and responder: +// - Create handshake states for both sides +// - Set PSK and prologue +// - Exchange messages (initiator write -> responder read -> responder write -> initiator read) +// - Split to get cipher states +// This is dominated by Curve25519 DH operations (expensive on ESP8266). +// No inner iterations — each handshake is already expensive enough. + +static void NoiseHandshake_Full(benchmark::State &state) { + // Matching ESPHome's protocol: Noise_NNpsk0_25519_ChaChaPoly_SHA256 + NoiseProtocolId nid; + memset(&nid, 0, sizeof(nid)); + nid.pattern_id = NOISE_PATTERN_NN; + nid.cipher_id = NOISE_CIPHER_CHACHAPOLY; + nid.dh_id = NOISE_DH_CURVE25519; + nid.prefix_id = NOISE_PREFIX_STANDARD; + nid.hybrid_id = NOISE_DH_NONE; + nid.hash_id = NOISE_HASH_SHA256; + nid.modifier_ids[0] = NOISE_MODIFIER_PSK0; + + // Dummy PSK (32 bytes) and prologue matching production setup + static constexpr uint8_t PSK[32] = {0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, + 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, + 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB}; + static constexpr uint8_t PROLOGUE[] = "NoESPHome"; + + // Message buffer for handshake exchange (max handshake message ~96 bytes) + uint8_t msg_buf[128]; + + for (auto _ : state) { + NoiseHandshakeState *initiator = nullptr; + NoiseHandshakeState *responder = nullptr; + NoiseCipherState *init_send = nullptr, *init_recv = nullptr; + NoiseCipherState *resp_send = nullptr, *resp_recv = nullptr; + int err; + + // Create both handshake states + err = noise_handshakestate_new_by_id(&initiator, &nid, NOISE_ROLE_INITIATOR); + if (err != NOISE_ERROR_NONE) { + state.SkipWithError("Failed to create initiator"); + return; + } + err = noise_handshakestate_new_by_id(&responder, &nid, NOISE_ROLE_RESPONDER); + if (err != NOISE_ERROR_NONE) { + state.SkipWithError("Failed to create responder"); + noise_handshakestate_free(initiator); + return; + } + + // Set PSK and prologue on both sides + noise_handshakestate_set_pre_shared_key(initiator, PSK, sizeof(PSK)); + noise_handshakestate_set_pre_shared_key(responder, PSK, sizeof(PSK)); + noise_handshakestate_set_prologue(initiator, PROLOGUE, sizeof(PROLOGUE) - 1); + noise_handshakestate_set_prologue(responder, PROLOGUE, sizeof(PROLOGUE) - 1); + + noise_handshakestate_start(initiator); + noise_handshakestate_start(responder); + + // Message 1: Initiator -> Responder + NoiseBuffer write_buf, read_buf; + noise_buffer_set_output(write_buf, msg_buf, sizeof(msg_buf)); + err = noise_handshakestate_write_message(initiator, &write_buf, nullptr); + if (err != NOISE_ERROR_NONE) { + state.SkipWithError("Initiator write_message failed"); + noise_handshakestate_free(initiator); + noise_handshakestate_free(responder); + return; + } + + noise_buffer_set_input(read_buf, msg_buf, write_buf.size); + err = noise_handshakestate_read_message(responder, &read_buf, nullptr); + if (err != NOISE_ERROR_NONE) { + state.SkipWithError("Responder read_message failed"); + noise_handshakestate_free(initiator); + noise_handshakestate_free(responder); + return; + } + + // Message 2: Responder -> Initiator + noise_buffer_set_output(write_buf, msg_buf, sizeof(msg_buf)); + err = noise_handshakestate_write_message(responder, &write_buf, nullptr); + if (err != NOISE_ERROR_NONE) { + state.SkipWithError("Responder write_message failed"); + noise_handshakestate_free(initiator); + noise_handshakestate_free(responder); + return; + } + + noise_buffer_set_input(read_buf, msg_buf, write_buf.size); + err = noise_handshakestate_read_message(initiator, &read_buf, nullptr); + if (err != NOISE_ERROR_NONE) { + state.SkipWithError("Initiator read_message failed"); + noise_handshakestate_free(initiator); + noise_handshakestate_free(responder); + return; + } + + // Split to get cipher states + err = noise_handshakestate_split(initiator, &init_send, &init_recv); + if (err != NOISE_ERROR_NONE) { + state.SkipWithError("Initiator split failed"); + noise_handshakestate_free(initiator); + noise_handshakestate_free(responder); + return; + } + err = noise_handshakestate_split(responder, &resp_send, &resp_recv); + if (err != NOISE_ERROR_NONE) { + state.SkipWithError("Responder split failed"); + noise_handshakestate_free(initiator); + noise_handshakestate_free(responder); + noise_cipherstate_free(init_send); + noise_cipherstate_free(init_recv); + return; + } + + benchmark::DoNotOptimize(init_send); + + // Cleanup + noise_handshakestate_free(initiator); + noise_handshakestate_free(responder); + noise_cipherstate_free(init_send); + noise_cipherstate_free(init_recv); + noise_cipherstate_free(resp_send); + noise_cipherstate_free(resp_recv); + } +} +BENCHMARK(NoiseHandshake_Full); + } // namespace esphome::api::benchmarks #endif // USE_API_NOISE From 382de7ca906b2b584b1c6188105a50523182f2f1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Mar 2026 13:40:53 -1000 Subject: [PATCH 121/166] [api] Store dump strings in PROGMEM to save RAM on ESP8266 (#14982) --- esphome/components/api/api_pb2.h | 274 +-- esphome/components/api/api_pb2_dump.cpp | 2425 ++++++++++---------- esphome/components/api/api_pb2_service.cpp | 4 +- esphome/components/api/api_pb2_service.h | 2 +- esphome/components/api/proto.h | 20 +- script/api_protobuf/api_protobuf.py | 96 +- 6 files changed, 1443 insertions(+), 1378 deletions(-) diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 86289a28d6..16586e6e9a 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -388,7 +388,7 @@ class HelloRequest final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 1; static constexpr uint8_t ESTIMATED_SIZE = 17; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "hello_request"; } + const LogString *message_name() const override { return LOG_STR("hello_request"); } #endif StringRef client_info{}; uint32_t api_version_major{0}; @@ -406,7 +406,7 @@ class HelloResponse final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 2; static constexpr uint8_t ESTIMATED_SIZE = 26; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "hello_response"; } + const LogString *message_name() const override { return LOG_STR("hello_response"); } #endif uint32_t api_version_major{0}; uint32_t api_version_minor{0}; @@ -425,7 +425,7 @@ class DisconnectRequest final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 5; static constexpr uint8_t ESTIMATED_SIZE = 0; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "disconnect_request"; } + const LogString *message_name() const override { return LOG_STR("disconnect_request"); } #endif #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -438,7 +438,7 @@ class DisconnectResponse final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 6; static constexpr uint8_t ESTIMATED_SIZE = 0; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "disconnect_response"; } + const LogString *message_name() const override { return LOG_STR("disconnect_response"); } #endif #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -451,7 +451,7 @@ class PingRequest final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 7; static constexpr uint8_t ESTIMATED_SIZE = 0; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "ping_request"; } + const LogString *message_name() const override { return LOG_STR("ping_request"); } #endif #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -464,7 +464,7 @@ class PingResponse final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 8; static constexpr uint8_t ESTIMATED_SIZE = 0; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "ping_response"; } + const LogString *message_name() const override { return LOG_STR("ping_response"); } #endif #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -520,7 +520,7 @@ class DeviceInfoResponse final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 10; static constexpr uint16_t ESTIMATED_SIZE = 309; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "device_info_response"; } + const LogString *message_name() const override { return LOG_STR("device_info_response"); } #endif StringRef name{}; StringRef mac_address{}; @@ -587,7 +587,7 @@ class ListEntitiesDoneResponse final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 19; static constexpr uint8_t ESTIMATED_SIZE = 0; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_done_response"; } + const LogString *message_name() const override { return LOG_STR("list_entities_done_response"); } #endif #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -601,7 +601,7 @@ class ListEntitiesBinarySensorResponse final : public InfoResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 12; static constexpr uint8_t ESTIMATED_SIZE = 51; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_binary_sensor_response"; } + const LogString *message_name() const override { return LOG_STR("list_entities_binary_sensor_response"); } #endif StringRef device_class{}; bool is_status_binary_sensor{false}; @@ -618,7 +618,7 @@ class BinarySensorStateResponse final : public StateResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 21; static constexpr uint8_t ESTIMATED_SIZE = 13; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "binary_sensor_state_response"; } + const LogString *message_name() const override { return LOG_STR("binary_sensor_state_response"); } #endif bool state{false}; bool missing_state{false}; @@ -637,7 +637,7 @@ class ListEntitiesCoverResponse final : public InfoResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 13; static constexpr uint8_t ESTIMATED_SIZE = 57; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_cover_response"; } + const LogString *message_name() const override { return LOG_STR("list_entities_cover_response"); } #endif bool assumed_state{false}; bool supports_position{false}; @@ -657,7 +657,7 @@ class CoverStateResponse final : public StateResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 22; static constexpr uint8_t ESTIMATED_SIZE = 21; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "cover_state_response"; } + const LogString *message_name() const override { return LOG_STR("cover_state_response"); } #endif float position{0.0f}; float tilt{0.0f}; @@ -675,7 +675,7 @@ class CoverCommandRequest final : public CommandProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 30; static constexpr uint8_t ESTIMATED_SIZE = 25; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "cover_command_request"; } + const LogString *message_name() const override { return LOG_STR("cover_command_request"); } #endif bool has_position{false}; float position{0.0f}; @@ -697,7 +697,7 @@ class ListEntitiesFanResponse final : public InfoResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 14; static constexpr uint8_t ESTIMATED_SIZE = 68; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_fan_response"; } + const LogString *message_name() const override { return LOG_STR("list_entities_fan_response"); } #endif bool supports_oscillation{false}; bool supports_speed{false}; @@ -717,7 +717,7 @@ class FanStateResponse final : public StateResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 23; static constexpr uint8_t ESTIMATED_SIZE = 28; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "fan_state_response"; } + const LogString *message_name() const override { return LOG_STR("fan_state_response"); } #endif bool state{false}; bool oscillating{false}; @@ -737,7 +737,7 @@ class FanCommandRequest final : public CommandProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 31; static constexpr uint8_t ESTIMATED_SIZE = 38; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "fan_command_request"; } + const LogString *message_name() const override { return LOG_STR("fan_command_request"); } #endif bool has_state{false}; bool state{false}; @@ -765,7 +765,7 @@ class ListEntitiesLightResponse final : public InfoResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 15; static constexpr uint8_t ESTIMATED_SIZE = 73; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_light_response"; } + const LogString *message_name() const override { return LOG_STR("list_entities_light_response"); } #endif const light::ColorModeMask *supported_color_modes{}; float min_mireds{0.0f}; @@ -784,7 +784,7 @@ class LightStateResponse final : public StateResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 24; static constexpr uint8_t ESTIMATED_SIZE = 67; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "light_state_response"; } + const LogString *message_name() const override { return LOG_STR("light_state_response"); } #endif bool state{false}; float brightness{0.0f}; @@ -811,7 +811,7 @@ class LightCommandRequest final : public CommandProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 32; static constexpr uint8_t ESTIMATED_SIZE = 112; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "light_command_request"; } + const LogString *message_name() const override { return LOG_STR("light_command_request"); } #endif bool has_state{false}; bool state{false}; @@ -855,7 +855,7 @@ class ListEntitiesSensorResponse final : public InfoResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 16; static constexpr uint8_t ESTIMATED_SIZE = 66; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_sensor_response"; } + const LogString *message_name() const override { return LOG_STR("list_entities_sensor_response"); } #endif StringRef unit_of_measurement{}; int32_t accuracy_decimals{0}; @@ -875,7 +875,7 @@ class SensorStateResponse final : public StateResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 25; static constexpr uint8_t ESTIMATED_SIZE = 16; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "sensor_state_response"; } + const LogString *message_name() const override { return LOG_STR("sensor_state_response"); } #endif float state{0.0f}; bool missing_state{false}; @@ -894,7 +894,7 @@ class ListEntitiesSwitchResponse final : public InfoResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 17; static constexpr uint8_t ESTIMATED_SIZE = 51; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_switch_response"; } + const LogString *message_name() const override { return LOG_STR("list_entities_switch_response"); } #endif bool assumed_state{false}; StringRef device_class{}; @@ -911,7 +911,7 @@ class SwitchStateResponse final : public StateResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 26; static constexpr uint8_t ESTIMATED_SIZE = 11; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "switch_state_response"; } + const LogString *message_name() const override { return LOG_STR("switch_state_response"); } #endif bool state{false}; void encode(ProtoWriteBuffer &buffer) const; @@ -927,7 +927,7 @@ class SwitchCommandRequest final : public CommandProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 33; static constexpr uint8_t ESTIMATED_SIZE = 11; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "switch_command_request"; } + const LogString *message_name() const override { return LOG_STR("switch_command_request"); } #endif bool state{false}; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -945,7 +945,7 @@ class ListEntitiesTextSensorResponse final : public InfoResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 18; static constexpr uint8_t ESTIMATED_SIZE = 49; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_text_sensor_response"; } + const LogString *message_name() const override { return LOG_STR("list_entities_text_sensor_response"); } #endif StringRef device_class{}; void encode(ProtoWriteBuffer &buffer) const; @@ -961,7 +961,7 @@ class TextSensorStateResponse final : public StateResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 27; static constexpr uint8_t ESTIMATED_SIZE = 20; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "text_sensor_state_response"; } + const LogString *message_name() const override { return LOG_STR("text_sensor_state_response"); } #endif StringRef state{}; bool missing_state{false}; @@ -979,7 +979,7 @@ class SubscribeLogsRequest final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 28; static constexpr uint8_t ESTIMATED_SIZE = 4; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "subscribe_logs_request"; } + const LogString *message_name() const override { return LOG_STR("subscribe_logs_request"); } #endif enums::LogLevel level{}; bool dump_config{false}; @@ -995,7 +995,7 @@ class SubscribeLogsResponse final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 29; static constexpr uint8_t ESTIMATED_SIZE = 21; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "subscribe_logs_response"; } + const LogString *message_name() const override { return LOG_STR("subscribe_logs_response"); } #endif enums::LogLevel level{}; const uint8_t *message_ptr_{nullptr}; @@ -1018,7 +1018,7 @@ class NoiseEncryptionSetKeyRequest final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 124; static constexpr uint8_t ESTIMATED_SIZE = 19; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "noise_encryption_set_key_request"; } + const LogString *message_name() const override { return LOG_STR("noise_encryption_set_key_request"); } #endif const uint8_t *key{nullptr}; uint16_t key_len{0}; @@ -1034,7 +1034,7 @@ class NoiseEncryptionSetKeyResponse final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 125; static constexpr uint8_t ESTIMATED_SIZE = 2; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "noise_encryption_set_key_response"; } + const LogString *message_name() const override { return LOG_STR("noise_encryption_set_key_response"); } #endif bool success{false}; void encode(ProtoWriteBuffer &buffer) const; @@ -1064,7 +1064,7 @@ class HomeassistantActionRequest final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 35; static constexpr uint8_t ESTIMATED_SIZE = 128; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "homeassistant_action_request"; } + const LogString *message_name() const override { return LOG_STR("homeassistant_action_request"); } #endif StringRef service{}; FixedVector data{}; @@ -1095,7 +1095,7 @@ class HomeassistantActionResponse final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 130; static constexpr uint8_t ESTIMATED_SIZE = 34; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "homeassistant_action_response"; } + const LogString *message_name() const override { return LOG_STR("homeassistant_action_response"); } #endif uint32_t call_id{0}; bool success{false}; @@ -1119,7 +1119,7 @@ class SubscribeHomeAssistantStateResponse final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 39; static constexpr uint8_t ESTIMATED_SIZE = 20; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "subscribe_home_assistant_state_response"; } + const LogString *message_name() const override { return LOG_STR("subscribe_home_assistant_state_response"); } #endif StringRef entity_id{}; StringRef attribute{}; @@ -1137,7 +1137,7 @@ class HomeAssistantStateResponse final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 40; static constexpr uint8_t ESTIMATED_SIZE = 27; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "home_assistant_state_response"; } + const LogString *message_name() const override { return LOG_STR("home_assistant_state_response"); } #endif StringRef entity_id{}; StringRef state{}; @@ -1155,7 +1155,7 @@ class GetTimeRequest final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 36; static constexpr uint8_t ESTIMATED_SIZE = 0; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "get_time_request"; } + const LogString *message_name() const override { return LOG_STR("get_time_request"); } #endif #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1197,7 +1197,7 @@ class GetTimeResponse final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 37; static constexpr uint8_t ESTIMATED_SIZE = 31; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "get_time_response"; } + const LogString *message_name() const override { return LOG_STR("get_time_response"); } #endif uint32_t epoch_seconds{0}; StringRef timezone{}; @@ -1228,7 +1228,7 @@ class ListEntitiesServicesResponse final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 41; static constexpr uint8_t ESTIMATED_SIZE = 50; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_services_response"; } + const LogString *message_name() const override { return LOG_STR("list_entities_services_response"); } #endif StringRef name{}; uint32_t key{0}; @@ -1268,7 +1268,7 @@ class ExecuteServiceRequest final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 42; static constexpr uint8_t ESTIMATED_SIZE = 45; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "execute_service_request"; } + const LogString *message_name() const override { return LOG_STR("execute_service_request"); } #endif uint32_t key{0}; FixedVector args{}; @@ -1295,7 +1295,7 @@ class ExecuteServiceResponse final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 131; static constexpr uint8_t ESTIMATED_SIZE = 34; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "execute_service_response"; } + const LogString *message_name() const override { return LOG_STR("execute_service_response"); } #endif uint32_t call_id{0}; bool success{false}; @@ -1319,7 +1319,7 @@ class ListEntitiesCameraResponse final : public InfoResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 43; static constexpr uint8_t ESTIMATED_SIZE = 40; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_camera_response"; } + const LogString *message_name() const override { return LOG_STR("list_entities_camera_response"); } #endif void encode(ProtoWriteBuffer &buffer) const; uint32_t calculate_size() const; @@ -1334,7 +1334,7 @@ class CameraImageResponse final : public StateResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 44; static constexpr uint8_t ESTIMATED_SIZE = 30; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "camera_image_response"; } + const LogString *message_name() const override { return LOG_STR("camera_image_response"); } #endif const uint8_t *data_ptr_{nullptr}; size_t data_len_{0}; @@ -1356,7 +1356,7 @@ class CameraImageRequest final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 45; static constexpr uint8_t ESTIMATED_SIZE = 4; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "camera_image_request"; } + const LogString *message_name() const override { return LOG_STR("camera_image_request"); } #endif bool single{false}; bool stream{false}; @@ -1374,7 +1374,7 @@ class ListEntitiesClimateResponse final : public InfoResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 46; static constexpr uint8_t ESTIMATED_SIZE = 150; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_climate_response"; } + const LogString *message_name() const override { return LOG_STR("list_entities_climate_response"); } #endif bool supports_current_temperature{false}; bool supports_two_point_target_temperature{false}; @@ -1407,7 +1407,7 @@ class ClimateStateResponse final : public StateResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 47; static constexpr uint8_t ESTIMATED_SIZE = 68; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "climate_state_response"; } + const LogString *message_name() const override { return LOG_STR("climate_state_response"); } #endif enums::ClimateMode mode{}; float current_temperature{0.0f}; @@ -1435,7 +1435,7 @@ class ClimateCommandRequest final : public CommandProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 48; static constexpr uint8_t ESTIMATED_SIZE = 84; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "climate_command_request"; } + const LogString *message_name() const override { return LOG_STR("climate_command_request"); } #endif bool has_mode{false}; enums::ClimateMode mode{}; @@ -1473,7 +1473,7 @@ class ListEntitiesWaterHeaterResponse final : public InfoResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 132; static constexpr uint8_t ESTIMATED_SIZE = 63; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_water_heater_response"; } + const LogString *message_name() const override { return LOG_STR("list_entities_water_heater_response"); } #endif float min_temperature{0.0f}; float max_temperature{0.0f}; @@ -1493,7 +1493,7 @@ class WaterHeaterStateResponse final : public StateResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 133; static constexpr uint8_t ESTIMATED_SIZE = 35; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "water_heater_state_response"; } + const LogString *message_name() const override { return LOG_STR("water_heater_state_response"); } #endif float current_temperature{0.0f}; float target_temperature{0.0f}; @@ -1514,7 +1514,7 @@ class WaterHeaterCommandRequest final : public CommandProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 134; static constexpr uint8_t ESTIMATED_SIZE = 34; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "water_heater_command_request"; } + const LogString *message_name() const override { return LOG_STR("water_heater_command_request"); } #endif uint32_t has_fields{0}; enums::WaterHeaterMode mode{}; @@ -1537,7 +1537,7 @@ class ListEntitiesNumberResponse final : public InfoResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 49; static constexpr uint8_t ESTIMATED_SIZE = 75; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_number_response"; } + const LogString *message_name() const override { return LOG_STR("list_entities_number_response"); } #endif float min_value{0.0f}; float max_value{0.0f}; @@ -1558,7 +1558,7 @@ class NumberStateResponse final : public StateResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 50; static constexpr uint8_t ESTIMATED_SIZE = 16; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "number_state_response"; } + const LogString *message_name() const override { return LOG_STR("number_state_response"); } #endif float state{0.0f}; bool missing_state{false}; @@ -1575,7 +1575,7 @@ class NumberCommandRequest final : public CommandProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 51; static constexpr uint8_t ESTIMATED_SIZE = 14; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "number_command_request"; } + const LogString *message_name() const override { return LOG_STR("number_command_request"); } #endif float state{0.0f}; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -1593,7 +1593,7 @@ class ListEntitiesSelectResponse final : public InfoResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 52; static constexpr uint8_t ESTIMATED_SIZE = 58; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_select_response"; } + const LogString *message_name() const override { return LOG_STR("list_entities_select_response"); } #endif const FixedVector *options{}; void encode(ProtoWriteBuffer &buffer) const; @@ -1609,7 +1609,7 @@ class SelectStateResponse final : public StateResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 53; static constexpr uint8_t ESTIMATED_SIZE = 20; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "select_state_response"; } + const LogString *message_name() const override { return LOG_STR("select_state_response"); } #endif StringRef state{}; bool missing_state{false}; @@ -1626,7 +1626,7 @@ class SelectCommandRequest final : public CommandProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 54; static constexpr uint8_t ESTIMATED_SIZE = 18; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "select_command_request"; } + const LogString *message_name() const override { return LOG_STR("select_command_request"); } #endif StringRef state{}; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -1645,7 +1645,7 @@ class ListEntitiesSirenResponse final : public InfoResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 55; static constexpr uint8_t ESTIMATED_SIZE = 62; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_siren_response"; } + const LogString *message_name() const override { return LOG_STR("list_entities_siren_response"); } #endif const FixedVector *tones{}; bool supports_duration{false}; @@ -1663,7 +1663,7 @@ class SirenStateResponse final : public StateResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 56; static constexpr uint8_t ESTIMATED_SIZE = 11; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "siren_state_response"; } + const LogString *message_name() const override { return LOG_STR("siren_state_response"); } #endif bool state{false}; void encode(ProtoWriteBuffer &buffer) const; @@ -1679,7 +1679,7 @@ class SirenCommandRequest final : public CommandProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 57; static constexpr uint8_t ESTIMATED_SIZE = 37; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "siren_command_request"; } + const LogString *message_name() const override { return LOG_STR("siren_command_request"); } #endif bool has_state{false}; bool state{false}; @@ -1705,7 +1705,7 @@ class ListEntitiesLockResponse final : public InfoResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 58; static constexpr uint8_t ESTIMATED_SIZE = 55; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_lock_response"; } + const LogString *message_name() const override { return LOG_STR("list_entities_lock_response"); } #endif bool assumed_state{false}; bool supports_open{false}; @@ -1724,7 +1724,7 @@ class LockStateResponse final : public StateResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 59; static constexpr uint8_t ESTIMATED_SIZE = 11; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "lock_state_response"; } + const LogString *message_name() const override { return LOG_STR("lock_state_response"); } #endif enums::LockState state{}; void encode(ProtoWriteBuffer &buffer) const; @@ -1740,7 +1740,7 @@ class LockCommandRequest final : public CommandProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 60; static constexpr uint8_t ESTIMATED_SIZE = 22; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "lock_command_request"; } + const LogString *message_name() const override { return LOG_STR("lock_command_request"); } #endif enums::LockCommand command{}; bool has_code{false}; @@ -1761,7 +1761,7 @@ class ListEntitiesButtonResponse final : public InfoResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 61; static constexpr uint8_t ESTIMATED_SIZE = 49; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_button_response"; } + const LogString *message_name() const override { return LOG_STR("list_entities_button_response"); } #endif StringRef device_class{}; void encode(ProtoWriteBuffer &buffer) const; @@ -1777,7 +1777,7 @@ class ButtonCommandRequest final : public CommandProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 62; static constexpr uint8_t ESTIMATED_SIZE = 9; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "button_command_request"; } + const LogString *message_name() const override { return LOG_STR("button_command_request"); } #endif #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1809,7 +1809,7 @@ class ListEntitiesMediaPlayerResponse final : public InfoResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 63; static constexpr uint8_t ESTIMATED_SIZE = 80; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_media_player_response"; } + const LogString *message_name() const override { return LOG_STR("list_entities_media_player_response"); } #endif bool supports_pause{false}; std::vector supported_formats{}; @@ -1827,7 +1827,7 @@ class MediaPlayerStateResponse final : public StateResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 64; static constexpr uint8_t ESTIMATED_SIZE = 18; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "media_player_state_response"; } + const LogString *message_name() const override { return LOG_STR("media_player_state_response"); } #endif enums::MediaPlayerState state{}; float volume{0.0f}; @@ -1845,7 +1845,7 @@ class MediaPlayerCommandRequest final : public CommandProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 65; static constexpr uint8_t ESTIMATED_SIZE = 35; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "media_player_command_request"; } + const LogString *message_name() const override { return LOG_STR("media_player_command_request"); } #endif bool has_command{false}; enums::MediaPlayerCommand command{}; @@ -1871,7 +1871,7 @@ class SubscribeBluetoothLEAdvertisementsRequest final : public ProtoDecodableMes static constexpr uint8_t MESSAGE_TYPE = 66; static constexpr uint8_t ESTIMATED_SIZE = 4; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "subscribe_bluetooth_le_advertisements_request"; } + const LogString *message_name() const override { return LOG_STR("subscribe_bluetooth_le_advertisements_request"); } #endif uint32_t flags{0}; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -1901,7 +1901,7 @@ class BluetoothLERawAdvertisementsResponse final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 93; static constexpr uint8_t ESTIMATED_SIZE = 136; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "bluetooth_le_raw_advertisements_response"; } + const LogString *message_name() const override { return LOG_STR("bluetooth_le_raw_advertisements_response"); } #endif std::array advertisements{}; uint16_t advertisements_len{0}; @@ -1918,7 +1918,7 @@ class BluetoothDeviceRequest final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 68; static constexpr uint8_t ESTIMATED_SIZE = 12; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "bluetooth_device_request"; } + const LogString *message_name() const override { return LOG_STR("bluetooth_device_request"); } #endif uint64_t address{0}; enums::BluetoothDeviceRequestType request_type{}; @@ -1936,7 +1936,7 @@ class BluetoothDeviceConnectionResponse final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 69; static constexpr uint8_t ESTIMATED_SIZE = 14; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "bluetooth_device_connection_response"; } + const LogString *message_name() const override { return LOG_STR("bluetooth_device_connection_response"); } #endif uint64_t address{0}; bool connected{false}; @@ -1955,7 +1955,7 @@ class BluetoothGATTGetServicesRequest final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 70; static constexpr uint8_t ESTIMATED_SIZE = 4; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "bluetooth_gatt_get_services_request"; } + const LogString *message_name() const override { return LOG_STR("bluetooth_gatt_get_services_request"); } #endif uint64_t address{0}; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -2012,7 +2012,7 @@ class BluetoothGATTGetServicesResponse final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 71; static constexpr uint8_t ESTIMATED_SIZE = 38; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "bluetooth_gatt_get_services_response"; } + const LogString *message_name() const override { return LOG_STR("bluetooth_gatt_get_services_response"); } #endif uint64_t address{0}; std::vector services{}; @@ -2029,7 +2029,7 @@ class BluetoothGATTGetServicesDoneResponse final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 72; static constexpr uint8_t ESTIMATED_SIZE = 4; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "bluetooth_gatt_get_services_done_response"; } + const LogString *message_name() const override { return LOG_STR("bluetooth_gatt_get_services_done_response"); } #endif uint64_t address{0}; void encode(ProtoWriteBuffer &buffer) const; @@ -2045,7 +2045,7 @@ class BluetoothGATTReadRequest final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 73; static constexpr uint8_t ESTIMATED_SIZE = 8; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "bluetooth_gatt_read_request"; } + const LogString *message_name() const override { return LOG_STR("bluetooth_gatt_read_request"); } #endif uint64_t address{0}; uint32_t handle{0}; @@ -2061,7 +2061,7 @@ class BluetoothGATTReadResponse final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 74; static constexpr uint8_t ESTIMATED_SIZE = 27; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "bluetooth_gatt_read_response"; } + const LogString *message_name() const override { return LOG_STR("bluetooth_gatt_read_response"); } #endif uint64_t address{0}; uint32_t handle{0}; @@ -2084,7 +2084,7 @@ class BluetoothGATTWriteRequest final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 75; static constexpr uint8_t ESTIMATED_SIZE = 29; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "bluetooth_gatt_write_request"; } + const LogString *message_name() const override { return LOG_STR("bluetooth_gatt_write_request"); } #endif uint64_t address{0}; uint32_t handle{0}; @@ -2104,7 +2104,7 @@ class BluetoothGATTReadDescriptorRequest final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 76; static constexpr uint8_t ESTIMATED_SIZE = 8; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "bluetooth_gatt_read_descriptor_request"; } + const LogString *message_name() const override { return LOG_STR("bluetooth_gatt_read_descriptor_request"); } #endif uint64_t address{0}; uint32_t handle{0}; @@ -2120,7 +2120,7 @@ class BluetoothGATTWriteDescriptorRequest final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 77; static constexpr uint8_t ESTIMATED_SIZE = 27; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "bluetooth_gatt_write_descriptor_request"; } + const LogString *message_name() const override { return LOG_STR("bluetooth_gatt_write_descriptor_request"); } #endif uint64_t address{0}; uint32_t handle{0}; @@ -2139,7 +2139,7 @@ class BluetoothGATTNotifyRequest final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 78; static constexpr uint8_t ESTIMATED_SIZE = 10; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "bluetooth_gatt_notify_request"; } + const LogString *message_name() const override { return LOG_STR("bluetooth_gatt_notify_request"); } #endif uint64_t address{0}; uint32_t handle{0}; @@ -2156,7 +2156,7 @@ class BluetoothGATTNotifyDataResponse final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 79; static constexpr uint8_t ESTIMATED_SIZE = 27; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "bluetooth_gatt_notify_data_response"; } + const LogString *message_name() const override { return LOG_STR("bluetooth_gatt_notify_data_response"); } #endif uint64_t address{0}; uint32_t handle{0}; @@ -2179,7 +2179,7 @@ class BluetoothConnectionsFreeResponse final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 81; static constexpr uint8_t ESTIMATED_SIZE = 20; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "bluetooth_connections_free_response"; } + const LogString *message_name() const override { return LOG_STR("bluetooth_connections_free_response"); } #endif uint32_t free{0}; uint32_t limit{0}; @@ -2197,7 +2197,7 @@ class BluetoothGATTErrorResponse final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 82; static constexpr uint8_t ESTIMATED_SIZE = 12; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "bluetooth_gatt_error_response"; } + const LogString *message_name() const override { return LOG_STR("bluetooth_gatt_error_response"); } #endif uint64_t address{0}; uint32_t handle{0}; @@ -2215,7 +2215,7 @@ class BluetoothGATTWriteResponse final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 83; static constexpr uint8_t ESTIMATED_SIZE = 8; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "bluetooth_gatt_write_response"; } + const LogString *message_name() const override { return LOG_STR("bluetooth_gatt_write_response"); } #endif uint64_t address{0}; uint32_t handle{0}; @@ -2232,7 +2232,7 @@ class BluetoothGATTNotifyResponse final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 84; static constexpr uint8_t ESTIMATED_SIZE = 8; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "bluetooth_gatt_notify_response"; } + const LogString *message_name() const override { return LOG_STR("bluetooth_gatt_notify_response"); } #endif uint64_t address{0}; uint32_t handle{0}; @@ -2249,7 +2249,7 @@ class BluetoothDevicePairingResponse final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 85; static constexpr uint8_t ESTIMATED_SIZE = 10; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "bluetooth_device_pairing_response"; } + const LogString *message_name() const override { return LOG_STR("bluetooth_device_pairing_response"); } #endif uint64_t address{0}; bool paired{false}; @@ -2267,7 +2267,7 @@ class BluetoothDeviceUnpairingResponse final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 86; static constexpr uint8_t ESTIMATED_SIZE = 10; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "bluetooth_device_unpairing_response"; } + const LogString *message_name() const override { return LOG_STR("bluetooth_device_unpairing_response"); } #endif uint64_t address{0}; bool success{false}; @@ -2285,7 +2285,7 @@ class BluetoothDeviceClearCacheResponse final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 88; static constexpr uint8_t ESTIMATED_SIZE = 10; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "bluetooth_device_clear_cache_response"; } + const LogString *message_name() const override { return LOG_STR("bluetooth_device_clear_cache_response"); } #endif uint64_t address{0}; bool success{false}; @@ -2303,7 +2303,7 @@ class BluetoothScannerStateResponse final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 126; static constexpr uint8_t ESTIMATED_SIZE = 6; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "bluetooth_scanner_state_response"; } + const LogString *message_name() const override { return LOG_STR("bluetooth_scanner_state_response"); } #endif enums::BluetoothScannerState state{}; enums::BluetoothScannerMode mode{}; @@ -2321,7 +2321,7 @@ class BluetoothScannerSetModeRequest final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 127; static constexpr uint8_t ESTIMATED_SIZE = 2; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "bluetooth_scanner_set_mode_request"; } + const LogString *message_name() const override { return LOG_STR("bluetooth_scanner_set_mode_request"); } #endif enums::BluetoothScannerMode mode{}; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -2338,7 +2338,7 @@ class SubscribeVoiceAssistantRequest final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 89; static constexpr uint8_t ESTIMATED_SIZE = 6; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "subscribe_voice_assistant_request"; } + const LogString *message_name() const override { return LOG_STR("subscribe_voice_assistant_request"); } #endif bool subscribe{false}; uint32_t flags{0}; @@ -2367,7 +2367,7 @@ class VoiceAssistantRequest final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 90; static constexpr uint8_t ESTIMATED_SIZE = 41; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "voice_assistant_request"; } + const LogString *message_name() const override { return LOG_STR("voice_assistant_request"); } #endif bool start{false}; StringRef conversation_id{}; @@ -2387,7 +2387,7 @@ class VoiceAssistantResponse final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 91; static constexpr uint8_t ESTIMATED_SIZE = 6; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "voice_assistant_response"; } + const LogString *message_name() const override { return LOG_STR("voice_assistant_response"); } #endif uint32_t port{0}; bool error{false}; @@ -2414,7 +2414,7 @@ class VoiceAssistantEventResponse final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 92; static constexpr uint8_t ESTIMATED_SIZE = 36; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "voice_assistant_event_response"; } + const LogString *message_name() const override { return LOG_STR("voice_assistant_event_response"); } #endif enums::VoiceAssistantEvent event_type{}; std::vector data{}; @@ -2431,7 +2431,7 @@ class VoiceAssistantAudio final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 106; static constexpr uint8_t ESTIMATED_SIZE = 21; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "voice_assistant_audio"; } + const LogString *message_name() const override { return LOG_STR("voice_assistant_audio"); } #endif const uint8_t *data{nullptr}; uint16_t data_len{0}; @@ -2451,7 +2451,7 @@ class VoiceAssistantTimerEventResponse final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 115; static constexpr uint8_t ESTIMATED_SIZE = 30; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "voice_assistant_timer_event_response"; } + const LogString *message_name() const override { return LOG_STR("voice_assistant_timer_event_response"); } #endif enums::VoiceAssistantTimerEvent event_type{}; StringRef timer_id{}; @@ -2472,7 +2472,7 @@ class VoiceAssistantAnnounceRequest final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 119; static constexpr uint8_t ESTIMATED_SIZE = 29; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "voice_assistant_announce_request"; } + const LogString *message_name() const override { return LOG_STR("voice_assistant_announce_request"); } #endif StringRef media_id{}; StringRef text{}; @@ -2491,7 +2491,7 @@ class VoiceAssistantAnnounceFinished final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 120; static constexpr uint8_t ESTIMATED_SIZE = 2; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "voice_assistant_announce_finished"; } + const LogString *message_name() const override { return LOG_STR("voice_assistant_announce_finished"); } #endif bool success{false}; void encode(ProtoWriteBuffer &buffer) const; @@ -2537,7 +2537,7 @@ class VoiceAssistantConfigurationRequest final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 121; static constexpr uint8_t ESTIMATED_SIZE = 34; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "voice_assistant_configuration_request"; } + const LogString *message_name() const override { return LOG_STR("voice_assistant_configuration_request"); } #endif std::vector external_wake_words{}; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -2552,7 +2552,7 @@ class VoiceAssistantConfigurationResponse final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 122; static constexpr uint8_t ESTIMATED_SIZE = 56; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "voice_assistant_configuration_response"; } + const LogString *message_name() const override { return LOG_STR("voice_assistant_configuration_response"); } #endif std::vector available_wake_words{}; const std::vector *active_wake_words{}; @@ -2570,7 +2570,7 @@ class VoiceAssistantSetConfiguration final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 123; static constexpr uint8_t ESTIMATED_SIZE = 18; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "voice_assistant_set_configuration"; } + const LogString *message_name() const override { return LOG_STR("voice_assistant_set_configuration"); } #endif std::vector active_wake_words{}; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -2587,7 +2587,7 @@ class ListEntitiesAlarmControlPanelResponse final : public InfoResponseProtoMess static constexpr uint8_t MESSAGE_TYPE = 94; static constexpr uint8_t ESTIMATED_SIZE = 48; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_alarm_control_panel_response"; } + const LogString *message_name() const override { return LOG_STR("list_entities_alarm_control_panel_response"); } #endif uint32_t supported_features{0}; bool requires_code{false}; @@ -2605,7 +2605,7 @@ class AlarmControlPanelStateResponse final : public StateResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 95; static constexpr uint8_t ESTIMATED_SIZE = 11; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "alarm_control_panel_state_response"; } + const LogString *message_name() const override { return LOG_STR("alarm_control_panel_state_response"); } #endif enums::AlarmControlPanelState state{}; void encode(ProtoWriteBuffer &buffer) const; @@ -2621,7 +2621,7 @@ class AlarmControlPanelCommandRequest final : public CommandProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 96; static constexpr uint8_t ESTIMATED_SIZE = 20; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "alarm_control_panel_command_request"; } + const LogString *message_name() const override { return LOG_STR("alarm_control_panel_command_request"); } #endif enums::AlarmControlPanelStateCommand command{}; StringRef code{}; @@ -2641,7 +2641,7 @@ class ListEntitiesTextResponse final : public InfoResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 97; static constexpr uint8_t ESTIMATED_SIZE = 59; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_text_response"; } + const LogString *message_name() const override { return LOG_STR("list_entities_text_response"); } #endif uint32_t min_length{0}; uint32_t max_length{0}; @@ -2660,7 +2660,7 @@ class TextStateResponse final : public StateResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 98; static constexpr uint8_t ESTIMATED_SIZE = 20; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "text_state_response"; } + const LogString *message_name() const override { return LOG_STR("text_state_response"); } #endif StringRef state{}; bool missing_state{false}; @@ -2677,7 +2677,7 @@ class TextCommandRequest final : public CommandProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 99; static constexpr uint8_t ESTIMATED_SIZE = 18; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "text_command_request"; } + const LogString *message_name() const override { return LOG_STR("text_command_request"); } #endif StringRef state{}; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -2696,7 +2696,7 @@ class ListEntitiesDateResponse final : public InfoResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 100; static constexpr uint8_t ESTIMATED_SIZE = 40; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_date_response"; } + const LogString *message_name() const override { return LOG_STR("list_entities_date_response"); } #endif void encode(ProtoWriteBuffer &buffer) const; uint32_t calculate_size() const; @@ -2711,7 +2711,7 @@ class DateStateResponse final : public StateResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 101; static constexpr uint8_t ESTIMATED_SIZE = 23; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "date_state_response"; } + const LogString *message_name() const override { return LOG_STR("date_state_response"); } #endif bool missing_state{false}; uint32_t year{0}; @@ -2730,7 +2730,7 @@ class DateCommandRequest final : public CommandProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 102; static constexpr uint8_t ESTIMATED_SIZE = 21; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "date_command_request"; } + const LogString *message_name() const override { return LOG_STR("date_command_request"); } #endif uint32_t year{0}; uint32_t month{0}; @@ -2750,7 +2750,7 @@ class ListEntitiesTimeResponse final : public InfoResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 103; static constexpr uint8_t ESTIMATED_SIZE = 40; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_time_response"; } + const LogString *message_name() const override { return LOG_STR("list_entities_time_response"); } #endif void encode(ProtoWriteBuffer &buffer) const; uint32_t calculate_size() const; @@ -2765,7 +2765,7 @@ class TimeStateResponse final : public StateResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 104; static constexpr uint8_t ESTIMATED_SIZE = 23; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "time_state_response"; } + const LogString *message_name() const override { return LOG_STR("time_state_response"); } #endif bool missing_state{false}; uint32_t hour{0}; @@ -2784,7 +2784,7 @@ class TimeCommandRequest final : public CommandProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 105; static constexpr uint8_t ESTIMATED_SIZE = 21; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "time_command_request"; } + const LogString *message_name() const override { return LOG_STR("time_command_request"); } #endif uint32_t hour{0}; uint32_t minute{0}; @@ -2804,7 +2804,7 @@ class ListEntitiesEventResponse final : public InfoResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 107; static constexpr uint8_t ESTIMATED_SIZE = 67; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_event_response"; } + const LogString *message_name() const override { return LOG_STR("list_entities_event_response"); } #endif StringRef device_class{}; const FixedVector *event_types{}; @@ -2821,7 +2821,7 @@ class EventResponse final : public StateResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 108; static constexpr uint8_t ESTIMATED_SIZE = 18; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "event_response"; } + const LogString *message_name() const override { return LOG_STR("event_response"); } #endif StringRef event_type{}; void encode(ProtoWriteBuffer &buffer) const; @@ -2839,7 +2839,7 @@ class ListEntitiesValveResponse final : public InfoResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 109; static constexpr uint8_t ESTIMATED_SIZE = 55; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_valve_response"; } + const LogString *message_name() const override { return LOG_STR("list_entities_valve_response"); } #endif StringRef device_class{}; bool assumed_state{false}; @@ -2858,7 +2858,7 @@ class ValveStateResponse final : public StateResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 110; static constexpr uint8_t ESTIMATED_SIZE = 16; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "valve_state_response"; } + const LogString *message_name() const override { return LOG_STR("valve_state_response"); } #endif float position{0.0f}; enums::ValveOperation current_operation{}; @@ -2875,7 +2875,7 @@ class ValveCommandRequest final : public CommandProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 111; static constexpr uint8_t ESTIMATED_SIZE = 18; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "valve_command_request"; } + const LogString *message_name() const override { return LOG_STR("valve_command_request"); } #endif bool has_position{false}; float position{0.0f}; @@ -2895,7 +2895,7 @@ class ListEntitiesDateTimeResponse final : public InfoResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 112; static constexpr uint8_t ESTIMATED_SIZE = 40; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_date_time_response"; } + const LogString *message_name() const override { return LOG_STR("list_entities_date_time_response"); } #endif void encode(ProtoWriteBuffer &buffer) const; uint32_t calculate_size() const; @@ -2910,7 +2910,7 @@ class DateTimeStateResponse final : public StateResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 113; static constexpr uint8_t ESTIMATED_SIZE = 16; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "date_time_state_response"; } + const LogString *message_name() const override { return LOG_STR("date_time_state_response"); } #endif bool missing_state{false}; uint32_t epoch_seconds{0}; @@ -2927,7 +2927,7 @@ class DateTimeCommandRequest final : public CommandProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 114; static constexpr uint8_t ESTIMATED_SIZE = 14; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "date_time_command_request"; } + const LogString *message_name() const override { return LOG_STR("date_time_command_request"); } #endif uint32_t epoch_seconds{0}; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -2945,7 +2945,7 @@ class ListEntitiesUpdateResponse final : public InfoResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 116; static constexpr uint8_t ESTIMATED_SIZE = 49; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_update_response"; } + const LogString *message_name() const override { return LOG_STR("list_entities_update_response"); } #endif StringRef device_class{}; void encode(ProtoWriteBuffer &buffer) const; @@ -2961,7 +2961,7 @@ class UpdateStateResponse final : public StateResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 117; static constexpr uint8_t ESTIMATED_SIZE = 65; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "update_state_response"; } + const LogString *message_name() const override { return LOG_STR("update_state_response"); } #endif bool missing_state{false}; bool in_progress{false}; @@ -2985,7 +2985,7 @@ class UpdateCommandRequest final : public CommandProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 118; static constexpr uint8_t ESTIMATED_SIZE = 11; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "update_command_request"; } + const LogString *message_name() const override { return LOG_STR("update_command_request"); } #endif enums::UpdateCommand command{}; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -3003,7 +3003,7 @@ class ZWaveProxyFrame final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 128; static constexpr uint8_t ESTIMATED_SIZE = 19; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "z_wave_proxy_frame"; } + const LogString *message_name() const override { return LOG_STR("z_wave_proxy_frame"); } #endif const uint8_t *data{nullptr}; uint16_t data_len{0}; @@ -3021,7 +3021,7 @@ class ZWaveProxyRequest final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 129; static constexpr uint8_t ESTIMATED_SIZE = 21; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "z_wave_proxy_request"; } + const LogString *message_name() const override { return LOG_STR("z_wave_proxy_request"); } #endif enums::ZWaveProxyRequestType type{}; const uint8_t *data{nullptr}; @@ -3043,7 +3043,7 @@ class ListEntitiesInfraredResponse final : public InfoResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 135; static constexpr uint8_t ESTIMATED_SIZE = 44; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_infrared_response"; } + const LogString *message_name() const override { return LOG_STR("list_entities_infrared_response"); } #endif uint32_t capabilities{0}; void encode(ProtoWriteBuffer &buffer) const; @@ -3061,7 +3061,7 @@ class InfraredRFTransmitRawTimingsRequest final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 136; static constexpr uint8_t ESTIMATED_SIZE = 220; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "infrared_rf_transmit_raw_timings_request"; } + const LogString *message_name() const override { return LOG_STR("infrared_rf_transmit_raw_timings_request"); } #endif #ifdef USE_DEVICES uint32_t device_id{0}; @@ -3086,7 +3086,7 @@ class InfraredRFReceiveEvent final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 137; static constexpr uint8_t ESTIMATED_SIZE = 17; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "infrared_rf_receive_event"; } + const LogString *message_name() const override { return LOG_STR("infrared_rf_receive_event"); } #endif #ifdef USE_DEVICES uint32_t device_id{0}; @@ -3108,7 +3108,7 @@ class SerialProxyConfigureRequest final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 138; static constexpr uint8_t ESTIMATED_SIZE = 20; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "serial_proxy_configure_request"; } + const LogString *message_name() const override { return LOG_STR("serial_proxy_configure_request"); } #endif uint32_t instance{0}; uint32_t baudrate{0}; @@ -3128,7 +3128,7 @@ class SerialProxyDataReceived final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 139; static constexpr uint8_t ESTIMATED_SIZE = 23; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "serial_proxy_data_received"; } + const LogString *message_name() const override { return LOG_STR("serial_proxy_data_received"); } #endif uint32_t instance{0}; const uint8_t *data_ptr_{nullptr}; @@ -3150,7 +3150,7 @@ class SerialProxyWriteRequest final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 140; static constexpr uint8_t ESTIMATED_SIZE = 23; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "serial_proxy_write_request"; } + const LogString *message_name() const override { return LOG_STR("serial_proxy_write_request"); } #endif uint32_t instance{0}; const uint8_t *data{nullptr}; @@ -3168,7 +3168,7 @@ class SerialProxySetModemPinsRequest final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 141; static constexpr uint8_t ESTIMATED_SIZE = 8; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "serial_proxy_set_modem_pins_request"; } + const LogString *message_name() const override { return LOG_STR("serial_proxy_set_modem_pins_request"); } #endif uint32_t instance{0}; uint32_t line_states{0}; @@ -3184,7 +3184,7 @@ class SerialProxyGetModemPinsRequest final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 142; static constexpr uint8_t ESTIMATED_SIZE = 4; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "serial_proxy_get_modem_pins_request"; } + const LogString *message_name() const override { return LOG_STR("serial_proxy_get_modem_pins_request"); } #endif uint32_t instance{0}; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -3199,7 +3199,7 @@ class SerialProxyGetModemPinsResponse final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 143; static constexpr uint8_t ESTIMATED_SIZE = 8; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "serial_proxy_get_modem_pins_response"; } + const LogString *message_name() const override { return LOG_STR("serial_proxy_get_modem_pins_response"); } #endif uint32_t instance{0}; uint32_t line_states{0}; @@ -3216,7 +3216,7 @@ class SerialProxyRequest final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 144; static constexpr uint8_t ESTIMATED_SIZE = 6; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "serial_proxy_request"; } + const LogString *message_name() const override { return LOG_STR("serial_proxy_request"); } #endif uint32_t instance{0}; enums::SerialProxyRequestType type{}; @@ -3232,7 +3232,7 @@ class SerialProxyRequestResponse final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 147; static constexpr uint8_t ESTIMATED_SIZE = 17; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "serial_proxy_request_response"; } + const LogString *message_name() const override { return LOG_STR("serial_proxy_request_response"); } #endif uint32_t instance{0}; enums::SerialProxyRequestType type{}; @@ -3253,7 +3253,7 @@ class BluetoothSetConnectionParamsRequest final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 145; static constexpr uint8_t ESTIMATED_SIZE = 20; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "bluetooth_set_connection_params_request"; } + const LogString *message_name() const override { return LOG_STR("bluetooth_set_connection_params_request"); } #endif uint64_t address{0}; uint32_t min_interval{0}; @@ -3272,7 +3272,7 @@ class BluetoothSetConnectionParamsResponse final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 146; static constexpr uint8_t ESTIMATED_SIZE = 8; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "bluetooth_set_connection_params_response"; } + const LogString *message_name() const override { return LOG_STR("bluetooth_set_connection_params_response"); } #endif uint64_t address{0}; int32_t error{0}; diff --git a/esphome/components/api/api_pb2_dump.cpp b/esphome/components/api/api_pb2_dump.cpp index 5a53f0281f..a11f3b231e 100644 --- a/esphome/components/api/api_pb2_dump.cpp +++ b/esphome/components/api/api_pb2_dump.cpp @@ -2,6 +2,7 @@ // See script/api_protobuf/api_protobuf.py #include "api_pb2.h" #include "esphome/core/helpers.h" +#include "esphome/core/progmem.h" #include @@ -9,6 +10,21 @@ namespace esphome::api { +#ifdef USE_ESP8266 +// Out-of-line to avoid inlining strlen_P/memcpy_P at every call site +void DumpBuffer::append_p_esp8266(const char *str) { + size_t len = strlen_P(str); + size_t space = CAPACITY - 1 - pos_; + if (len > space) + len = space; + if (len > 0) { + memcpy_P(buf_ + pos_, str, len); + pos_ += len; + buf_[pos_] = '\0'; + } +} +#endif + // Helper function to append a quoted string, handling empty StringRef static inline void append_quoted_string(DumpBuffer &out, const StringRef &ref) { out.append("'"); @@ -19,8 +35,9 @@ static inline void append_quoted_string(DumpBuffer &out, const StringRef &ref) { } // Common helpers for dump_field functions +// field_name is a PROGMEM pointer (flash on ESP8266, regular pointer on other platforms) static inline void append_field_prefix(DumpBuffer &out, const char *field_name, int indent) { - out.append(indent, ' ').append(field_name).append(": "); + out.append(indent, ' ').append_p(field_name).append(": "); } static inline void append_uint(DumpBuffer &out, uint32_t value) { @@ -28,10 +45,11 @@ static inline void append_uint(DumpBuffer &out, uint32_t value) { } // RAII helper for message dump formatting +// message_name is a PROGMEM pointer (flash on ESP8266, regular pointer on other platforms) class MessageDumpHelper { public: MessageDumpHelper(DumpBuffer &out, const char *message_name) : out_(out) { - out_.append(message_name); + out_.append_p(message_name); out_.append(" {\n"); } ~MessageDumpHelper() { out_.append(" }"); } @@ -41,6 +59,10 @@ class MessageDumpHelper { }; // Helper functions to reduce code duplication in dump methods +// field_name parameters are PROGMEM pointers (flash on ESP8266, regular pointers on other platforms) +// Not all overloads are used in every build (depends on enabled components) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wunused-function" static void dump_field(DumpBuffer &out, const char *field_name, int32_t value, int indent = 2) { append_field_prefix(out, field_name, indent); out.set_pos(buf_append_printf(out.data(), DumpBuffer::CAPACITY, out.pos(), "%" PRId32 "\n", value)); @@ -85,56 +107,59 @@ static void dump_field(DumpBuffer &out, const char *field_name, const char *valu out.append("\n"); } +// proto_enum_to_string returns PROGMEM pointers, so use append_p template static void dump_field(DumpBuffer &out, const char *field_name, T value, int indent = 2) { append_field_prefix(out, field_name, indent); - out.append(proto_enum_to_string(value)); + out.append_p(proto_enum_to_string(value)); out.append("\n"); } // Helper for bytes fields - uses stack buffer to avoid heap allocation // Buffer sized for 160 bytes of data (480 chars with separators) to fit typical log buffer +// field_name is a PROGMEM pointer (flash on ESP8266, regular pointer on other platforms) static void dump_bytes_field(DumpBuffer &out, const char *field_name, const uint8_t *data, size_t len, int indent = 2) { char hex_buf[format_hex_pretty_size(160)]; append_field_prefix(out, field_name, indent); format_hex_pretty_to(hex_buf, data, len); out.append(hex_buf).append("\n"); } +#pragma GCC diagnostic pop template<> const char *proto_enum_to_string(enums::SerialProxyPortType value) { switch (value) { case enums::SERIAL_PROXY_PORT_TYPE_TTL: - return "SERIAL_PROXY_PORT_TYPE_TTL"; + return ESPHOME_PSTR("SERIAL_PROXY_PORT_TYPE_TTL"); case enums::SERIAL_PROXY_PORT_TYPE_RS232: - return "SERIAL_PROXY_PORT_TYPE_RS232"; + return ESPHOME_PSTR("SERIAL_PROXY_PORT_TYPE_RS232"); case enums::SERIAL_PROXY_PORT_TYPE_RS485: - return "SERIAL_PROXY_PORT_TYPE_RS485"; + return ESPHOME_PSTR("SERIAL_PROXY_PORT_TYPE_RS485"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } template<> const char *proto_enum_to_string(enums::EntityCategory value) { switch (value) { case enums::ENTITY_CATEGORY_NONE: - return "ENTITY_CATEGORY_NONE"; + return ESPHOME_PSTR("ENTITY_CATEGORY_NONE"); case enums::ENTITY_CATEGORY_CONFIG: - return "ENTITY_CATEGORY_CONFIG"; + return ESPHOME_PSTR("ENTITY_CATEGORY_CONFIG"); case enums::ENTITY_CATEGORY_DIAGNOSTIC: - return "ENTITY_CATEGORY_DIAGNOSTIC"; + return ESPHOME_PSTR("ENTITY_CATEGORY_DIAGNOSTIC"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } #ifdef USE_COVER template<> const char *proto_enum_to_string(enums::CoverOperation value) { switch (value) { case enums::COVER_OPERATION_IDLE: - return "COVER_OPERATION_IDLE"; + return ESPHOME_PSTR("COVER_OPERATION_IDLE"); case enums::COVER_OPERATION_IS_OPENING: - return "COVER_OPERATION_IS_OPENING"; + return ESPHOME_PSTR("COVER_OPERATION_IS_OPENING"); case enums::COVER_OPERATION_IS_CLOSING: - return "COVER_OPERATION_IS_CLOSING"; + return ESPHOME_PSTR("COVER_OPERATION_IS_CLOSING"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } #endif @@ -142,11 +167,11 @@ template<> const char *proto_enum_to_string(enums::CoverO template<> const char *proto_enum_to_string(enums::FanDirection value) { switch (value) { case enums::FAN_DIRECTION_FORWARD: - return "FAN_DIRECTION_FORWARD"; + return ESPHOME_PSTR("FAN_DIRECTION_FORWARD"); case enums::FAN_DIRECTION_REVERSE: - return "FAN_DIRECTION_REVERSE"; + return ESPHOME_PSTR("FAN_DIRECTION_REVERSE"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } #endif @@ -154,29 +179,29 @@ template<> const char *proto_enum_to_string(enums::FanDirec template<> const char *proto_enum_to_string(enums::ColorMode value) { switch (value) { case enums::COLOR_MODE_UNKNOWN: - return "COLOR_MODE_UNKNOWN"; + return ESPHOME_PSTR("COLOR_MODE_UNKNOWN"); case enums::COLOR_MODE_ON_OFF: - return "COLOR_MODE_ON_OFF"; + return ESPHOME_PSTR("COLOR_MODE_ON_OFF"); case enums::COLOR_MODE_LEGACY_BRIGHTNESS: - return "COLOR_MODE_LEGACY_BRIGHTNESS"; + return ESPHOME_PSTR("COLOR_MODE_LEGACY_BRIGHTNESS"); case enums::COLOR_MODE_BRIGHTNESS: - return "COLOR_MODE_BRIGHTNESS"; + return ESPHOME_PSTR("COLOR_MODE_BRIGHTNESS"); case enums::COLOR_MODE_WHITE: - return "COLOR_MODE_WHITE"; + return ESPHOME_PSTR("COLOR_MODE_WHITE"); case enums::COLOR_MODE_COLOR_TEMPERATURE: - return "COLOR_MODE_COLOR_TEMPERATURE"; + return ESPHOME_PSTR("COLOR_MODE_COLOR_TEMPERATURE"); case enums::COLOR_MODE_COLD_WARM_WHITE: - return "COLOR_MODE_COLD_WARM_WHITE"; + return ESPHOME_PSTR("COLOR_MODE_COLD_WARM_WHITE"); case enums::COLOR_MODE_RGB: - return "COLOR_MODE_RGB"; + return ESPHOME_PSTR("COLOR_MODE_RGB"); case enums::COLOR_MODE_RGB_WHITE: - return "COLOR_MODE_RGB_WHITE"; + return ESPHOME_PSTR("COLOR_MODE_RGB_WHITE"); case enums::COLOR_MODE_RGB_COLOR_TEMPERATURE: - return "COLOR_MODE_RGB_COLOR_TEMPERATURE"; + return ESPHOME_PSTR("COLOR_MODE_RGB_COLOR_TEMPERATURE"); case enums::COLOR_MODE_RGB_COLD_WARM_WHITE: - return "COLOR_MODE_RGB_COLD_WARM_WHITE"; + return ESPHOME_PSTR("COLOR_MODE_RGB_COLD_WARM_WHITE"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } #endif @@ -184,91 +209,91 @@ template<> const char *proto_enum_to_string(enums::ColorMode v template<> const char *proto_enum_to_string(enums::SensorStateClass value) { switch (value) { case enums::STATE_CLASS_NONE: - return "STATE_CLASS_NONE"; + return ESPHOME_PSTR("STATE_CLASS_NONE"); case enums::STATE_CLASS_MEASUREMENT: - return "STATE_CLASS_MEASUREMENT"; + return ESPHOME_PSTR("STATE_CLASS_MEASUREMENT"); case enums::STATE_CLASS_TOTAL_INCREASING: - return "STATE_CLASS_TOTAL_INCREASING"; + return ESPHOME_PSTR("STATE_CLASS_TOTAL_INCREASING"); case enums::STATE_CLASS_TOTAL: - return "STATE_CLASS_TOTAL"; + return ESPHOME_PSTR("STATE_CLASS_TOTAL"); case enums::STATE_CLASS_MEASUREMENT_ANGLE: - return "STATE_CLASS_MEASUREMENT_ANGLE"; + return ESPHOME_PSTR("STATE_CLASS_MEASUREMENT_ANGLE"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } #endif template<> const char *proto_enum_to_string(enums::LogLevel value) { switch (value) { case enums::LOG_LEVEL_NONE: - return "LOG_LEVEL_NONE"; + return ESPHOME_PSTR("LOG_LEVEL_NONE"); case enums::LOG_LEVEL_ERROR: - return "LOG_LEVEL_ERROR"; + return ESPHOME_PSTR("LOG_LEVEL_ERROR"); case enums::LOG_LEVEL_WARN: - return "LOG_LEVEL_WARN"; + return ESPHOME_PSTR("LOG_LEVEL_WARN"); case enums::LOG_LEVEL_INFO: - return "LOG_LEVEL_INFO"; + return ESPHOME_PSTR("LOG_LEVEL_INFO"); case enums::LOG_LEVEL_CONFIG: - return "LOG_LEVEL_CONFIG"; + return ESPHOME_PSTR("LOG_LEVEL_CONFIG"); case enums::LOG_LEVEL_DEBUG: - return "LOG_LEVEL_DEBUG"; + return ESPHOME_PSTR("LOG_LEVEL_DEBUG"); case enums::LOG_LEVEL_VERBOSE: - return "LOG_LEVEL_VERBOSE"; + return ESPHOME_PSTR("LOG_LEVEL_VERBOSE"); case enums::LOG_LEVEL_VERY_VERBOSE: - return "LOG_LEVEL_VERY_VERBOSE"; + return ESPHOME_PSTR("LOG_LEVEL_VERY_VERBOSE"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } template<> const char *proto_enum_to_string(enums::DSTRuleType value) { switch (value) { case enums::DST_RULE_TYPE_NONE: - return "DST_RULE_TYPE_NONE"; + return ESPHOME_PSTR("DST_RULE_TYPE_NONE"); case enums::DST_RULE_TYPE_MONTH_WEEK_DAY: - return "DST_RULE_TYPE_MONTH_WEEK_DAY"; + return ESPHOME_PSTR("DST_RULE_TYPE_MONTH_WEEK_DAY"); case enums::DST_RULE_TYPE_JULIAN_NO_LEAP: - return "DST_RULE_TYPE_JULIAN_NO_LEAP"; + return ESPHOME_PSTR("DST_RULE_TYPE_JULIAN_NO_LEAP"); case enums::DST_RULE_TYPE_DAY_OF_YEAR: - return "DST_RULE_TYPE_DAY_OF_YEAR"; + return ESPHOME_PSTR("DST_RULE_TYPE_DAY_OF_YEAR"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } #ifdef USE_API_USER_DEFINED_ACTIONS template<> const char *proto_enum_to_string(enums::ServiceArgType value) { switch (value) { case enums::SERVICE_ARG_TYPE_BOOL: - return "SERVICE_ARG_TYPE_BOOL"; + return ESPHOME_PSTR("SERVICE_ARG_TYPE_BOOL"); case enums::SERVICE_ARG_TYPE_INT: - return "SERVICE_ARG_TYPE_INT"; + return ESPHOME_PSTR("SERVICE_ARG_TYPE_INT"); case enums::SERVICE_ARG_TYPE_FLOAT: - return "SERVICE_ARG_TYPE_FLOAT"; + return ESPHOME_PSTR("SERVICE_ARG_TYPE_FLOAT"); case enums::SERVICE_ARG_TYPE_STRING: - return "SERVICE_ARG_TYPE_STRING"; + return ESPHOME_PSTR("SERVICE_ARG_TYPE_STRING"); case enums::SERVICE_ARG_TYPE_BOOL_ARRAY: - return "SERVICE_ARG_TYPE_BOOL_ARRAY"; + return ESPHOME_PSTR("SERVICE_ARG_TYPE_BOOL_ARRAY"); case enums::SERVICE_ARG_TYPE_INT_ARRAY: - return "SERVICE_ARG_TYPE_INT_ARRAY"; + return ESPHOME_PSTR("SERVICE_ARG_TYPE_INT_ARRAY"); case enums::SERVICE_ARG_TYPE_FLOAT_ARRAY: - return "SERVICE_ARG_TYPE_FLOAT_ARRAY"; + return ESPHOME_PSTR("SERVICE_ARG_TYPE_FLOAT_ARRAY"); case enums::SERVICE_ARG_TYPE_STRING_ARRAY: - return "SERVICE_ARG_TYPE_STRING_ARRAY"; + return ESPHOME_PSTR("SERVICE_ARG_TYPE_STRING_ARRAY"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } template<> const char *proto_enum_to_string(enums::SupportsResponseType value) { switch (value) { case enums::SUPPORTS_RESPONSE_NONE: - return "SUPPORTS_RESPONSE_NONE"; + return ESPHOME_PSTR("SUPPORTS_RESPONSE_NONE"); case enums::SUPPORTS_RESPONSE_OPTIONAL: - return "SUPPORTS_RESPONSE_OPTIONAL"; + return ESPHOME_PSTR("SUPPORTS_RESPONSE_OPTIONAL"); case enums::SUPPORTS_RESPONSE_ONLY: - return "SUPPORTS_RESPONSE_ONLY"; + return ESPHOME_PSTR("SUPPORTS_RESPONSE_ONLY"); case enums::SUPPORTS_RESPONSE_STATUS: - return "SUPPORTS_RESPONSE_STATUS"; + return ESPHOME_PSTR("SUPPORTS_RESPONSE_STATUS"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } #endif @@ -276,103 +301,103 @@ template<> const char *proto_enum_to_string(enums:: template<> const char *proto_enum_to_string(enums::ClimateMode value) { switch (value) { case enums::CLIMATE_MODE_OFF: - return "CLIMATE_MODE_OFF"; + return ESPHOME_PSTR("CLIMATE_MODE_OFF"); case enums::CLIMATE_MODE_HEAT_COOL: - return "CLIMATE_MODE_HEAT_COOL"; + return ESPHOME_PSTR("CLIMATE_MODE_HEAT_COOL"); case enums::CLIMATE_MODE_COOL: - return "CLIMATE_MODE_COOL"; + return ESPHOME_PSTR("CLIMATE_MODE_COOL"); case enums::CLIMATE_MODE_HEAT: - return "CLIMATE_MODE_HEAT"; + return ESPHOME_PSTR("CLIMATE_MODE_HEAT"); case enums::CLIMATE_MODE_FAN_ONLY: - return "CLIMATE_MODE_FAN_ONLY"; + return ESPHOME_PSTR("CLIMATE_MODE_FAN_ONLY"); case enums::CLIMATE_MODE_DRY: - return "CLIMATE_MODE_DRY"; + return ESPHOME_PSTR("CLIMATE_MODE_DRY"); case enums::CLIMATE_MODE_AUTO: - return "CLIMATE_MODE_AUTO"; + return ESPHOME_PSTR("CLIMATE_MODE_AUTO"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } template<> const char *proto_enum_to_string(enums::ClimateFanMode value) { switch (value) { case enums::CLIMATE_FAN_ON: - return "CLIMATE_FAN_ON"; + return ESPHOME_PSTR("CLIMATE_FAN_ON"); case enums::CLIMATE_FAN_OFF: - return "CLIMATE_FAN_OFF"; + return ESPHOME_PSTR("CLIMATE_FAN_OFF"); case enums::CLIMATE_FAN_AUTO: - return "CLIMATE_FAN_AUTO"; + return ESPHOME_PSTR("CLIMATE_FAN_AUTO"); case enums::CLIMATE_FAN_LOW: - return "CLIMATE_FAN_LOW"; + return ESPHOME_PSTR("CLIMATE_FAN_LOW"); case enums::CLIMATE_FAN_MEDIUM: - return "CLIMATE_FAN_MEDIUM"; + return ESPHOME_PSTR("CLIMATE_FAN_MEDIUM"); case enums::CLIMATE_FAN_HIGH: - return "CLIMATE_FAN_HIGH"; + return ESPHOME_PSTR("CLIMATE_FAN_HIGH"); case enums::CLIMATE_FAN_MIDDLE: - return "CLIMATE_FAN_MIDDLE"; + return ESPHOME_PSTR("CLIMATE_FAN_MIDDLE"); case enums::CLIMATE_FAN_FOCUS: - return "CLIMATE_FAN_FOCUS"; + return ESPHOME_PSTR("CLIMATE_FAN_FOCUS"); case enums::CLIMATE_FAN_DIFFUSE: - return "CLIMATE_FAN_DIFFUSE"; + return ESPHOME_PSTR("CLIMATE_FAN_DIFFUSE"); case enums::CLIMATE_FAN_QUIET: - return "CLIMATE_FAN_QUIET"; + return ESPHOME_PSTR("CLIMATE_FAN_QUIET"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } template<> const char *proto_enum_to_string(enums::ClimateSwingMode value) { switch (value) { case enums::CLIMATE_SWING_OFF: - return "CLIMATE_SWING_OFF"; + return ESPHOME_PSTR("CLIMATE_SWING_OFF"); case enums::CLIMATE_SWING_BOTH: - return "CLIMATE_SWING_BOTH"; + return ESPHOME_PSTR("CLIMATE_SWING_BOTH"); case enums::CLIMATE_SWING_VERTICAL: - return "CLIMATE_SWING_VERTICAL"; + return ESPHOME_PSTR("CLIMATE_SWING_VERTICAL"); case enums::CLIMATE_SWING_HORIZONTAL: - return "CLIMATE_SWING_HORIZONTAL"; + return ESPHOME_PSTR("CLIMATE_SWING_HORIZONTAL"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } template<> const char *proto_enum_to_string(enums::ClimateAction value) { switch (value) { case enums::CLIMATE_ACTION_OFF: - return "CLIMATE_ACTION_OFF"; + return ESPHOME_PSTR("CLIMATE_ACTION_OFF"); case enums::CLIMATE_ACTION_COOLING: - return "CLIMATE_ACTION_COOLING"; + return ESPHOME_PSTR("CLIMATE_ACTION_COOLING"); case enums::CLIMATE_ACTION_HEATING: - return "CLIMATE_ACTION_HEATING"; + return ESPHOME_PSTR("CLIMATE_ACTION_HEATING"); case enums::CLIMATE_ACTION_IDLE: - return "CLIMATE_ACTION_IDLE"; + return ESPHOME_PSTR("CLIMATE_ACTION_IDLE"); case enums::CLIMATE_ACTION_DRYING: - return "CLIMATE_ACTION_DRYING"; + return ESPHOME_PSTR("CLIMATE_ACTION_DRYING"); case enums::CLIMATE_ACTION_FAN: - return "CLIMATE_ACTION_FAN"; + return ESPHOME_PSTR("CLIMATE_ACTION_FAN"); case enums::CLIMATE_ACTION_DEFROSTING: - return "CLIMATE_ACTION_DEFROSTING"; + return ESPHOME_PSTR("CLIMATE_ACTION_DEFROSTING"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } template<> const char *proto_enum_to_string(enums::ClimatePreset value) { switch (value) { case enums::CLIMATE_PRESET_NONE: - return "CLIMATE_PRESET_NONE"; + return ESPHOME_PSTR("CLIMATE_PRESET_NONE"); case enums::CLIMATE_PRESET_HOME: - return "CLIMATE_PRESET_HOME"; + return ESPHOME_PSTR("CLIMATE_PRESET_HOME"); case enums::CLIMATE_PRESET_AWAY: - return "CLIMATE_PRESET_AWAY"; + return ESPHOME_PSTR("CLIMATE_PRESET_AWAY"); case enums::CLIMATE_PRESET_BOOST: - return "CLIMATE_PRESET_BOOST"; + return ESPHOME_PSTR("CLIMATE_PRESET_BOOST"); case enums::CLIMATE_PRESET_COMFORT: - return "CLIMATE_PRESET_COMFORT"; + return ESPHOME_PSTR("CLIMATE_PRESET_COMFORT"); case enums::CLIMATE_PRESET_ECO: - return "CLIMATE_PRESET_ECO"; + return ESPHOME_PSTR("CLIMATE_PRESET_ECO"); case enums::CLIMATE_PRESET_SLEEP: - return "CLIMATE_PRESET_SLEEP"; + return ESPHOME_PSTR("CLIMATE_PRESET_SLEEP"); case enums::CLIMATE_PRESET_ACTIVITY: - return "CLIMATE_PRESET_ACTIVITY"; + return ESPHOME_PSTR("CLIMATE_PRESET_ACTIVITY"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } #endif @@ -380,21 +405,21 @@ template<> const char *proto_enum_to_string(enums::Climate template<> const char *proto_enum_to_string(enums::WaterHeaterMode value) { switch (value) { case enums::WATER_HEATER_MODE_OFF: - return "WATER_HEATER_MODE_OFF"; + return ESPHOME_PSTR("WATER_HEATER_MODE_OFF"); case enums::WATER_HEATER_MODE_ECO: - return "WATER_HEATER_MODE_ECO"; + return ESPHOME_PSTR("WATER_HEATER_MODE_ECO"); case enums::WATER_HEATER_MODE_ELECTRIC: - return "WATER_HEATER_MODE_ELECTRIC"; + return ESPHOME_PSTR("WATER_HEATER_MODE_ELECTRIC"); case enums::WATER_HEATER_MODE_PERFORMANCE: - return "WATER_HEATER_MODE_PERFORMANCE"; + return ESPHOME_PSTR("WATER_HEATER_MODE_PERFORMANCE"); case enums::WATER_HEATER_MODE_HIGH_DEMAND: - return "WATER_HEATER_MODE_HIGH_DEMAND"; + return ESPHOME_PSTR("WATER_HEATER_MODE_HIGH_DEMAND"); case enums::WATER_HEATER_MODE_HEAT_PUMP: - return "WATER_HEATER_MODE_HEAT_PUMP"; + return ESPHOME_PSTR("WATER_HEATER_MODE_HEAT_PUMP"); case enums::WATER_HEATER_MODE_GAS: - return "WATER_HEATER_MODE_GAS"; + return ESPHOME_PSTR("WATER_HEATER_MODE_GAS"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } #endif @@ -402,36 +427,36 @@ template<> const char *proto_enum_to_string(enums::WaterHeaterCommandHasField value) { switch (value) { case enums::WATER_HEATER_COMMAND_HAS_NONE: - return "WATER_HEATER_COMMAND_HAS_NONE"; + return ESPHOME_PSTR("WATER_HEATER_COMMAND_HAS_NONE"); case enums::WATER_HEATER_COMMAND_HAS_MODE: - return "WATER_HEATER_COMMAND_HAS_MODE"; + return ESPHOME_PSTR("WATER_HEATER_COMMAND_HAS_MODE"); case enums::WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE: - return "WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE"; + return ESPHOME_PSTR("WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE"); case enums::WATER_HEATER_COMMAND_HAS_STATE: - return "WATER_HEATER_COMMAND_HAS_STATE"; + return ESPHOME_PSTR("WATER_HEATER_COMMAND_HAS_STATE"); case enums::WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_LOW: - return "WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_LOW"; + return ESPHOME_PSTR("WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_LOW"); case enums::WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH: - return "WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH"; + return ESPHOME_PSTR("WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH"); case enums::WATER_HEATER_COMMAND_HAS_ON_STATE: - return "WATER_HEATER_COMMAND_HAS_ON_STATE"; + return ESPHOME_PSTR("WATER_HEATER_COMMAND_HAS_ON_STATE"); case enums::WATER_HEATER_COMMAND_HAS_AWAY_STATE: - return "WATER_HEATER_COMMAND_HAS_AWAY_STATE"; + return ESPHOME_PSTR("WATER_HEATER_COMMAND_HAS_AWAY_STATE"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } #ifdef USE_NUMBER template<> const char *proto_enum_to_string(enums::NumberMode value) { switch (value) { case enums::NUMBER_MODE_AUTO: - return "NUMBER_MODE_AUTO"; + return ESPHOME_PSTR("NUMBER_MODE_AUTO"); case enums::NUMBER_MODE_BOX: - return "NUMBER_MODE_BOX"; + return ESPHOME_PSTR("NUMBER_MODE_BOX"); case enums::NUMBER_MODE_SLIDER: - return "NUMBER_MODE_SLIDER"; + return ESPHOME_PSTR("NUMBER_MODE_SLIDER"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } #endif @@ -439,31 +464,31 @@ template<> const char *proto_enum_to_string(enums::NumberMode template<> const char *proto_enum_to_string(enums::LockState value) { switch (value) { case enums::LOCK_STATE_NONE: - return "LOCK_STATE_NONE"; + return ESPHOME_PSTR("LOCK_STATE_NONE"); case enums::LOCK_STATE_LOCKED: - return "LOCK_STATE_LOCKED"; + return ESPHOME_PSTR("LOCK_STATE_LOCKED"); case enums::LOCK_STATE_UNLOCKED: - return "LOCK_STATE_UNLOCKED"; + return ESPHOME_PSTR("LOCK_STATE_UNLOCKED"); case enums::LOCK_STATE_JAMMED: - return "LOCK_STATE_JAMMED"; + return ESPHOME_PSTR("LOCK_STATE_JAMMED"); case enums::LOCK_STATE_LOCKING: - return "LOCK_STATE_LOCKING"; + return ESPHOME_PSTR("LOCK_STATE_LOCKING"); case enums::LOCK_STATE_UNLOCKING: - return "LOCK_STATE_UNLOCKING"; + return ESPHOME_PSTR("LOCK_STATE_UNLOCKING"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } template<> const char *proto_enum_to_string(enums::LockCommand value) { switch (value) { case enums::LOCK_UNLOCK: - return "LOCK_UNLOCK"; + return ESPHOME_PSTR("LOCK_UNLOCK"); case enums::LOCK_LOCK: - return "LOCK_LOCK"; + return ESPHOME_PSTR("LOCK_LOCK"); case enums::LOCK_OPEN: - return "LOCK_OPEN"; + return ESPHOME_PSTR("LOCK_OPEN"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } #endif @@ -471,65 +496,65 @@ template<> const char *proto_enum_to_string(enums::LockComma template<> const char *proto_enum_to_string(enums::MediaPlayerState value) { switch (value) { case enums::MEDIA_PLAYER_STATE_NONE: - return "MEDIA_PLAYER_STATE_NONE"; + return ESPHOME_PSTR("MEDIA_PLAYER_STATE_NONE"); case enums::MEDIA_PLAYER_STATE_IDLE: - return "MEDIA_PLAYER_STATE_IDLE"; + return ESPHOME_PSTR("MEDIA_PLAYER_STATE_IDLE"); case enums::MEDIA_PLAYER_STATE_PLAYING: - return "MEDIA_PLAYER_STATE_PLAYING"; + return ESPHOME_PSTR("MEDIA_PLAYER_STATE_PLAYING"); case enums::MEDIA_PLAYER_STATE_PAUSED: - return "MEDIA_PLAYER_STATE_PAUSED"; + return ESPHOME_PSTR("MEDIA_PLAYER_STATE_PAUSED"); case enums::MEDIA_PLAYER_STATE_ANNOUNCING: - return "MEDIA_PLAYER_STATE_ANNOUNCING"; + return ESPHOME_PSTR("MEDIA_PLAYER_STATE_ANNOUNCING"); case enums::MEDIA_PLAYER_STATE_OFF: - return "MEDIA_PLAYER_STATE_OFF"; + return ESPHOME_PSTR("MEDIA_PLAYER_STATE_OFF"); case enums::MEDIA_PLAYER_STATE_ON: - return "MEDIA_PLAYER_STATE_ON"; + return ESPHOME_PSTR("MEDIA_PLAYER_STATE_ON"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } template<> const char *proto_enum_to_string(enums::MediaPlayerCommand value) { switch (value) { case enums::MEDIA_PLAYER_COMMAND_PLAY: - return "MEDIA_PLAYER_COMMAND_PLAY"; + return ESPHOME_PSTR("MEDIA_PLAYER_COMMAND_PLAY"); case enums::MEDIA_PLAYER_COMMAND_PAUSE: - return "MEDIA_PLAYER_COMMAND_PAUSE"; + return ESPHOME_PSTR("MEDIA_PLAYER_COMMAND_PAUSE"); case enums::MEDIA_PLAYER_COMMAND_STOP: - return "MEDIA_PLAYER_COMMAND_STOP"; + return ESPHOME_PSTR("MEDIA_PLAYER_COMMAND_STOP"); case enums::MEDIA_PLAYER_COMMAND_MUTE: - return "MEDIA_PLAYER_COMMAND_MUTE"; + return ESPHOME_PSTR("MEDIA_PLAYER_COMMAND_MUTE"); case enums::MEDIA_PLAYER_COMMAND_UNMUTE: - return "MEDIA_PLAYER_COMMAND_UNMUTE"; + return ESPHOME_PSTR("MEDIA_PLAYER_COMMAND_UNMUTE"); case enums::MEDIA_PLAYER_COMMAND_TOGGLE: - return "MEDIA_PLAYER_COMMAND_TOGGLE"; + return ESPHOME_PSTR("MEDIA_PLAYER_COMMAND_TOGGLE"); case enums::MEDIA_PLAYER_COMMAND_VOLUME_UP: - return "MEDIA_PLAYER_COMMAND_VOLUME_UP"; + return ESPHOME_PSTR("MEDIA_PLAYER_COMMAND_VOLUME_UP"); case enums::MEDIA_PLAYER_COMMAND_VOLUME_DOWN: - return "MEDIA_PLAYER_COMMAND_VOLUME_DOWN"; + return ESPHOME_PSTR("MEDIA_PLAYER_COMMAND_VOLUME_DOWN"); case enums::MEDIA_PLAYER_COMMAND_ENQUEUE: - return "MEDIA_PLAYER_COMMAND_ENQUEUE"; + return ESPHOME_PSTR("MEDIA_PLAYER_COMMAND_ENQUEUE"); case enums::MEDIA_PLAYER_COMMAND_REPEAT_ONE: - return "MEDIA_PLAYER_COMMAND_REPEAT_ONE"; + return ESPHOME_PSTR("MEDIA_PLAYER_COMMAND_REPEAT_ONE"); case enums::MEDIA_PLAYER_COMMAND_REPEAT_OFF: - return "MEDIA_PLAYER_COMMAND_REPEAT_OFF"; + return ESPHOME_PSTR("MEDIA_PLAYER_COMMAND_REPEAT_OFF"); case enums::MEDIA_PLAYER_COMMAND_CLEAR_PLAYLIST: - return "MEDIA_PLAYER_COMMAND_CLEAR_PLAYLIST"; + return ESPHOME_PSTR("MEDIA_PLAYER_COMMAND_CLEAR_PLAYLIST"); case enums::MEDIA_PLAYER_COMMAND_TURN_ON: - return "MEDIA_PLAYER_COMMAND_TURN_ON"; + return ESPHOME_PSTR("MEDIA_PLAYER_COMMAND_TURN_ON"); case enums::MEDIA_PLAYER_COMMAND_TURN_OFF: - return "MEDIA_PLAYER_COMMAND_TURN_OFF"; + return ESPHOME_PSTR("MEDIA_PLAYER_COMMAND_TURN_OFF"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } template<> const char *proto_enum_to_string(enums::MediaPlayerFormatPurpose value) { switch (value) { case enums::MEDIA_PLAYER_FORMAT_PURPOSE_DEFAULT: - return "MEDIA_PLAYER_FORMAT_PURPOSE_DEFAULT"; + return ESPHOME_PSTR("MEDIA_PLAYER_FORMAT_PURPOSE_DEFAULT"); case enums::MEDIA_PLAYER_FORMAT_PURPOSE_ANNOUNCEMENT: - return "MEDIA_PLAYER_FORMAT_PURPOSE_ANNOUNCEMENT"; + return ESPHOME_PSTR("MEDIA_PLAYER_FORMAT_PURPOSE_ANNOUNCEMENT"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } #endif @@ -538,49 +563,49 @@ template<> const char *proto_enum_to_string(enums::BluetoothDeviceRequestType value) { switch (value) { case enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT: - return "BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT"; + return ESPHOME_PSTR("BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT"); case enums::BLUETOOTH_DEVICE_REQUEST_TYPE_DISCONNECT: - return "BLUETOOTH_DEVICE_REQUEST_TYPE_DISCONNECT"; + return ESPHOME_PSTR("BLUETOOTH_DEVICE_REQUEST_TYPE_DISCONNECT"); case enums::BLUETOOTH_DEVICE_REQUEST_TYPE_PAIR: - return "BLUETOOTH_DEVICE_REQUEST_TYPE_PAIR"; + return ESPHOME_PSTR("BLUETOOTH_DEVICE_REQUEST_TYPE_PAIR"); case enums::BLUETOOTH_DEVICE_REQUEST_TYPE_UNPAIR: - return "BLUETOOTH_DEVICE_REQUEST_TYPE_UNPAIR"; + return ESPHOME_PSTR("BLUETOOTH_DEVICE_REQUEST_TYPE_UNPAIR"); case enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT_V3_WITH_CACHE: - return "BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT_V3_WITH_CACHE"; + return ESPHOME_PSTR("BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT_V3_WITH_CACHE"); case enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT_V3_WITHOUT_CACHE: - return "BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT_V3_WITHOUT_CACHE"; + return ESPHOME_PSTR("BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT_V3_WITHOUT_CACHE"); case enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CLEAR_CACHE: - return "BLUETOOTH_DEVICE_REQUEST_TYPE_CLEAR_CACHE"; + return ESPHOME_PSTR("BLUETOOTH_DEVICE_REQUEST_TYPE_CLEAR_CACHE"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } template<> const char *proto_enum_to_string(enums::BluetoothScannerState value) { switch (value) { case enums::BLUETOOTH_SCANNER_STATE_IDLE: - return "BLUETOOTH_SCANNER_STATE_IDLE"; + return ESPHOME_PSTR("BLUETOOTH_SCANNER_STATE_IDLE"); case enums::BLUETOOTH_SCANNER_STATE_STARTING: - return "BLUETOOTH_SCANNER_STATE_STARTING"; + return ESPHOME_PSTR("BLUETOOTH_SCANNER_STATE_STARTING"); case enums::BLUETOOTH_SCANNER_STATE_RUNNING: - return "BLUETOOTH_SCANNER_STATE_RUNNING"; + return ESPHOME_PSTR("BLUETOOTH_SCANNER_STATE_RUNNING"); case enums::BLUETOOTH_SCANNER_STATE_FAILED: - return "BLUETOOTH_SCANNER_STATE_FAILED"; + return ESPHOME_PSTR("BLUETOOTH_SCANNER_STATE_FAILED"); case enums::BLUETOOTH_SCANNER_STATE_STOPPING: - return "BLUETOOTH_SCANNER_STATE_STOPPING"; + return ESPHOME_PSTR("BLUETOOTH_SCANNER_STATE_STOPPING"); case enums::BLUETOOTH_SCANNER_STATE_STOPPED: - return "BLUETOOTH_SCANNER_STATE_STOPPED"; + return ESPHOME_PSTR("BLUETOOTH_SCANNER_STATE_STOPPED"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } template<> const char *proto_enum_to_string(enums::BluetoothScannerMode value) { switch (value) { case enums::BLUETOOTH_SCANNER_MODE_PASSIVE: - return "BLUETOOTH_SCANNER_MODE_PASSIVE"; + return ESPHOME_PSTR("BLUETOOTH_SCANNER_MODE_PASSIVE"); case enums::BLUETOOTH_SCANNER_MODE_ACTIVE: - return "BLUETOOTH_SCANNER_MODE_ACTIVE"; + return ESPHOME_PSTR("BLUETOOTH_SCANNER_MODE_ACTIVE"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } #endif @@ -588,76 +613,76 @@ template<> const char *proto_enum_to_string(enums::VoiceAssistantSubscribeFlag value) { switch (value) { case enums::VOICE_ASSISTANT_SUBSCRIBE_NONE: - return "VOICE_ASSISTANT_SUBSCRIBE_NONE"; + return ESPHOME_PSTR("VOICE_ASSISTANT_SUBSCRIBE_NONE"); case enums::VOICE_ASSISTANT_SUBSCRIBE_API_AUDIO: - return "VOICE_ASSISTANT_SUBSCRIBE_API_AUDIO"; + return ESPHOME_PSTR("VOICE_ASSISTANT_SUBSCRIBE_API_AUDIO"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } template<> const char *proto_enum_to_string(enums::VoiceAssistantRequestFlag value) { switch (value) { case enums::VOICE_ASSISTANT_REQUEST_NONE: - return "VOICE_ASSISTANT_REQUEST_NONE"; + return ESPHOME_PSTR("VOICE_ASSISTANT_REQUEST_NONE"); case enums::VOICE_ASSISTANT_REQUEST_USE_VAD: - return "VOICE_ASSISTANT_REQUEST_USE_VAD"; + return ESPHOME_PSTR("VOICE_ASSISTANT_REQUEST_USE_VAD"); case enums::VOICE_ASSISTANT_REQUEST_USE_WAKE_WORD: - return "VOICE_ASSISTANT_REQUEST_USE_WAKE_WORD"; + return ESPHOME_PSTR("VOICE_ASSISTANT_REQUEST_USE_WAKE_WORD"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } #ifdef USE_VOICE_ASSISTANT template<> const char *proto_enum_to_string(enums::VoiceAssistantEvent value) { switch (value) { case enums::VOICE_ASSISTANT_ERROR: - return "VOICE_ASSISTANT_ERROR"; + return ESPHOME_PSTR("VOICE_ASSISTANT_ERROR"); case enums::VOICE_ASSISTANT_RUN_START: - return "VOICE_ASSISTANT_RUN_START"; + return ESPHOME_PSTR("VOICE_ASSISTANT_RUN_START"); case enums::VOICE_ASSISTANT_RUN_END: - return "VOICE_ASSISTANT_RUN_END"; + return ESPHOME_PSTR("VOICE_ASSISTANT_RUN_END"); case enums::VOICE_ASSISTANT_STT_START: - return "VOICE_ASSISTANT_STT_START"; + return ESPHOME_PSTR("VOICE_ASSISTANT_STT_START"); case enums::VOICE_ASSISTANT_STT_END: - return "VOICE_ASSISTANT_STT_END"; + return ESPHOME_PSTR("VOICE_ASSISTANT_STT_END"); case enums::VOICE_ASSISTANT_INTENT_START: - return "VOICE_ASSISTANT_INTENT_START"; + return ESPHOME_PSTR("VOICE_ASSISTANT_INTENT_START"); case enums::VOICE_ASSISTANT_INTENT_END: - return "VOICE_ASSISTANT_INTENT_END"; + return ESPHOME_PSTR("VOICE_ASSISTANT_INTENT_END"); case enums::VOICE_ASSISTANT_TTS_START: - return "VOICE_ASSISTANT_TTS_START"; + return ESPHOME_PSTR("VOICE_ASSISTANT_TTS_START"); case enums::VOICE_ASSISTANT_TTS_END: - return "VOICE_ASSISTANT_TTS_END"; + return ESPHOME_PSTR("VOICE_ASSISTANT_TTS_END"); case enums::VOICE_ASSISTANT_WAKE_WORD_START: - return "VOICE_ASSISTANT_WAKE_WORD_START"; + return ESPHOME_PSTR("VOICE_ASSISTANT_WAKE_WORD_START"); case enums::VOICE_ASSISTANT_WAKE_WORD_END: - return "VOICE_ASSISTANT_WAKE_WORD_END"; + return ESPHOME_PSTR("VOICE_ASSISTANT_WAKE_WORD_END"); case enums::VOICE_ASSISTANT_STT_VAD_START: - return "VOICE_ASSISTANT_STT_VAD_START"; + return ESPHOME_PSTR("VOICE_ASSISTANT_STT_VAD_START"); case enums::VOICE_ASSISTANT_STT_VAD_END: - return "VOICE_ASSISTANT_STT_VAD_END"; + return ESPHOME_PSTR("VOICE_ASSISTANT_STT_VAD_END"); case enums::VOICE_ASSISTANT_TTS_STREAM_START: - return "VOICE_ASSISTANT_TTS_STREAM_START"; + return ESPHOME_PSTR("VOICE_ASSISTANT_TTS_STREAM_START"); case enums::VOICE_ASSISTANT_TTS_STREAM_END: - return "VOICE_ASSISTANT_TTS_STREAM_END"; + return ESPHOME_PSTR("VOICE_ASSISTANT_TTS_STREAM_END"); case enums::VOICE_ASSISTANT_INTENT_PROGRESS: - return "VOICE_ASSISTANT_INTENT_PROGRESS"; + return ESPHOME_PSTR("VOICE_ASSISTANT_INTENT_PROGRESS"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } template<> const char *proto_enum_to_string(enums::VoiceAssistantTimerEvent value) { switch (value) { case enums::VOICE_ASSISTANT_TIMER_STARTED: - return "VOICE_ASSISTANT_TIMER_STARTED"; + return ESPHOME_PSTR("VOICE_ASSISTANT_TIMER_STARTED"); case enums::VOICE_ASSISTANT_TIMER_UPDATED: - return "VOICE_ASSISTANT_TIMER_UPDATED"; + return ESPHOME_PSTR("VOICE_ASSISTANT_TIMER_UPDATED"); case enums::VOICE_ASSISTANT_TIMER_CANCELLED: - return "VOICE_ASSISTANT_TIMER_CANCELLED"; + return ESPHOME_PSTR("VOICE_ASSISTANT_TIMER_CANCELLED"); case enums::VOICE_ASSISTANT_TIMER_FINISHED: - return "VOICE_ASSISTANT_TIMER_FINISHED"; + return ESPHOME_PSTR("VOICE_ASSISTANT_TIMER_FINISHED"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } #endif @@ -665,48 +690,48 @@ template<> const char *proto_enum_to_string(enu template<> const char *proto_enum_to_string(enums::AlarmControlPanelState value) { switch (value) { case enums::ALARM_STATE_DISARMED: - return "ALARM_STATE_DISARMED"; + return ESPHOME_PSTR("ALARM_STATE_DISARMED"); case enums::ALARM_STATE_ARMED_HOME: - return "ALARM_STATE_ARMED_HOME"; + return ESPHOME_PSTR("ALARM_STATE_ARMED_HOME"); case enums::ALARM_STATE_ARMED_AWAY: - return "ALARM_STATE_ARMED_AWAY"; + return ESPHOME_PSTR("ALARM_STATE_ARMED_AWAY"); case enums::ALARM_STATE_ARMED_NIGHT: - return "ALARM_STATE_ARMED_NIGHT"; + return ESPHOME_PSTR("ALARM_STATE_ARMED_NIGHT"); case enums::ALARM_STATE_ARMED_VACATION: - return "ALARM_STATE_ARMED_VACATION"; + return ESPHOME_PSTR("ALARM_STATE_ARMED_VACATION"); case enums::ALARM_STATE_ARMED_CUSTOM_BYPASS: - return "ALARM_STATE_ARMED_CUSTOM_BYPASS"; + return ESPHOME_PSTR("ALARM_STATE_ARMED_CUSTOM_BYPASS"); case enums::ALARM_STATE_PENDING: - return "ALARM_STATE_PENDING"; + return ESPHOME_PSTR("ALARM_STATE_PENDING"); case enums::ALARM_STATE_ARMING: - return "ALARM_STATE_ARMING"; + return ESPHOME_PSTR("ALARM_STATE_ARMING"); case enums::ALARM_STATE_DISARMING: - return "ALARM_STATE_DISARMING"; + return ESPHOME_PSTR("ALARM_STATE_DISARMING"); case enums::ALARM_STATE_TRIGGERED: - return "ALARM_STATE_TRIGGERED"; + return ESPHOME_PSTR("ALARM_STATE_TRIGGERED"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } template<> const char *proto_enum_to_string(enums::AlarmControlPanelStateCommand value) { switch (value) { case enums::ALARM_CONTROL_PANEL_DISARM: - return "ALARM_CONTROL_PANEL_DISARM"; + return ESPHOME_PSTR("ALARM_CONTROL_PANEL_DISARM"); case enums::ALARM_CONTROL_PANEL_ARM_AWAY: - return "ALARM_CONTROL_PANEL_ARM_AWAY"; + return ESPHOME_PSTR("ALARM_CONTROL_PANEL_ARM_AWAY"); case enums::ALARM_CONTROL_PANEL_ARM_HOME: - return "ALARM_CONTROL_PANEL_ARM_HOME"; + return ESPHOME_PSTR("ALARM_CONTROL_PANEL_ARM_HOME"); case enums::ALARM_CONTROL_PANEL_ARM_NIGHT: - return "ALARM_CONTROL_PANEL_ARM_NIGHT"; + return ESPHOME_PSTR("ALARM_CONTROL_PANEL_ARM_NIGHT"); case enums::ALARM_CONTROL_PANEL_ARM_VACATION: - return "ALARM_CONTROL_PANEL_ARM_VACATION"; + return ESPHOME_PSTR("ALARM_CONTROL_PANEL_ARM_VACATION"); case enums::ALARM_CONTROL_PANEL_ARM_CUSTOM_BYPASS: - return "ALARM_CONTROL_PANEL_ARM_CUSTOM_BYPASS"; + return ESPHOME_PSTR("ALARM_CONTROL_PANEL_ARM_CUSTOM_BYPASS"); case enums::ALARM_CONTROL_PANEL_TRIGGER: - return "ALARM_CONTROL_PANEL_TRIGGER"; + return ESPHOME_PSTR("ALARM_CONTROL_PANEL_TRIGGER"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } #endif @@ -714,11 +739,11 @@ const char *proto_enum_to_string(enums::Al template<> const char *proto_enum_to_string(enums::TextMode value) { switch (value) { case enums::TEXT_MODE_TEXT: - return "TEXT_MODE_TEXT"; + return ESPHOME_PSTR("TEXT_MODE_TEXT"); case enums::TEXT_MODE_PASSWORD: - return "TEXT_MODE_PASSWORD"; + return ESPHOME_PSTR("TEXT_MODE_PASSWORD"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } #endif @@ -726,13 +751,13 @@ template<> const char *proto_enum_to_string(enums::TextMode val template<> const char *proto_enum_to_string(enums::ValveOperation value) { switch (value) { case enums::VALVE_OPERATION_IDLE: - return "VALVE_OPERATION_IDLE"; + return ESPHOME_PSTR("VALVE_OPERATION_IDLE"); case enums::VALVE_OPERATION_IS_OPENING: - return "VALVE_OPERATION_IS_OPENING"; + return ESPHOME_PSTR("VALVE_OPERATION_IS_OPENING"); case enums::VALVE_OPERATION_IS_CLOSING: - return "VALVE_OPERATION_IS_CLOSING"; + return ESPHOME_PSTR("VALVE_OPERATION_IS_CLOSING"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } #endif @@ -740,13 +765,13 @@ template<> const char *proto_enum_to_string(enums::ValveO template<> const char *proto_enum_to_string(enums::UpdateCommand value) { switch (value) { case enums::UPDATE_COMMAND_NONE: - return "UPDATE_COMMAND_NONE"; + return ESPHOME_PSTR("UPDATE_COMMAND_NONE"); case enums::UPDATE_COMMAND_UPDATE: - return "UPDATE_COMMAND_UPDATE"; + return ESPHOME_PSTR("UPDATE_COMMAND_UPDATE"); case enums::UPDATE_COMMAND_CHECK: - return "UPDATE_COMMAND_CHECK"; + return ESPHOME_PSTR("UPDATE_COMMAND_CHECK"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } #endif @@ -754,13 +779,13 @@ template<> const char *proto_enum_to_string(enums::UpdateC template<> const char *proto_enum_to_string(enums::ZWaveProxyRequestType value) { switch (value) { case enums::ZWAVE_PROXY_REQUEST_TYPE_SUBSCRIBE: - return "ZWAVE_PROXY_REQUEST_TYPE_SUBSCRIBE"; + return ESPHOME_PSTR("ZWAVE_PROXY_REQUEST_TYPE_SUBSCRIBE"); case enums::ZWAVE_PROXY_REQUEST_TYPE_UNSUBSCRIBE: - return "ZWAVE_PROXY_REQUEST_TYPE_UNSUBSCRIBE"; + return ESPHOME_PSTR("ZWAVE_PROXY_REQUEST_TYPE_UNSUBSCRIBE"); case enums::ZWAVE_PROXY_REQUEST_TYPE_HOME_ID_CHANGE: - return "ZWAVE_PROXY_REQUEST_TYPE_HOME_ID_CHANGE"; + return ESPHOME_PSTR("ZWAVE_PROXY_REQUEST_TYPE_HOME_ID_CHANGE"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } #endif @@ -768,165 +793,165 @@ template<> const char *proto_enum_to_string(enums: template<> const char *proto_enum_to_string(enums::SerialProxyParity value) { switch (value) { case enums::SERIAL_PROXY_PARITY_NONE: - return "SERIAL_PROXY_PARITY_NONE"; + return ESPHOME_PSTR("SERIAL_PROXY_PARITY_NONE"); case enums::SERIAL_PROXY_PARITY_EVEN: - return "SERIAL_PROXY_PARITY_EVEN"; + return ESPHOME_PSTR("SERIAL_PROXY_PARITY_EVEN"); case enums::SERIAL_PROXY_PARITY_ODD: - return "SERIAL_PROXY_PARITY_ODD"; + return ESPHOME_PSTR("SERIAL_PROXY_PARITY_ODD"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } template<> const char *proto_enum_to_string(enums::SerialProxyRequestType value) { switch (value) { case enums::SERIAL_PROXY_REQUEST_TYPE_SUBSCRIBE: - return "SERIAL_PROXY_REQUEST_TYPE_SUBSCRIBE"; + return ESPHOME_PSTR("SERIAL_PROXY_REQUEST_TYPE_SUBSCRIBE"); case enums::SERIAL_PROXY_REQUEST_TYPE_UNSUBSCRIBE: - return "SERIAL_PROXY_REQUEST_TYPE_UNSUBSCRIBE"; + return ESPHOME_PSTR("SERIAL_PROXY_REQUEST_TYPE_UNSUBSCRIBE"); case enums::SERIAL_PROXY_REQUEST_TYPE_FLUSH: - return "SERIAL_PROXY_REQUEST_TYPE_FLUSH"; + return ESPHOME_PSTR("SERIAL_PROXY_REQUEST_TYPE_FLUSH"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } template<> const char *proto_enum_to_string(enums::SerialProxyStatus value) { switch (value) { case enums::SERIAL_PROXY_STATUS_OK: - return "SERIAL_PROXY_STATUS_OK"; + return ESPHOME_PSTR("SERIAL_PROXY_STATUS_OK"); case enums::SERIAL_PROXY_STATUS_ASSUMED_SUCCESS: - return "SERIAL_PROXY_STATUS_ASSUMED_SUCCESS"; + return ESPHOME_PSTR("SERIAL_PROXY_STATUS_ASSUMED_SUCCESS"); case enums::SERIAL_PROXY_STATUS_ERROR: - return "SERIAL_PROXY_STATUS_ERROR"; + return ESPHOME_PSTR("SERIAL_PROXY_STATUS_ERROR"); case enums::SERIAL_PROXY_STATUS_TIMEOUT: - return "SERIAL_PROXY_STATUS_TIMEOUT"; + return ESPHOME_PSTR("SERIAL_PROXY_STATUS_TIMEOUT"); case enums::SERIAL_PROXY_STATUS_NOT_SUPPORTED: - return "SERIAL_PROXY_STATUS_NOT_SUPPORTED"; + return ESPHOME_PSTR("SERIAL_PROXY_STATUS_NOT_SUPPORTED"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } #endif const char *HelloRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "HelloRequest"); - dump_field(out, "client_info", this->client_info); - dump_field(out, "api_version_major", this->api_version_major); - dump_field(out, "api_version_minor", this->api_version_minor); + MessageDumpHelper helper(out, ESPHOME_PSTR("HelloRequest")); + dump_field(out, ESPHOME_PSTR("client_info"), this->client_info); + dump_field(out, ESPHOME_PSTR("api_version_major"), this->api_version_major); + dump_field(out, ESPHOME_PSTR("api_version_minor"), this->api_version_minor); return out.c_str(); } const char *HelloResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "HelloResponse"); - dump_field(out, "api_version_major", this->api_version_major); - dump_field(out, "api_version_minor", this->api_version_minor); - dump_field(out, "server_info", this->server_info); - dump_field(out, "name", this->name); + MessageDumpHelper helper(out, ESPHOME_PSTR("HelloResponse")); + dump_field(out, ESPHOME_PSTR("api_version_major"), this->api_version_major); + dump_field(out, ESPHOME_PSTR("api_version_minor"), this->api_version_minor); + dump_field(out, ESPHOME_PSTR("server_info"), this->server_info); + dump_field(out, ESPHOME_PSTR("name"), this->name); return out.c_str(); } const char *DisconnectRequest::dump_to(DumpBuffer &out) const { - out.append("DisconnectRequest {}"); + out.append_p(ESPHOME_PSTR("DisconnectRequest {}")); return out.c_str(); } const char *DisconnectResponse::dump_to(DumpBuffer &out) const { - out.append("DisconnectResponse {}"); + out.append_p(ESPHOME_PSTR("DisconnectResponse {}")); return out.c_str(); } const char *PingRequest::dump_to(DumpBuffer &out) const { - out.append("PingRequest {}"); + out.append_p(ESPHOME_PSTR("PingRequest {}")); return out.c_str(); } const char *PingResponse::dump_to(DumpBuffer &out) const { - out.append("PingResponse {}"); + out.append_p(ESPHOME_PSTR("PingResponse {}")); return out.c_str(); } #ifdef USE_AREAS const char *AreaInfo::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "AreaInfo"); - dump_field(out, "area_id", this->area_id); - dump_field(out, "name", this->name); + MessageDumpHelper helper(out, ESPHOME_PSTR("AreaInfo")); + dump_field(out, ESPHOME_PSTR("area_id"), this->area_id); + dump_field(out, ESPHOME_PSTR("name"), this->name); return out.c_str(); } #endif #ifdef USE_DEVICES const char *DeviceInfo::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "DeviceInfo"); - dump_field(out, "device_id", this->device_id); - dump_field(out, "name", this->name); - dump_field(out, "area_id", this->area_id); + MessageDumpHelper helper(out, ESPHOME_PSTR("DeviceInfo")); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); + dump_field(out, ESPHOME_PSTR("name"), this->name); + dump_field(out, ESPHOME_PSTR("area_id"), this->area_id); return out.c_str(); } #endif #ifdef USE_SERIAL_PROXY const char *SerialProxyInfo::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "SerialProxyInfo"); - dump_field(out, "name", this->name); - dump_field(out, "port_type", static_cast(this->port_type)); + MessageDumpHelper helper(out, ESPHOME_PSTR("SerialProxyInfo")); + dump_field(out, ESPHOME_PSTR("name"), this->name); + dump_field(out, ESPHOME_PSTR("port_type"), static_cast(this->port_type)); return out.c_str(); } #endif const char *DeviceInfoResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "DeviceInfoResponse"); - dump_field(out, "name", this->name); - dump_field(out, "mac_address", this->mac_address); - dump_field(out, "esphome_version", this->esphome_version); - dump_field(out, "compilation_time", this->compilation_time); - dump_field(out, "model", this->model); + MessageDumpHelper helper(out, ESPHOME_PSTR("DeviceInfoResponse")); + dump_field(out, ESPHOME_PSTR("name"), this->name); + dump_field(out, ESPHOME_PSTR("mac_address"), this->mac_address); + dump_field(out, ESPHOME_PSTR("esphome_version"), this->esphome_version); + dump_field(out, ESPHOME_PSTR("compilation_time"), this->compilation_time); + dump_field(out, ESPHOME_PSTR("model"), this->model); #ifdef USE_DEEP_SLEEP - dump_field(out, "has_deep_sleep", this->has_deep_sleep); + dump_field(out, ESPHOME_PSTR("has_deep_sleep"), this->has_deep_sleep); #endif #ifdef ESPHOME_PROJECT_NAME - dump_field(out, "project_name", this->project_name); + dump_field(out, ESPHOME_PSTR("project_name"), this->project_name); #endif #ifdef ESPHOME_PROJECT_NAME - dump_field(out, "project_version", this->project_version); + dump_field(out, ESPHOME_PSTR("project_version"), this->project_version); #endif #ifdef USE_WEBSERVER - dump_field(out, "webserver_port", this->webserver_port); + dump_field(out, ESPHOME_PSTR("webserver_port"), this->webserver_port); #endif #ifdef USE_BLUETOOTH_PROXY - dump_field(out, "bluetooth_proxy_feature_flags", this->bluetooth_proxy_feature_flags); + dump_field(out, ESPHOME_PSTR("bluetooth_proxy_feature_flags"), this->bluetooth_proxy_feature_flags); #endif - dump_field(out, "manufacturer", this->manufacturer); - dump_field(out, "friendly_name", this->friendly_name); + dump_field(out, ESPHOME_PSTR("manufacturer"), this->manufacturer); + dump_field(out, ESPHOME_PSTR("friendly_name"), this->friendly_name); #ifdef USE_VOICE_ASSISTANT - dump_field(out, "voice_assistant_feature_flags", this->voice_assistant_feature_flags); + dump_field(out, ESPHOME_PSTR("voice_assistant_feature_flags"), this->voice_assistant_feature_flags); #endif #ifdef USE_AREAS - dump_field(out, "suggested_area", this->suggested_area); + dump_field(out, ESPHOME_PSTR("suggested_area"), this->suggested_area); #endif #ifdef USE_BLUETOOTH_PROXY - dump_field(out, "bluetooth_mac_address", this->bluetooth_mac_address); + dump_field(out, ESPHOME_PSTR("bluetooth_mac_address"), this->bluetooth_mac_address); #endif #ifdef USE_API_NOISE - dump_field(out, "api_encryption_supported", this->api_encryption_supported); + dump_field(out, ESPHOME_PSTR("api_encryption_supported"), this->api_encryption_supported); #endif #ifdef USE_DEVICES for (const auto &it : this->devices) { - out.append(" devices: "); + out.append(4, ' ').append_p(ESPHOME_PSTR("devices")).append(": "); it.dump_to(out); out.append("\n"); } #endif #ifdef USE_AREAS for (const auto &it : this->areas) { - out.append(" areas: "); + out.append(4, ' ').append_p(ESPHOME_PSTR("areas")).append(": "); it.dump_to(out); out.append("\n"); } #endif #ifdef USE_AREAS - out.append(" area: "); + out.append(2, ' ').append_p(ESPHOME_PSTR("area")).append(": "); this->area.dump_to(out); out.append("\n"); #endif #ifdef USE_ZWAVE_PROXY - dump_field(out, "zwave_proxy_feature_flags", this->zwave_proxy_feature_flags); + dump_field(out, ESPHOME_PSTR("zwave_proxy_feature_flags"), this->zwave_proxy_feature_flags); #endif #ifdef USE_ZWAVE_PROXY - dump_field(out, "zwave_home_id", this->zwave_home_id); + dump_field(out, ESPHOME_PSTR("zwave_home_id"), this->zwave_home_id); #endif #ifdef USE_SERIAL_PROXY for (const auto &it : this->serial_proxies) { - out.append(" serial_proxies: "); + out.append(4, ' ').append_p(ESPHOME_PSTR("serial_proxies")).append(": "); it.dump_to(out); out.append("\n"); } @@ -934,1720 +959,1720 @@ const char *DeviceInfoResponse::dump_to(DumpBuffer &out) const { return out.c_str(); } const char *ListEntitiesDoneResponse::dump_to(DumpBuffer &out) const { - out.append("ListEntitiesDoneResponse {}"); + out.append_p(ESPHOME_PSTR("ListEntitiesDoneResponse {}")); return out.c_str(); } #ifdef USE_BINARY_SENSOR const char *ListEntitiesBinarySensorResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesBinarySensorResponse"); - dump_field(out, "object_id", this->object_id); - dump_field(out, "key", this->key); - dump_field(out, "name", this->name); - dump_field(out, "device_class", this->device_class); - dump_field(out, "is_status_binary_sensor", this->is_status_binary_sensor); - dump_field(out, "disabled_by_default", this->disabled_by_default); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesBinarySensorResponse")); + dump_field(out, ESPHOME_PSTR("object_id"), this->object_id); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("name"), this->name); + dump_field(out, ESPHOME_PSTR("device_class"), this->device_class); + dump_field(out, ESPHOME_PSTR("is_status_binary_sensor"), this->is_status_binary_sensor); + dump_field(out, ESPHOME_PSTR("disabled_by_default"), this->disabled_by_default); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon); + dump_field(out, ESPHOME_PSTR("icon"), this->icon); #endif - dump_field(out, "entity_category", static_cast(this->entity_category)); + dump_field(out, ESPHOME_PSTR("entity_category"), static_cast(this->entity_category)); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *BinarySensorStateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BinarySensorStateResponse"); - dump_field(out, "key", this->key); - dump_field(out, "state", this->state); - dump_field(out, "missing_state", this->missing_state); + MessageDumpHelper helper(out, ESPHOME_PSTR("BinarySensorStateResponse")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("state"), this->state); + dump_field(out, ESPHOME_PSTR("missing_state"), this->missing_state); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } #endif #ifdef USE_COVER const char *ListEntitiesCoverResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesCoverResponse"); - dump_field(out, "object_id", this->object_id); - dump_field(out, "key", this->key); - dump_field(out, "name", this->name); - dump_field(out, "assumed_state", this->assumed_state); - dump_field(out, "supports_position", this->supports_position); - dump_field(out, "supports_tilt", this->supports_tilt); - dump_field(out, "device_class", this->device_class); - dump_field(out, "disabled_by_default", this->disabled_by_default); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesCoverResponse")); + dump_field(out, ESPHOME_PSTR("object_id"), this->object_id); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("name"), this->name); + dump_field(out, ESPHOME_PSTR("assumed_state"), this->assumed_state); + dump_field(out, ESPHOME_PSTR("supports_position"), this->supports_position); + dump_field(out, ESPHOME_PSTR("supports_tilt"), this->supports_tilt); + dump_field(out, ESPHOME_PSTR("device_class"), this->device_class); + dump_field(out, ESPHOME_PSTR("disabled_by_default"), this->disabled_by_default); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon); + dump_field(out, ESPHOME_PSTR("icon"), this->icon); #endif - dump_field(out, "entity_category", static_cast(this->entity_category)); - dump_field(out, "supports_stop", this->supports_stop); + dump_field(out, ESPHOME_PSTR("entity_category"), static_cast(this->entity_category)); + dump_field(out, ESPHOME_PSTR("supports_stop"), this->supports_stop); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *CoverStateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "CoverStateResponse"); - dump_field(out, "key", this->key); - dump_field(out, "position", this->position); - dump_field(out, "tilt", this->tilt); - dump_field(out, "current_operation", static_cast(this->current_operation)); + MessageDumpHelper helper(out, ESPHOME_PSTR("CoverStateResponse")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("position"), this->position); + dump_field(out, ESPHOME_PSTR("tilt"), this->tilt); + dump_field(out, ESPHOME_PSTR("current_operation"), static_cast(this->current_operation)); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *CoverCommandRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "CoverCommandRequest"); - dump_field(out, "key", this->key); - dump_field(out, "has_position", this->has_position); - dump_field(out, "position", this->position); - dump_field(out, "has_tilt", this->has_tilt); - dump_field(out, "tilt", this->tilt); - dump_field(out, "stop", this->stop); + MessageDumpHelper helper(out, ESPHOME_PSTR("CoverCommandRequest")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("has_position"), this->has_position); + dump_field(out, ESPHOME_PSTR("position"), this->position); + dump_field(out, ESPHOME_PSTR("has_tilt"), this->has_tilt); + dump_field(out, ESPHOME_PSTR("tilt"), this->tilt); + dump_field(out, ESPHOME_PSTR("stop"), this->stop); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } #endif #ifdef USE_FAN const char *ListEntitiesFanResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesFanResponse"); - dump_field(out, "object_id", this->object_id); - dump_field(out, "key", this->key); - dump_field(out, "name", this->name); - dump_field(out, "supports_oscillation", this->supports_oscillation); - dump_field(out, "supports_speed", this->supports_speed); - dump_field(out, "supports_direction", this->supports_direction); - dump_field(out, "supported_speed_count", this->supported_speed_count); - dump_field(out, "disabled_by_default", this->disabled_by_default); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesFanResponse")); + dump_field(out, ESPHOME_PSTR("object_id"), this->object_id); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("name"), this->name); + dump_field(out, ESPHOME_PSTR("supports_oscillation"), this->supports_oscillation); + dump_field(out, ESPHOME_PSTR("supports_speed"), this->supports_speed); + dump_field(out, ESPHOME_PSTR("supports_direction"), this->supports_direction); + dump_field(out, ESPHOME_PSTR("supported_speed_count"), this->supported_speed_count); + dump_field(out, ESPHOME_PSTR("disabled_by_default"), this->disabled_by_default); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon); + dump_field(out, ESPHOME_PSTR("icon"), this->icon); #endif - dump_field(out, "entity_category", static_cast(this->entity_category)); + dump_field(out, ESPHOME_PSTR("entity_category"), static_cast(this->entity_category)); for (const auto &it : *this->supported_preset_modes) { - dump_field(out, "supported_preset_modes", it, 4); + dump_field(out, ESPHOME_PSTR("supported_preset_modes"), it, 4); } #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *FanStateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "FanStateResponse"); - dump_field(out, "key", this->key); - dump_field(out, "state", this->state); - dump_field(out, "oscillating", this->oscillating); - dump_field(out, "direction", static_cast(this->direction)); - dump_field(out, "speed_level", this->speed_level); - dump_field(out, "preset_mode", this->preset_mode); + MessageDumpHelper helper(out, ESPHOME_PSTR("FanStateResponse")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("state"), this->state); + dump_field(out, ESPHOME_PSTR("oscillating"), this->oscillating); + dump_field(out, ESPHOME_PSTR("direction"), static_cast(this->direction)); + dump_field(out, ESPHOME_PSTR("speed_level"), this->speed_level); + dump_field(out, ESPHOME_PSTR("preset_mode"), this->preset_mode); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *FanCommandRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "FanCommandRequest"); - dump_field(out, "key", this->key); - dump_field(out, "has_state", this->has_state); - dump_field(out, "state", this->state); - dump_field(out, "has_oscillating", this->has_oscillating); - dump_field(out, "oscillating", this->oscillating); - dump_field(out, "has_direction", this->has_direction); - dump_field(out, "direction", static_cast(this->direction)); - dump_field(out, "has_speed_level", this->has_speed_level); - dump_field(out, "speed_level", this->speed_level); - dump_field(out, "has_preset_mode", this->has_preset_mode); - dump_field(out, "preset_mode", this->preset_mode); + MessageDumpHelper helper(out, ESPHOME_PSTR("FanCommandRequest")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("has_state"), this->has_state); + dump_field(out, ESPHOME_PSTR("state"), this->state); + dump_field(out, ESPHOME_PSTR("has_oscillating"), this->has_oscillating); + dump_field(out, ESPHOME_PSTR("oscillating"), this->oscillating); + dump_field(out, ESPHOME_PSTR("has_direction"), this->has_direction); + dump_field(out, ESPHOME_PSTR("direction"), static_cast(this->direction)); + dump_field(out, ESPHOME_PSTR("has_speed_level"), this->has_speed_level); + dump_field(out, ESPHOME_PSTR("speed_level"), this->speed_level); + dump_field(out, ESPHOME_PSTR("has_preset_mode"), this->has_preset_mode); + dump_field(out, ESPHOME_PSTR("preset_mode"), this->preset_mode); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } #endif #ifdef USE_LIGHT const char *ListEntitiesLightResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesLightResponse"); - dump_field(out, "object_id", this->object_id); - dump_field(out, "key", this->key); - dump_field(out, "name", this->name); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesLightResponse")); + dump_field(out, ESPHOME_PSTR("object_id"), this->object_id); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("name"), this->name); for (const auto &it : *this->supported_color_modes) { - dump_field(out, "supported_color_modes", static_cast(it), 4); + dump_field(out, ESPHOME_PSTR("supported_color_modes"), static_cast(it), 4); } - dump_field(out, "min_mireds", this->min_mireds); - dump_field(out, "max_mireds", this->max_mireds); + dump_field(out, ESPHOME_PSTR("min_mireds"), this->min_mireds); + dump_field(out, ESPHOME_PSTR("max_mireds"), this->max_mireds); for (const auto &it : *this->effects) { - dump_field(out, "effects", it, 4); + dump_field(out, ESPHOME_PSTR("effects"), it, 4); } - dump_field(out, "disabled_by_default", this->disabled_by_default); + dump_field(out, ESPHOME_PSTR("disabled_by_default"), this->disabled_by_default); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon); + dump_field(out, ESPHOME_PSTR("icon"), this->icon); #endif - dump_field(out, "entity_category", static_cast(this->entity_category)); + dump_field(out, ESPHOME_PSTR("entity_category"), static_cast(this->entity_category)); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *LightStateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "LightStateResponse"); - dump_field(out, "key", this->key); - dump_field(out, "state", this->state); - dump_field(out, "brightness", this->brightness); - dump_field(out, "color_mode", static_cast(this->color_mode)); - dump_field(out, "color_brightness", this->color_brightness); - dump_field(out, "red", this->red); - dump_field(out, "green", this->green); - dump_field(out, "blue", this->blue); - dump_field(out, "white", this->white); - dump_field(out, "color_temperature", this->color_temperature); - dump_field(out, "cold_white", this->cold_white); - dump_field(out, "warm_white", this->warm_white); - dump_field(out, "effect", this->effect); + MessageDumpHelper helper(out, ESPHOME_PSTR("LightStateResponse")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("state"), this->state); + dump_field(out, ESPHOME_PSTR("brightness"), this->brightness); + dump_field(out, ESPHOME_PSTR("color_mode"), static_cast(this->color_mode)); + dump_field(out, ESPHOME_PSTR("color_brightness"), this->color_brightness); + dump_field(out, ESPHOME_PSTR("red"), this->red); + dump_field(out, ESPHOME_PSTR("green"), this->green); + dump_field(out, ESPHOME_PSTR("blue"), this->blue); + dump_field(out, ESPHOME_PSTR("white"), this->white); + dump_field(out, ESPHOME_PSTR("color_temperature"), this->color_temperature); + dump_field(out, ESPHOME_PSTR("cold_white"), this->cold_white); + dump_field(out, ESPHOME_PSTR("warm_white"), this->warm_white); + dump_field(out, ESPHOME_PSTR("effect"), this->effect); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *LightCommandRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "LightCommandRequest"); - dump_field(out, "key", this->key); - dump_field(out, "has_state", this->has_state); - dump_field(out, "state", this->state); - dump_field(out, "has_brightness", this->has_brightness); - dump_field(out, "brightness", this->brightness); - dump_field(out, "has_color_mode", this->has_color_mode); - dump_field(out, "color_mode", static_cast(this->color_mode)); - dump_field(out, "has_color_brightness", this->has_color_brightness); - dump_field(out, "color_brightness", this->color_brightness); - dump_field(out, "has_rgb", this->has_rgb); - dump_field(out, "red", this->red); - dump_field(out, "green", this->green); - dump_field(out, "blue", this->blue); - dump_field(out, "has_white", this->has_white); - dump_field(out, "white", this->white); - dump_field(out, "has_color_temperature", this->has_color_temperature); - dump_field(out, "color_temperature", this->color_temperature); - dump_field(out, "has_cold_white", this->has_cold_white); - dump_field(out, "cold_white", this->cold_white); - dump_field(out, "has_warm_white", this->has_warm_white); - dump_field(out, "warm_white", this->warm_white); - dump_field(out, "has_transition_length", this->has_transition_length); - dump_field(out, "transition_length", this->transition_length); - dump_field(out, "has_flash_length", this->has_flash_length); - dump_field(out, "flash_length", this->flash_length); - dump_field(out, "has_effect", this->has_effect); - dump_field(out, "effect", this->effect); + MessageDumpHelper helper(out, ESPHOME_PSTR("LightCommandRequest")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("has_state"), this->has_state); + dump_field(out, ESPHOME_PSTR("state"), this->state); + dump_field(out, ESPHOME_PSTR("has_brightness"), this->has_brightness); + dump_field(out, ESPHOME_PSTR("brightness"), this->brightness); + dump_field(out, ESPHOME_PSTR("has_color_mode"), this->has_color_mode); + dump_field(out, ESPHOME_PSTR("color_mode"), static_cast(this->color_mode)); + dump_field(out, ESPHOME_PSTR("has_color_brightness"), this->has_color_brightness); + dump_field(out, ESPHOME_PSTR("color_brightness"), this->color_brightness); + dump_field(out, ESPHOME_PSTR("has_rgb"), this->has_rgb); + dump_field(out, ESPHOME_PSTR("red"), this->red); + dump_field(out, ESPHOME_PSTR("green"), this->green); + dump_field(out, ESPHOME_PSTR("blue"), this->blue); + dump_field(out, ESPHOME_PSTR("has_white"), this->has_white); + dump_field(out, ESPHOME_PSTR("white"), this->white); + dump_field(out, ESPHOME_PSTR("has_color_temperature"), this->has_color_temperature); + dump_field(out, ESPHOME_PSTR("color_temperature"), this->color_temperature); + dump_field(out, ESPHOME_PSTR("has_cold_white"), this->has_cold_white); + dump_field(out, ESPHOME_PSTR("cold_white"), this->cold_white); + dump_field(out, ESPHOME_PSTR("has_warm_white"), this->has_warm_white); + dump_field(out, ESPHOME_PSTR("warm_white"), this->warm_white); + dump_field(out, ESPHOME_PSTR("has_transition_length"), this->has_transition_length); + dump_field(out, ESPHOME_PSTR("transition_length"), this->transition_length); + dump_field(out, ESPHOME_PSTR("has_flash_length"), this->has_flash_length); + dump_field(out, ESPHOME_PSTR("flash_length"), this->flash_length); + dump_field(out, ESPHOME_PSTR("has_effect"), this->has_effect); + dump_field(out, ESPHOME_PSTR("effect"), this->effect); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } #endif #ifdef USE_SENSOR const char *ListEntitiesSensorResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesSensorResponse"); - dump_field(out, "object_id", this->object_id); - dump_field(out, "key", this->key); - dump_field(out, "name", this->name); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesSensorResponse")); + dump_field(out, ESPHOME_PSTR("object_id"), this->object_id); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("name"), this->name); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon); + dump_field(out, ESPHOME_PSTR("icon"), this->icon); #endif - dump_field(out, "unit_of_measurement", this->unit_of_measurement); - dump_field(out, "accuracy_decimals", this->accuracy_decimals); - dump_field(out, "force_update", this->force_update); - dump_field(out, "device_class", this->device_class); - dump_field(out, "state_class", static_cast(this->state_class)); - dump_field(out, "disabled_by_default", this->disabled_by_default); - dump_field(out, "entity_category", static_cast(this->entity_category)); + dump_field(out, ESPHOME_PSTR("unit_of_measurement"), this->unit_of_measurement); + dump_field(out, ESPHOME_PSTR("accuracy_decimals"), this->accuracy_decimals); + dump_field(out, ESPHOME_PSTR("force_update"), this->force_update); + dump_field(out, ESPHOME_PSTR("device_class"), this->device_class); + dump_field(out, ESPHOME_PSTR("state_class"), static_cast(this->state_class)); + dump_field(out, ESPHOME_PSTR("disabled_by_default"), this->disabled_by_default); + dump_field(out, ESPHOME_PSTR("entity_category"), static_cast(this->entity_category)); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *SensorStateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "SensorStateResponse"); - dump_field(out, "key", this->key); - dump_field(out, "state", this->state); - dump_field(out, "missing_state", this->missing_state); + MessageDumpHelper helper(out, ESPHOME_PSTR("SensorStateResponse")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("state"), this->state); + dump_field(out, ESPHOME_PSTR("missing_state"), this->missing_state); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } #endif #ifdef USE_SWITCH const char *ListEntitiesSwitchResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesSwitchResponse"); - dump_field(out, "object_id", this->object_id); - dump_field(out, "key", this->key); - dump_field(out, "name", this->name); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesSwitchResponse")); + dump_field(out, ESPHOME_PSTR("object_id"), this->object_id); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("name"), this->name); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon); + dump_field(out, ESPHOME_PSTR("icon"), this->icon); #endif - dump_field(out, "assumed_state", this->assumed_state); - dump_field(out, "disabled_by_default", this->disabled_by_default); - dump_field(out, "entity_category", static_cast(this->entity_category)); - dump_field(out, "device_class", this->device_class); + dump_field(out, ESPHOME_PSTR("assumed_state"), this->assumed_state); + dump_field(out, ESPHOME_PSTR("disabled_by_default"), this->disabled_by_default); + dump_field(out, ESPHOME_PSTR("entity_category"), static_cast(this->entity_category)); + dump_field(out, ESPHOME_PSTR("device_class"), this->device_class); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *SwitchStateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "SwitchStateResponse"); - dump_field(out, "key", this->key); - dump_field(out, "state", this->state); + MessageDumpHelper helper(out, ESPHOME_PSTR("SwitchStateResponse")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("state"), this->state); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *SwitchCommandRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "SwitchCommandRequest"); - dump_field(out, "key", this->key); - dump_field(out, "state", this->state); + MessageDumpHelper helper(out, ESPHOME_PSTR("SwitchCommandRequest")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("state"), this->state); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } #endif #ifdef USE_TEXT_SENSOR const char *ListEntitiesTextSensorResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesTextSensorResponse"); - dump_field(out, "object_id", this->object_id); - dump_field(out, "key", this->key); - dump_field(out, "name", this->name); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesTextSensorResponse")); + dump_field(out, ESPHOME_PSTR("object_id"), this->object_id); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("name"), this->name); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon); + dump_field(out, ESPHOME_PSTR("icon"), this->icon); #endif - dump_field(out, "disabled_by_default", this->disabled_by_default); - dump_field(out, "entity_category", static_cast(this->entity_category)); - dump_field(out, "device_class", this->device_class); + dump_field(out, ESPHOME_PSTR("disabled_by_default"), this->disabled_by_default); + dump_field(out, ESPHOME_PSTR("entity_category"), static_cast(this->entity_category)); + dump_field(out, ESPHOME_PSTR("device_class"), this->device_class); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *TextSensorStateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "TextSensorStateResponse"); - dump_field(out, "key", this->key); - dump_field(out, "state", this->state); - dump_field(out, "missing_state", this->missing_state); + MessageDumpHelper helper(out, ESPHOME_PSTR("TextSensorStateResponse")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("state"), this->state); + dump_field(out, ESPHOME_PSTR("missing_state"), this->missing_state); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } #endif const char *SubscribeLogsRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "SubscribeLogsRequest"); - dump_field(out, "level", static_cast(this->level)); - dump_field(out, "dump_config", this->dump_config); + MessageDumpHelper helper(out, ESPHOME_PSTR("SubscribeLogsRequest")); + dump_field(out, ESPHOME_PSTR("level"), static_cast(this->level)); + dump_field(out, ESPHOME_PSTR("dump_config"), this->dump_config); return out.c_str(); } const char *SubscribeLogsResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "SubscribeLogsResponse"); - dump_field(out, "level", static_cast(this->level)); - dump_bytes_field(out, "message", this->message_ptr_, this->message_len_); + MessageDumpHelper helper(out, ESPHOME_PSTR("SubscribeLogsResponse")); + dump_field(out, ESPHOME_PSTR("level"), static_cast(this->level)); + dump_bytes_field(out, ESPHOME_PSTR("message"), this->message_ptr_, this->message_len_); return out.c_str(); } #ifdef USE_API_NOISE const char *NoiseEncryptionSetKeyRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "NoiseEncryptionSetKeyRequest"); - dump_bytes_field(out, "key", this->key, this->key_len); + MessageDumpHelper helper(out, ESPHOME_PSTR("NoiseEncryptionSetKeyRequest")); + dump_bytes_field(out, ESPHOME_PSTR("key"), this->key, this->key_len); return out.c_str(); } const char *NoiseEncryptionSetKeyResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "NoiseEncryptionSetKeyResponse"); - dump_field(out, "success", this->success); + MessageDumpHelper helper(out, ESPHOME_PSTR("NoiseEncryptionSetKeyResponse")); + dump_field(out, ESPHOME_PSTR("success"), this->success); return out.c_str(); } #endif #ifdef USE_API_HOMEASSISTANT_SERVICES const char *HomeassistantServiceMap::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "HomeassistantServiceMap"); - dump_field(out, "key", this->key); - dump_field(out, "value", this->value); + MessageDumpHelper helper(out, ESPHOME_PSTR("HomeassistantServiceMap")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("value"), this->value); return out.c_str(); } const char *HomeassistantActionRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "HomeassistantActionRequest"); - dump_field(out, "service", this->service); + MessageDumpHelper helper(out, ESPHOME_PSTR("HomeassistantActionRequest")); + dump_field(out, ESPHOME_PSTR("service"), this->service); for (const auto &it : this->data) { - out.append(" data: "); + out.append(4, ' ').append_p(ESPHOME_PSTR("data")).append(": "); it.dump_to(out); out.append("\n"); } for (const auto &it : this->data_template) { - out.append(" data_template: "); + out.append(4, ' ').append_p(ESPHOME_PSTR("data_template")).append(": "); it.dump_to(out); out.append("\n"); } for (const auto &it : this->variables) { - out.append(" variables: "); + out.append(4, ' ').append_p(ESPHOME_PSTR("variables")).append(": "); it.dump_to(out); out.append("\n"); } - dump_field(out, "is_event", this->is_event); + dump_field(out, ESPHOME_PSTR("is_event"), this->is_event); #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES - dump_field(out, "call_id", this->call_id); + dump_field(out, ESPHOME_PSTR("call_id"), this->call_id); #endif #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON - dump_field(out, "wants_response", this->wants_response); + dump_field(out, ESPHOME_PSTR("wants_response"), this->wants_response); #endif #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON - dump_field(out, "response_template", this->response_template); + dump_field(out, ESPHOME_PSTR("response_template"), this->response_template); #endif return out.c_str(); } #endif #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES const char *HomeassistantActionResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "HomeassistantActionResponse"); - dump_field(out, "call_id", this->call_id); - dump_field(out, "success", this->success); - dump_field(out, "error_message", this->error_message); + MessageDumpHelper helper(out, ESPHOME_PSTR("HomeassistantActionResponse")); + dump_field(out, ESPHOME_PSTR("call_id"), this->call_id); + dump_field(out, ESPHOME_PSTR("success"), this->success); + dump_field(out, ESPHOME_PSTR("error_message"), this->error_message); #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON - dump_bytes_field(out, "response_data", this->response_data, this->response_data_len); + dump_bytes_field(out, ESPHOME_PSTR("response_data"), this->response_data, this->response_data_len); #endif return out.c_str(); } #endif #ifdef USE_API_HOMEASSISTANT_STATES const char *SubscribeHomeAssistantStateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "SubscribeHomeAssistantStateResponse"); - dump_field(out, "entity_id", this->entity_id); - dump_field(out, "attribute", this->attribute); - dump_field(out, "once", this->once); + MessageDumpHelper helper(out, ESPHOME_PSTR("SubscribeHomeAssistantStateResponse")); + dump_field(out, ESPHOME_PSTR("entity_id"), this->entity_id); + dump_field(out, ESPHOME_PSTR("attribute"), this->attribute); + dump_field(out, ESPHOME_PSTR("once"), this->once); return out.c_str(); } const char *HomeAssistantStateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "HomeAssistantStateResponse"); - dump_field(out, "entity_id", this->entity_id); - dump_field(out, "state", this->state); - dump_field(out, "attribute", this->attribute); + MessageDumpHelper helper(out, ESPHOME_PSTR("HomeAssistantStateResponse")); + dump_field(out, ESPHOME_PSTR("entity_id"), this->entity_id); + dump_field(out, ESPHOME_PSTR("state"), this->state); + dump_field(out, ESPHOME_PSTR("attribute"), this->attribute); return out.c_str(); } #endif const char *GetTimeRequest::dump_to(DumpBuffer &out) const { - out.append("GetTimeRequest {}"); + out.append_p(ESPHOME_PSTR("GetTimeRequest {}")); return out.c_str(); } const char *DSTRule::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "DSTRule"); - dump_field(out, "time_seconds", this->time_seconds); - dump_field(out, "day", this->day); - dump_field(out, "type", static_cast(this->type)); - dump_field(out, "month", this->month); - dump_field(out, "week", this->week); - dump_field(out, "day_of_week", this->day_of_week); + MessageDumpHelper helper(out, ESPHOME_PSTR("DSTRule")); + dump_field(out, ESPHOME_PSTR("time_seconds"), this->time_seconds); + dump_field(out, ESPHOME_PSTR("day"), this->day); + dump_field(out, ESPHOME_PSTR("type"), static_cast(this->type)); + dump_field(out, ESPHOME_PSTR("month"), this->month); + dump_field(out, ESPHOME_PSTR("week"), this->week); + dump_field(out, ESPHOME_PSTR("day_of_week"), this->day_of_week); return out.c_str(); } const char *ParsedTimezone::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ParsedTimezone"); - dump_field(out, "std_offset_seconds", this->std_offset_seconds); - dump_field(out, "dst_offset_seconds", this->dst_offset_seconds); - out.append(" dst_start: "); + MessageDumpHelper helper(out, ESPHOME_PSTR("ParsedTimezone")); + dump_field(out, ESPHOME_PSTR("std_offset_seconds"), this->std_offset_seconds); + dump_field(out, ESPHOME_PSTR("dst_offset_seconds"), this->dst_offset_seconds); + out.append(2, ' ').append_p(ESPHOME_PSTR("dst_start")).append(": "); this->dst_start.dump_to(out); out.append("\n"); - out.append(" dst_end: "); + out.append(2, ' ').append_p(ESPHOME_PSTR("dst_end")).append(": "); this->dst_end.dump_to(out); out.append("\n"); return out.c_str(); } const char *GetTimeResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "GetTimeResponse"); - dump_field(out, "epoch_seconds", this->epoch_seconds); - dump_field(out, "timezone", this->timezone); - out.append(" parsed_timezone: "); + MessageDumpHelper helper(out, ESPHOME_PSTR("GetTimeResponse")); + dump_field(out, ESPHOME_PSTR("epoch_seconds"), this->epoch_seconds); + dump_field(out, ESPHOME_PSTR("timezone"), this->timezone); + out.append(2, ' ').append_p(ESPHOME_PSTR("parsed_timezone")).append(": "); this->parsed_timezone.dump_to(out); out.append("\n"); return out.c_str(); } #ifdef USE_API_USER_DEFINED_ACTIONS const char *ListEntitiesServicesArgument::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesServicesArgument"); - dump_field(out, "name", this->name); - dump_field(out, "type", static_cast(this->type)); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesServicesArgument")); + dump_field(out, ESPHOME_PSTR("name"), this->name); + dump_field(out, ESPHOME_PSTR("type"), static_cast(this->type)); return out.c_str(); } const char *ListEntitiesServicesResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesServicesResponse"); - dump_field(out, "name", this->name); - dump_field(out, "key", this->key); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesServicesResponse")); + dump_field(out, ESPHOME_PSTR("name"), this->name); + dump_field(out, ESPHOME_PSTR("key"), this->key); for (const auto &it : this->args) { - out.append(" args: "); + out.append(4, ' ').append_p(ESPHOME_PSTR("args")).append(": "); it.dump_to(out); out.append("\n"); } - dump_field(out, "supports_response", static_cast(this->supports_response)); + dump_field(out, ESPHOME_PSTR("supports_response"), static_cast(this->supports_response)); return out.c_str(); } const char *ExecuteServiceArgument::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ExecuteServiceArgument"); - dump_field(out, "bool_", this->bool_); - dump_field(out, "legacy_int", this->legacy_int); - dump_field(out, "float_", this->float_); - dump_field(out, "string_", this->string_); - dump_field(out, "int_", this->int_); + MessageDumpHelper helper(out, ESPHOME_PSTR("ExecuteServiceArgument")); + dump_field(out, ESPHOME_PSTR("bool_"), this->bool_); + dump_field(out, ESPHOME_PSTR("legacy_int"), this->legacy_int); + dump_field(out, ESPHOME_PSTR("float_"), this->float_); + dump_field(out, ESPHOME_PSTR("string_"), this->string_); + dump_field(out, ESPHOME_PSTR("int_"), this->int_); for (const auto it : this->bool_array) { - dump_field(out, "bool_array", static_cast(it), 4); + dump_field(out, ESPHOME_PSTR("bool_array"), static_cast(it), 4); } for (const auto &it : this->int_array) { - dump_field(out, "int_array", it, 4); + dump_field(out, ESPHOME_PSTR("int_array"), it, 4); } for (const auto &it : this->float_array) { - dump_field(out, "float_array", it, 4); + dump_field(out, ESPHOME_PSTR("float_array"), it, 4); } for (const auto &it : this->string_array) { - dump_field(out, "string_array", it, 4); + dump_field(out, ESPHOME_PSTR("string_array"), it, 4); } return out.c_str(); } const char *ExecuteServiceRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ExecuteServiceRequest"); - dump_field(out, "key", this->key); + MessageDumpHelper helper(out, ESPHOME_PSTR("ExecuteServiceRequest")); + dump_field(out, ESPHOME_PSTR("key"), this->key); for (const auto &it : this->args) { - out.append(" args: "); + out.append(4, ' ').append_p(ESPHOME_PSTR("args")).append(": "); it.dump_to(out); out.append("\n"); } #ifdef USE_API_USER_DEFINED_ACTION_RESPONSES - dump_field(out, "call_id", this->call_id); + dump_field(out, ESPHOME_PSTR("call_id"), this->call_id); #endif #ifdef USE_API_USER_DEFINED_ACTION_RESPONSES - dump_field(out, "return_response", this->return_response); + dump_field(out, ESPHOME_PSTR("return_response"), this->return_response); #endif return out.c_str(); } #endif #ifdef USE_API_USER_DEFINED_ACTION_RESPONSES const char *ExecuteServiceResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ExecuteServiceResponse"); - dump_field(out, "call_id", this->call_id); - dump_field(out, "success", this->success); - dump_field(out, "error_message", this->error_message); + MessageDumpHelper helper(out, ESPHOME_PSTR("ExecuteServiceResponse")); + dump_field(out, ESPHOME_PSTR("call_id"), this->call_id); + dump_field(out, ESPHOME_PSTR("success"), this->success); + dump_field(out, ESPHOME_PSTR("error_message"), this->error_message); #ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON - dump_bytes_field(out, "response_data", this->response_data, this->response_data_len); + dump_bytes_field(out, ESPHOME_PSTR("response_data"), this->response_data, this->response_data_len); #endif return out.c_str(); } #endif #ifdef USE_CAMERA const char *ListEntitiesCameraResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesCameraResponse"); - dump_field(out, "object_id", this->object_id); - dump_field(out, "key", this->key); - dump_field(out, "name", this->name); - dump_field(out, "disabled_by_default", this->disabled_by_default); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesCameraResponse")); + dump_field(out, ESPHOME_PSTR("object_id"), this->object_id); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("name"), this->name); + dump_field(out, ESPHOME_PSTR("disabled_by_default"), this->disabled_by_default); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon); + dump_field(out, ESPHOME_PSTR("icon"), this->icon); #endif - dump_field(out, "entity_category", static_cast(this->entity_category)); + dump_field(out, ESPHOME_PSTR("entity_category"), static_cast(this->entity_category)); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *CameraImageResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "CameraImageResponse"); - dump_field(out, "key", this->key); - dump_bytes_field(out, "data", this->data_ptr_, this->data_len_); - dump_field(out, "done", this->done); + MessageDumpHelper helper(out, ESPHOME_PSTR("CameraImageResponse")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_bytes_field(out, ESPHOME_PSTR("data"), this->data_ptr_, this->data_len_); + dump_field(out, ESPHOME_PSTR("done"), this->done); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *CameraImageRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "CameraImageRequest"); - dump_field(out, "single", this->single); - dump_field(out, "stream", this->stream); + MessageDumpHelper helper(out, ESPHOME_PSTR("CameraImageRequest")); + dump_field(out, ESPHOME_PSTR("single"), this->single); + dump_field(out, ESPHOME_PSTR("stream"), this->stream); return out.c_str(); } #endif #ifdef USE_CLIMATE const char *ListEntitiesClimateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesClimateResponse"); - dump_field(out, "object_id", this->object_id); - dump_field(out, "key", this->key); - dump_field(out, "name", this->name); - dump_field(out, "supports_current_temperature", this->supports_current_temperature); - dump_field(out, "supports_two_point_target_temperature", this->supports_two_point_target_temperature); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesClimateResponse")); + dump_field(out, ESPHOME_PSTR("object_id"), this->object_id); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("name"), this->name); + dump_field(out, ESPHOME_PSTR("supports_current_temperature"), this->supports_current_temperature); + dump_field(out, ESPHOME_PSTR("supports_two_point_target_temperature"), this->supports_two_point_target_temperature); for (const auto &it : *this->supported_modes) { - dump_field(out, "supported_modes", static_cast(it), 4); + dump_field(out, ESPHOME_PSTR("supported_modes"), static_cast(it), 4); } - dump_field(out, "visual_min_temperature", this->visual_min_temperature); - dump_field(out, "visual_max_temperature", this->visual_max_temperature); - dump_field(out, "visual_target_temperature_step", this->visual_target_temperature_step); - dump_field(out, "supports_action", this->supports_action); + dump_field(out, ESPHOME_PSTR("visual_min_temperature"), this->visual_min_temperature); + dump_field(out, ESPHOME_PSTR("visual_max_temperature"), this->visual_max_temperature); + dump_field(out, ESPHOME_PSTR("visual_target_temperature_step"), this->visual_target_temperature_step); + dump_field(out, ESPHOME_PSTR("supports_action"), this->supports_action); for (const auto &it : *this->supported_fan_modes) { - dump_field(out, "supported_fan_modes", static_cast(it), 4); + dump_field(out, ESPHOME_PSTR("supported_fan_modes"), static_cast(it), 4); } for (const auto &it : *this->supported_swing_modes) { - dump_field(out, "supported_swing_modes", static_cast(it), 4); + dump_field(out, ESPHOME_PSTR("supported_swing_modes"), static_cast(it), 4); } for (const auto &it : *this->supported_custom_fan_modes) { - dump_field(out, "supported_custom_fan_modes", it, 4); + dump_field(out, ESPHOME_PSTR("supported_custom_fan_modes"), it, 4); } for (const auto &it : *this->supported_presets) { - dump_field(out, "supported_presets", static_cast(it), 4); + dump_field(out, ESPHOME_PSTR("supported_presets"), static_cast(it), 4); } for (const auto &it : *this->supported_custom_presets) { - dump_field(out, "supported_custom_presets", it, 4); + dump_field(out, ESPHOME_PSTR("supported_custom_presets"), it, 4); } - dump_field(out, "disabled_by_default", this->disabled_by_default); + dump_field(out, ESPHOME_PSTR("disabled_by_default"), this->disabled_by_default); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon); + dump_field(out, ESPHOME_PSTR("icon"), this->icon); #endif - dump_field(out, "entity_category", static_cast(this->entity_category)); - dump_field(out, "visual_current_temperature_step", this->visual_current_temperature_step); - dump_field(out, "supports_current_humidity", this->supports_current_humidity); - dump_field(out, "supports_target_humidity", this->supports_target_humidity); - dump_field(out, "visual_min_humidity", this->visual_min_humidity); - dump_field(out, "visual_max_humidity", this->visual_max_humidity); + dump_field(out, ESPHOME_PSTR("entity_category"), static_cast(this->entity_category)); + dump_field(out, ESPHOME_PSTR("visual_current_temperature_step"), this->visual_current_temperature_step); + dump_field(out, ESPHOME_PSTR("supports_current_humidity"), this->supports_current_humidity); + dump_field(out, ESPHOME_PSTR("supports_target_humidity"), this->supports_target_humidity); + dump_field(out, ESPHOME_PSTR("visual_min_humidity"), this->visual_min_humidity); + dump_field(out, ESPHOME_PSTR("visual_max_humidity"), this->visual_max_humidity); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif - dump_field(out, "feature_flags", this->feature_flags); + dump_field(out, ESPHOME_PSTR("feature_flags"), this->feature_flags); return out.c_str(); } const char *ClimateStateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ClimateStateResponse"); - dump_field(out, "key", this->key); - dump_field(out, "mode", static_cast(this->mode)); - dump_field(out, "current_temperature", this->current_temperature); - dump_field(out, "target_temperature", this->target_temperature); - dump_field(out, "target_temperature_low", this->target_temperature_low); - dump_field(out, "target_temperature_high", this->target_temperature_high); - dump_field(out, "action", static_cast(this->action)); - dump_field(out, "fan_mode", static_cast(this->fan_mode)); - dump_field(out, "swing_mode", static_cast(this->swing_mode)); - dump_field(out, "custom_fan_mode", this->custom_fan_mode); - dump_field(out, "preset", static_cast(this->preset)); - dump_field(out, "custom_preset", this->custom_preset); - dump_field(out, "current_humidity", this->current_humidity); - dump_field(out, "target_humidity", this->target_humidity); + MessageDumpHelper helper(out, ESPHOME_PSTR("ClimateStateResponse")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("mode"), static_cast(this->mode)); + dump_field(out, ESPHOME_PSTR("current_temperature"), this->current_temperature); + dump_field(out, ESPHOME_PSTR("target_temperature"), this->target_temperature); + dump_field(out, ESPHOME_PSTR("target_temperature_low"), this->target_temperature_low); + dump_field(out, ESPHOME_PSTR("target_temperature_high"), this->target_temperature_high); + dump_field(out, ESPHOME_PSTR("action"), static_cast(this->action)); + dump_field(out, ESPHOME_PSTR("fan_mode"), static_cast(this->fan_mode)); + dump_field(out, ESPHOME_PSTR("swing_mode"), static_cast(this->swing_mode)); + dump_field(out, ESPHOME_PSTR("custom_fan_mode"), this->custom_fan_mode); + dump_field(out, ESPHOME_PSTR("preset"), static_cast(this->preset)); + dump_field(out, ESPHOME_PSTR("custom_preset"), this->custom_preset); + dump_field(out, ESPHOME_PSTR("current_humidity"), this->current_humidity); + dump_field(out, ESPHOME_PSTR("target_humidity"), this->target_humidity); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *ClimateCommandRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ClimateCommandRequest"); - dump_field(out, "key", this->key); - dump_field(out, "has_mode", this->has_mode); - dump_field(out, "mode", static_cast(this->mode)); - dump_field(out, "has_target_temperature", this->has_target_temperature); - dump_field(out, "target_temperature", this->target_temperature); - dump_field(out, "has_target_temperature_low", this->has_target_temperature_low); - dump_field(out, "target_temperature_low", this->target_temperature_low); - dump_field(out, "has_target_temperature_high", this->has_target_temperature_high); - dump_field(out, "target_temperature_high", this->target_temperature_high); - dump_field(out, "has_fan_mode", this->has_fan_mode); - dump_field(out, "fan_mode", static_cast(this->fan_mode)); - dump_field(out, "has_swing_mode", this->has_swing_mode); - dump_field(out, "swing_mode", static_cast(this->swing_mode)); - dump_field(out, "has_custom_fan_mode", this->has_custom_fan_mode); - dump_field(out, "custom_fan_mode", this->custom_fan_mode); - dump_field(out, "has_preset", this->has_preset); - dump_field(out, "preset", static_cast(this->preset)); - dump_field(out, "has_custom_preset", this->has_custom_preset); - dump_field(out, "custom_preset", this->custom_preset); - dump_field(out, "has_target_humidity", this->has_target_humidity); - dump_field(out, "target_humidity", this->target_humidity); + MessageDumpHelper helper(out, ESPHOME_PSTR("ClimateCommandRequest")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("has_mode"), this->has_mode); + dump_field(out, ESPHOME_PSTR("mode"), static_cast(this->mode)); + dump_field(out, ESPHOME_PSTR("has_target_temperature"), this->has_target_temperature); + dump_field(out, ESPHOME_PSTR("target_temperature"), this->target_temperature); + dump_field(out, ESPHOME_PSTR("has_target_temperature_low"), this->has_target_temperature_low); + dump_field(out, ESPHOME_PSTR("target_temperature_low"), this->target_temperature_low); + dump_field(out, ESPHOME_PSTR("has_target_temperature_high"), this->has_target_temperature_high); + dump_field(out, ESPHOME_PSTR("target_temperature_high"), this->target_temperature_high); + dump_field(out, ESPHOME_PSTR("has_fan_mode"), this->has_fan_mode); + dump_field(out, ESPHOME_PSTR("fan_mode"), static_cast(this->fan_mode)); + dump_field(out, ESPHOME_PSTR("has_swing_mode"), this->has_swing_mode); + dump_field(out, ESPHOME_PSTR("swing_mode"), static_cast(this->swing_mode)); + dump_field(out, ESPHOME_PSTR("has_custom_fan_mode"), this->has_custom_fan_mode); + dump_field(out, ESPHOME_PSTR("custom_fan_mode"), this->custom_fan_mode); + dump_field(out, ESPHOME_PSTR("has_preset"), this->has_preset); + dump_field(out, ESPHOME_PSTR("preset"), static_cast(this->preset)); + dump_field(out, ESPHOME_PSTR("has_custom_preset"), this->has_custom_preset); + dump_field(out, ESPHOME_PSTR("custom_preset"), this->custom_preset); + dump_field(out, ESPHOME_PSTR("has_target_humidity"), this->has_target_humidity); + dump_field(out, ESPHOME_PSTR("target_humidity"), this->target_humidity); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } #endif #ifdef USE_WATER_HEATER const char *ListEntitiesWaterHeaterResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesWaterHeaterResponse"); - dump_field(out, "object_id", this->object_id); - dump_field(out, "key", this->key); - dump_field(out, "name", this->name); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesWaterHeaterResponse")); + dump_field(out, ESPHOME_PSTR("object_id"), this->object_id); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("name"), this->name); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon); + dump_field(out, ESPHOME_PSTR("icon"), this->icon); #endif - dump_field(out, "disabled_by_default", this->disabled_by_default); - dump_field(out, "entity_category", static_cast(this->entity_category)); + dump_field(out, ESPHOME_PSTR("disabled_by_default"), this->disabled_by_default); + dump_field(out, ESPHOME_PSTR("entity_category"), static_cast(this->entity_category)); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif - dump_field(out, "min_temperature", this->min_temperature); - dump_field(out, "max_temperature", this->max_temperature); - dump_field(out, "target_temperature_step", this->target_temperature_step); + dump_field(out, ESPHOME_PSTR("min_temperature"), this->min_temperature); + dump_field(out, ESPHOME_PSTR("max_temperature"), this->max_temperature); + dump_field(out, ESPHOME_PSTR("target_temperature_step"), this->target_temperature_step); for (const auto &it : *this->supported_modes) { - dump_field(out, "supported_modes", static_cast(it), 4); + dump_field(out, ESPHOME_PSTR("supported_modes"), static_cast(it), 4); } - dump_field(out, "supported_features", this->supported_features); + dump_field(out, ESPHOME_PSTR("supported_features"), this->supported_features); return out.c_str(); } const char *WaterHeaterStateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "WaterHeaterStateResponse"); - dump_field(out, "key", this->key); - dump_field(out, "current_temperature", this->current_temperature); - dump_field(out, "target_temperature", this->target_temperature); - dump_field(out, "mode", static_cast(this->mode)); + MessageDumpHelper helper(out, ESPHOME_PSTR("WaterHeaterStateResponse")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("current_temperature"), this->current_temperature); + dump_field(out, ESPHOME_PSTR("target_temperature"), this->target_temperature); + dump_field(out, ESPHOME_PSTR("mode"), static_cast(this->mode)); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif - dump_field(out, "state", this->state); - dump_field(out, "target_temperature_low", this->target_temperature_low); - dump_field(out, "target_temperature_high", this->target_temperature_high); + dump_field(out, ESPHOME_PSTR("state"), this->state); + dump_field(out, ESPHOME_PSTR("target_temperature_low"), this->target_temperature_low); + dump_field(out, ESPHOME_PSTR("target_temperature_high"), this->target_temperature_high); return out.c_str(); } const char *WaterHeaterCommandRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "WaterHeaterCommandRequest"); - dump_field(out, "key", this->key); - dump_field(out, "has_fields", this->has_fields); - dump_field(out, "mode", static_cast(this->mode)); - dump_field(out, "target_temperature", this->target_temperature); + MessageDumpHelper helper(out, ESPHOME_PSTR("WaterHeaterCommandRequest")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("has_fields"), this->has_fields); + dump_field(out, ESPHOME_PSTR("mode"), static_cast(this->mode)); + dump_field(out, ESPHOME_PSTR("target_temperature"), this->target_temperature); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif - dump_field(out, "state", this->state); - dump_field(out, "target_temperature_low", this->target_temperature_low); - dump_field(out, "target_temperature_high", this->target_temperature_high); + dump_field(out, ESPHOME_PSTR("state"), this->state); + dump_field(out, ESPHOME_PSTR("target_temperature_low"), this->target_temperature_low); + dump_field(out, ESPHOME_PSTR("target_temperature_high"), this->target_temperature_high); return out.c_str(); } #endif #ifdef USE_NUMBER const char *ListEntitiesNumberResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesNumberResponse"); - dump_field(out, "object_id", this->object_id); - dump_field(out, "key", this->key); - dump_field(out, "name", this->name); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesNumberResponse")); + dump_field(out, ESPHOME_PSTR("object_id"), this->object_id); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("name"), this->name); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon); + dump_field(out, ESPHOME_PSTR("icon"), this->icon); #endif - dump_field(out, "min_value", this->min_value); - dump_field(out, "max_value", this->max_value); - dump_field(out, "step", this->step); - dump_field(out, "disabled_by_default", this->disabled_by_default); - dump_field(out, "entity_category", static_cast(this->entity_category)); - dump_field(out, "unit_of_measurement", this->unit_of_measurement); - dump_field(out, "mode", static_cast(this->mode)); - dump_field(out, "device_class", this->device_class); + dump_field(out, ESPHOME_PSTR("min_value"), this->min_value); + dump_field(out, ESPHOME_PSTR("max_value"), this->max_value); + dump_field(out, ESPHOME_PSTR("step"), this->step); + dump_field(out, ESPHOME_PSTR("disabled_by_default"), this->disabled_by_default); + dump_field(out, ESPHOME_PSTR("entity_category"), static_cast(this->entity_category)); + dump_field(out, ESPHOME_PSTR("unit_of_measurement"), this->unit_of_measurement); + dump_field(out, ESPHOME_PSTR("mode"), static_cast(this->mode)); + dump_field(out, ESPHOME_PSTR("device_class"), this->device_class); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *NumberStateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "NumberStateResponse"); - dump_field(out, "key", this->key); - dump_field(out, "state", this->state); - dump_field(out, "missing_state", this->missing_state); + MessageDumpHelper helper(out, ESPHOME_PSTR("NumberStateResponse")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("state"), this->state); + dump_field(out, ESPHOME_PSTR("missing_state"), this->missing_state); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *NumberCommandRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "NumberCommandRequest"); - dump_field(out, "key", this->key); - dump_field(out, "state", this->state); + MessageDumpHelper helper(out, ESPHOME_PSTR("NumberCommandRequest")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("state"), this->state); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } #endif #ifdef USE_SELECT const char *ListEntitiesSelectResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesSelectResponse"); - dump_field(out, "object_id", this->object_id); - dump_field(out, "key", this->key); - dump_field(out, "name", this->name); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesSelectResponse")); + dump_field(out, ESPHOME_PSTR("object_id"), this->object_id); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("name"), this->name); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon); + dump_field(out, ESPHOME_PSTR("icon"), this->icon); #endif for (const auto &it : *this->options) { - dump_field(out, "options", it, 4); + dump_field(out, ESPHOME_PSTR("options"), it, 4); } - dump_field(out, "disabled_by_default", this->disabled_by_default); - dump_field(out, "entity_category", static_cast(this->entity_category)); + dump_field(out, ESPHOME_PSTR("disabled_by_default"), this->disabled_by_default); + dump_field(out, ESPHOME_PSTR("entity_category"), static_cast(this->entity_category)); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *SelectStateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "SelectStateResponse"); - dump_field(out, "key", this->key); - dump_field(out, "state", this->state); - dump_field(out, "missing_state", this->missing_state); + MessageDumpHelper helper(out, ESPHOME_PSTR("SelectStateResponse")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("state"), this->state); + dump_field(out, ESPHOME_PSTR("missing_state"), this->missing_state); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *SelectCommandRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "SelectCommandRequest"); - dump_field(out, "key", this->key); - dump_field(out, "state", this->state); + MessageDumpHelper helper(out, ESPHOME_PSTR("SelectCommandRequest")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("state"), this->state); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } #endif #ifdef USE_SIREN const char *ListEntitiesSirenResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesSirenResponse"); - dump_field(out, "object_id", this->object_id); - dump_field(out, "key", this->key); - dump_field(out, "name", this->name); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesSirenResponse")); + dump_field(out, ESPHOME_PSTR("object_id"), this->object_id); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("name"), this->name); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon); + dump_field(out, ESPHOME_PSTR("icon"), this->icon); #endif - dump_field(out, "disabled_by_default", this->disabled_by_default); + dump_field(out, ESPHOME_PSTR("disabled_by_default"), this->disabled_by_default); for (const auto &it : *this->tones) { - dump_field(out, "tones", it, 4); + dump_field(out, ESPHOME_PSTR("tones"), it, 4); } - dump_field(out, "supports_duration", this->supports_duration); - dump_field(out, "supports_volume", this->supports_volume); - dump_field(out, "entity_category", static_cast(this->entity_category)); + dump_field(out, ESPHOME_PSTR("supports_duration"), this->supports_duration); + dump_field(out, ESPHOME_PSTR("supports_volume"), this->supports_volume); + dump_field(out, ESPHOME_PSTR("entity_category"), static_cast(this->entity_category)); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *SirenStateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "SirenStateResponse"); - dump_field(out, "key", this->key); - dump_field(out, "state", this->state); + MessageDumpHelper helper(out, ESPHOME_PSTR("SirenStateResponse")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("state"), this->state); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *SirenCommandRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "SirenCommandRequest"); - dump_field(out, "key", this->key); - dump_field(out, "has_state", this->has_state); - dump_field(out, "state", this->state); - dump_field(out, "has_tone", this->has_tone); - dump_field(out, "tone", this->tone); - dump_field(out, "has_duration", this->has_duration); - dump_field(out, "duration", this->duration); - dump_field(out, "has_volume", this->has_volume); - dump_field(out, "volume", this->volume); + MessageDumpHelper helper(out, ESPHOME_PSTR("SirenCommandRequest")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("has_state"), this->has_state); + dump_field(out, ESPHOME_PSTR("state"), this->state); + dump_field(out, ESPHOME_PSTR("has_tone"), this->has_tone); + dump_field(out, ESPHOME_PSTR("tone"), this->tone); + dump_field(out, ESPHOME_PSTR("has_duration"), this->has_duration); + dump_field(out, ESPHOME_PSTR("duration"), this->duration); + dump_field(out, ESPHOME_PSTR("has_volume"), this->has_volume); + dump_field(out, ESPHOME_PSTR("volume"), this->volume); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } #endif #ifdef USE_LOCK const char *ListEntitiesLockResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesLockResponse"); - dump_field(out, "object_id", this->object_id); - dump_field(out, "key", this->key); - dump_field(out, "name", this->name); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesLockResponse")); + dump_field(out, ESPHOME_PSTR("object_id"), this->object_id); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("name"), this->name); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon); + dump_field(out, ESPHOME_PSTR("icon"), this->icon); #endif - dump_field(out, "disabled_by_default", this->disabled_by_default); - dump_field(out, "entity_category", static_cast(this->entity_category)); - dump_field(out, "assumed_state", this->assumed_state); - dump_field(out, "supports_open", this->supports_open); - dump_field(out, "requires_code", this->requires_code); - dump_field(out, "code_format", this->code_format); + dump_field(out, ESPHOME_PSTR("disabled_by_default"), this->disabled_by_default); + dump_field(out, ESPHOME_PSTR("entity_category"), static_cast(this->entity_category)); + dump_field(out, ESPHOME_PSTR("assumed_state"), this->assumed_state); + dump_field(out, ESPHOME_PSTR("supports_open"), this->supports_open); + dump_field(out, ESPHOME_PSTR("requires_code"), this->requires_code); + dump_field(out, ESPHOME_PSTR("code_format"), this->code_format); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *LockStateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "LockStateResponse"); - dump_field(out, "key", this->key); - dump_field(out, "state", static_cast(this->state)); + MessageDumpHelper helper(out, ESPHOME_PSTR("LockStateResponse")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("state"), static_cast(this->state)); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *LockCommandRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "LockCommandRequest"); - dump_field(out, "key", this->key); - dump_field(out, "command", static_cast(this->command)); - dump_field(out, "has_code", this->has_code); - dump_field(out, "code", this->code); + MessageDumpHelper helper(out, ESPHOME_PSTR("LockCommandRequest")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("command"), static_cast(this->command)); + dump_field(out, ESPHOME_PSTR("has_code"), this->has_code); + dump_field(out, ESPHOME_PSTR("code"), this->code); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } #endif #ifdef USE_BUTTON const char *ListEntitiesButtonResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesButtonResponse"); - dump_field(out, "object_id", this->object_id); - dump_field(out, "key", this->key); - dump_field(out, "name", this->name); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesButtonResponse")); + dump_field(out, ESPHOME_PSTR("object_id"), this->object_id); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("name"), this->name); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon); + dump_field(out, ESPHOME_PSTR("icon"), this->icon); #endif - dump_field(out, "disabled_by_default", this->disabled_by_default); - dump_field(out, "entity_category", static_cast(this->entity_category)); - dump_field(out, "device_class", this->device_class); + dump_field(out, ESPHOME_PSTR("disabled_by_default"), this->disabled_by_default); + dump_field(out, ESPHOME_PSTR("entity_category"), static_cast(this->entity_category)); + dump_field(out, ESPHOME_PSTR("device_class"), this->device_class); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *ButtonCommandRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ButtonCommandRequest"); - dump_field(out, "key", this->key); + MessageDumpHelper helper(out, ESPHOME_PSTR("ButtonCommandRequest")); + dump_field(out, ESPHOME_PSTR("key"), this->key); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } #endif #ifdef USE_MEDIA_PLAYER const char *MediaPlayerSupportedFormat::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "MediaPlayerSupportedFormat"); - dump_field(out, "format", this->format); - dump_field(out, "sample_rate", this->sample_rate); - dump_field(out, "num_channels", this->num_channels); - dump_field(out, "purpose", static_cast(this->purpose)); - dump_field(out, "sample_bytes", this->sample_bytes); + MessageDumpHelper helper(out, ESPHOME_PSTR("MediaPlayerSupportedFormat")); + dump_field(out, ESPHOME_PSTR("format"), this->format); + dump_field(out, ESPHOME_PSTR("sample_rate"), this->sample_rate); + dump_field(out, ESPHOME_PSTR("num_channels"), this->num_channels); + dump_field(out, ESPHOME_PSTR("purpose"), static_cast(this->purpose)); + dump_field(out, ESPHOME_PSTR("sample_bytes"), this->sample_bytes); return out.c_str(); } const char *ListEntitiesMediaPlayerResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesMediaPlayerResponse"); - dump_field(out, "object_id", this->object_id); - dump_field(out, "key", this->key); - dump_field(out, "name", this->name); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesMediaPlayerResponse")); + dump_field(out, ESPHOME_PSTR("object_id"), this->object_id); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("name"), this->name); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon); + dump_field(out, ESPHOME_PSTR("icon"), this->icon); #endif - dump_field(out, "disabled_by_default", this->disabled_by_default); - dump_field(out, "entity_category", static_cast(this->entity_category)); - dump_field(out, "supports_pause", this->supports_pause); + dump_field(out, ESPHOME_PSTR("disabled_by_default"), this->disabled_by_default); + dump_field(out, ESPHOME_PSTR("entity_category"), static_cast(this->entity_category)); + dump_field(out, ESPHOME_PSTR("supports_pause"), this->supports_pause); for (const auto &it : this->supported_formats) { - out.append(" supported_formats: "); + out.append(4, ' ').append_p(ESPHOME_PSTR("supported_formats")).append(": "); it.dump_to(out); out.append("\n"); } #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif - dump_field(out, "feature_flags", this->feature_flags); + dump_field(out, ESPHOME_PSTR("feature_flags"), this->feature_flags); return out.c_str(); } const char *MediaPlayerStateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "MediaPlayerStateResponse"); - dump_field(out, "key", this->key); - dump_field(out, "state", static_cast(this->state)); - dump_field(out, "volume", this->volume); - dump_field(out, "muted", this->muted); + MessageDumpHelper helper(out, ESPHOME_PSTR("MediaPlayerStateResponse")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("state"), static_cast(this->state)); + dump_field(out, ESPHOME_PSTR("volume"), this->volume); + dump_field(out, ESPHOME_PSTR("muted"), this->muted); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *MediaPlayerCommandRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "MediaPlayerCommandRequest"); - dump_field(out, "key", this->key); - dump_field(out, "has_command", this->has_command); - dump_field(out, "command", static_cast(this->command)); - dump_field(out, "has_volume", this->has_volume); - dump_field(out, "volume", this->volume); - dump_field(out, "has_media_url", this->has_media_url); - dump_field(out, "media_url", this->media_url); - dump_field(out, "has_announcement", this->has_announcement); - dump_field(out, "announcement", this->announcement); + MessageDumpHelper helper(out, ESPHOME_PSTR("MediaPlayerCommandRequest")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("has_command"), this->has_command); + dump_field(out, ESPHOME_PSTR("command"), static_cast(this->command)); + dump_field(out, ESPHOME_PSTR("has_volume"), this->has_volume); + dump_field(out, ESPHOME_PSTR("volume"), this->volume); + dump_field(out, ESPHOME_PSTR("has_media_url"), this->has_media_url); + dump_field(out, ESPHOME_PSTR("media_url"), this->media_url); + dump_field(out, ESPHOME_PSTR("has_announcement"), this->has_announcement); + dump_field(out, ESPHOME_PSTR("announcement"), this->announcement); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } #endif #ifdef USE_BLUETOOTH_PROXY const char *SubscribeBluetoothLEAdvertisementsRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "SubscribeBluetoothLEAdvertisementsRequest"); - dump_field(out, "flags", this->flags); + MessageDumpHelper helper(out, ESPHOME_PSTR("SubscribeBluetoothLEAdvertisementsRequest")); + dump_field(out, ESPHOME_PSTR("flags"), this->flags); return out.c_str(); } const char *BluetoothLERawAdvertisement::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothLERawAdvertisement"); - dump_field(out, "address", this->address); - dump_field(out, "rssi", this->rssi); - dump_field(out, "address_type", this->address_type); - dump_bytes_field(out, "data", this->data, this->data_len); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothLERawAdvertisement")); + dump_field(out, ESPHOME_PSTR("address"), this->address); + dump_field(out, ESPHOME_PSTR("rssi"), this->rssi); + dump_field(out, ESPHOME_PSTR("address_type"), this->address_type); + dump_bytes_field(out, ESPHOME_PSTR("data"), this->data, this->data_len); return out.c_str(); } const char *BluetoothLERawAdvertisementsResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothLERawAdvertisementsResponse"); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothLERawAdvertisementsResponse")); for (uint16_t i = 0; i < this->advertisements_len; i++) { - out.append(" advertisements: "); + out.append(4, ' ').append_p(ESPHOME_PSTR("advertisements")).append(": "); this->advertisements[i].dump_to(out); out.append("\n"); } return out.c_str(); } const char *BluetoothDeviceRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothDeviceRequest"); - dump_field(out, "address", this->address); - dump_field(out, "request_type", static_cast(this->request_type)); - dump_field(out, "has_address_type", this->has_address_type); - dump_field(out, "address_type", this->address_type); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothDeviceRequest")); + dump_field(out, ESPHOME_PSTR("address"), this->address); + dump_field(out, ESPHOME_PSTR("request_type"), static_cast(this->request_type)); + dump_field(out, ESPHOME_PSTR("has_address_type"), this->has_address_type); + dump_field(out, ESPHOME_PSTR("address_type"), this->address_type); return out.c_str(); } const char *BluetoothDeviceConnectionResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothDeviceConnectionResponse"); - dump_field(out, "address", this->address); - dump_field(out, "connected", this->connected); - dump_field(out, "mtu", this->mtu); - dump_field(out, "error", this->error); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothDeviceConnectionResponse")); + dump_field(out, ESPHOME_PSTR("address"), this->address); + dump_field(out, ESPHOME_PSTR("connected"), this->connected); + dump_field(out, ESPHOME_PSTR("mtu"), this->mtu); + dump_field(out, ESPHOME_PSTR("error"), this->error); return out.c_str(); } const char *BluetoothGATTGetServicesRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothGATTGetServicesRequest"); - dump_field(out, "address", this->address); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothGATTGetServicesRequest")); + dump_field(out, ESPHOME_PSTR("address"), this->address); return out.c_str(); } const char *BluetoothGATTDescriptor::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothGATTDescriptor"); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothGATTDescriptor")); for (const auto &it : this->uuid) { - dump_field(out, "uuid", it, 4); + dump_field(out, ESPHOME_PSTR("uuid"), it, 4); } - dump_field(out, "handle", this->handle); - dump_field(out, "short_uuid", this->short_uuid); + dump_field(out, ESPHOME_PSTR("handle"), this->handle); + dump_field(out, ESPHOME_PSTR("short_uuid"), this->short_uuid); return out.c_str(); } const char *BluetoothGATTCharacteristic::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothGATTCharacteristic"); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothGATTCharacteristic")); for (const auto &it : this->uuid) { - dump_field(out, "uuid", it, 4); + dump_field(out, ESPHOME_PSTR("uuid"), it, 4); } - dump_field(out, "handle", this->handle); - dump_field(out, "properties", this->properties); + dump_field(out, ESPHOME_PSTR("handle"), this->handle); + dump_field(out, ESPHOME_PSTR("properties"), this->properties); for (const auto &it : this->descriptors) { - out.append(" descriptors: "); + out.append(4, ' ').append_p(ESPHOME_PSTR("descriptors")).append(": "); it.dump_to(out); out.append("\n"); } - dump_field(out, "short_uuid", this->short_uuid); + dump_field(out, ESPHOME_PSTR("short_uuid"), this->short_uuid); return out.c_str(); } const char *BluetoothGATTService::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothGATTService"); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothGATTService")); for (const auto &it : this->uuid) { - dump_field(out, "uuid", it, 4); + dump_field(out, ESPHOME_PSTR("uuid"), it, 4); } - dump_field(out, "handle", this->handle); + dump_field(out, ESPHOME_PSTR("handle"), this->handle); for (const auto &it : this->characteristics) { - out.append(" characteristics: "); + out.append(4, ' ').append_p(ESPHOME_PSTR("characteristics")).append(": "); it.dump_to(out); out.append("\n"); } - dump_field(out, "short_uuid", this->short_uuid); + dump_field(out, ESPHOME_PSTR("short_uuid"), this->short_uuid); return out.c_str(); } const char *BluetoothGATTGetServicesResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothGATTGetServicesResponse"); - dump_field(out, "address", this->address); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothGATTGetServicesResponse")); + dump_field(out, ESPHOME_PSTR("address"), this->address); for (const auto &it : this->services) { - out.append(" services: "); + out.append(4, ' ').append_p(ESPHOME_PSTR("services")).append(": "); it.dump_to(out); out.append("\n"); } return out.c_str(); } const char *BluetoothGATTGetServicesDoneResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothGATTGetServicesDoneResponse"); - dump_field(out, "address", this->address); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothGATTGetServicesDoneResponse")); + dump_field(out, ESPHOME_PSTR("address"), this->address); return out.c_str(); } const char *BluetoothGATTReadRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothGATTReadRequest"); - dump_field(out, "address", this->address); - dump_field(out, "handle", this->handle); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothGATTReadRequest")); + dump_field(out, ESPHOME_PSTR("address"), this->address); + dump_field(out, ESPHOME_PSTR("handle"), this->handle); return out.c_str(); } const char *BluetoothGATTReadResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothGATTReadResponse"); - dump_field(out, "address", this->address); - dump_field(out, "handle", this->handle); - dump_bytes_field(out, "data", this->data_ptr_, this->data_len_); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothGATTReadResponse")); + dump_field(out, ESPHOME_PSTR("address"), this->address); + dump_field(out, ESPHOME_PSTR("handle"), this->handle); + dump_bytes_field(out, ESPHOME_PSTR("data"), this->data_ptr_, this->data_len_); return out.c_str(); } const char *BluetoothGATTWriteRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothGATTWriteRequest"); - dump_field(out, "address", this->address); - dump_field(out, "handle", this->handle); - dump_field(out, "response", this->response); - dump_bytes_field(out, "data", this->data, this->data_len); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothGATTWriteRequest")); + dump_field(out, ESPHOME_PSTR("address"), this->address); + dump_field(out, ESPHOME_PSTR("handle"), this->handle); + dump_field(out, ESPHOME_PSTR("response"), this->response); + dump_bytes_field(out, ESPHOME_PSTR("data"), this->data, this->data_len); return out.c_str(); } const char *BluetoothGATTReadDescriptorRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothGATTReadDescriptorRequest"); - dump_field(out, "address", this->address); - dump_field(out, "handle", this->handle); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothGATTReadDescriptorRequest")); + dump_field(out, ESPHOME_PSTR("address"), this->address); + dump_field(out, ESPHOME_PSTR("handle"), this->handle); return out.c_str(); } const char *BluetoothGATTWriteDescriptorRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothGATTWriteDescriptorRequest"); - dump_field(out, "address", this->address); - dump_field(out, "handle", this->handle); - dump_bytes_field(out, "data", this->data, this->data_len); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothGATTWriteDescriptorRequest")); + dump_field(out, ESPHOME_PSTR("address"), this->address); + dump_field(out, ESPHOME_PSTR("handle"), this->handle); + dump_bytes_field(out, ESPHOME_PSTR("data"), this->data, this->data_len); return out.c_str(); } const char *BluetoothGATTNotifyRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothGATTNotifyRequest"); - dump_field(out, "address", this->address); - dump_field(out, "handle", this->handle); - dump_field(out, "enable", this->enable); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothGATTNotifyRequest")); + dump_field(out, ESPHOME_PSTR("address"), this->address); + dump_field(out, ESPHOME_PSTR("handle"), this->handle); + dump_field(out, ESPHOME_PSTR("enable"), this->enable); return out.c_str(); } const char *BluetoothGATTNotifyDataResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothGATTNotifyDataResponse"); - dump_field(out, "address", this->address); - dump_field(out, "handle", this->handle); - dump_bytes_field(out, "data", this->data_ptr_, this->data_len_); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothGATTNotifyDataResponse")); + dump_field(out, ESPHOME_PSTR("address"), this->address); + dump_field(out, ESPHOME_PSTR("handle"), this->handle); + dump_bytes_field(out, ESPHOME_PSTR("data"), this->data_ptr_, this->data_len_); return out.c_str(); } const char *BluetoothConnectionsFreeResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothConnectionsFreeResponse"); - dump_field(out, "free", this->free); - dump_field(out, "limit", this->limit); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothConnectionsFreeResponse")); + dump_field(out, ESPHOME_PSTR("free"), this->free); + dump_field(out, ESPHOME_PSTR("limit"), this->limit); for (const auto &it : this->allocated) { - dump_field(out, "allocated", it, 4); + dump_field(out, ESPHOME_PSTR("allocated"), it, 4); } return out.c_str(); } const char *BluetoothGATTErrorResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothGATTErrorResponse"); - dump_field(out, "address", this->address); - dump_field(out, "handle", this->handle); - dump_field(out, "error", this->error); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothGATTErrorResponse")); + dump_field(out, ESPHOME_PSTR("address"), this->address); + dump_field(out, ESPHOME_PSTR("handle"), this->handle); + dump_field(out, ESPHOME_PSTR("error"), this->error); return out.c_str(); } const char *BluetoothGATTWriteResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothGATTWriteResponse"); - dump_field(out, "address", this->address); - dump_field(out, "handle", this->handle); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothGATTWriteResponse")); + dump_field(out, ESPHOME_PSTR("address"), this->address); + dump_field(out, ESPHOME_PSTR("handle"), this->handle); return out.c_str(); } const char *BluetoothGATTNotifyResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothGATTNotifyResponse"); - dump_field(out, "address", this->address); - dump_field(out, "handle", this->handle); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothGATTNotifyResponse")); + dump_field(out, ESPHOME_PSTR("address"), this->address); + dump_field(out, ESPHOME_PSTR("handle"), this->handle); return out.c_str(); } const char *BluetoothDevicePairingResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothDevicePairingResponse"); - dump_field(out, "address", this->address); - dump_field(out, "paired", this->paired); - dump_field(out, "error", this->error); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothDevicePairingResponse")); + dump_field(out, ESPHOME_PSTR("address"), this->address); + dump_field(out, ESPHOME_PSTR("paired"), this->paired); + dump_field(out, ESPHOME_PSTR("error"), this->error); return out.c_str(); } const char *BluetoothDeviceUnpairingResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothDeviceUnpairingResponse"); - dump_field(out, "address", this->address); - dump_field(out, "success", this->success); - dump_field(out, "error", this->error); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothDeviceUnpairingResponse")); + dump_field(out, ESPHOME_PSTR("address"), this->address); + dump_field(out, ESPHOME_PSTR("success"), this->success); + dump_field(out, ESPHOME_PSTR("error"), this->error); return out.c_str(); } const char *BluetoothDeviceClearCacheResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothDeviceClearCacheResponse"); - dump_field(out, "address", this->address); - dump_field(out, "success", this->success); - dump_field(out, "error", this->error); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothDeviceClearCacheResponse")); + dump_field(out, ESPHOME_PSTR("address"), this->address); + dump_field(out, ESPHOME_PSTR("success"), this->success); + dump_field(out, ESPHOME_PSTR("error"), this->error); return out.c_str(); } const char *BluetoothScannerStateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothScannerStateResponse"); - dump_field(out, "state", static_cast(this->state)); - dump_field(out, "mode", static_cast(this->mode)); - dump_field(out, "configured_mode", static_cast(this->configured_mode)); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothScannerStateResponse")); + dump_field(out, ESPHOME_PSTR("state"), static_cast(this->state)); + dump_field(out, ESPHOME_PSTR("mode"), static_cast(this->mode)); + dump_field(out, ESPHOME_PSTR("configured_mode"), static_cast(this->configured_mode)); return out.c_str(); } const char *BluetoothScannerSetModeRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothScannerSetModeRequest"); - dump_field(out, "mode", static_cast(this->mode)); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothScannerSetModeRequest")); + dump_field(out, ESPHOME_PSTR("mode"), static_cast(this->mode)); return out.c_str(); } #endif #ifdef USE_VOICE_ASSISTANT const char *SubscribeVoiceAssistantRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "SubscribeVoiceAssistantRequest"); - dump_field(out, "subscribe", this->subscribe); - dump_field(out, "flags", this->flags); + MessageDumpHelper helper(out, ESPHOME_PSTR("SubscribeVoiceAssistantRequest")); + dump_field(out, ESPHOME_PSTR("subscribe"), this->subscribe); + dump_field(out, ESPHOME_PSTR("flags"), this->flags); return out.c_str(); } const char *VoiceAssistantAudioSettings::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "VoiceAssistantAudioSettings"); - dump_field(out, "noise_suppression_level", this->noise_suppression_level); - dump_field(out, "auto_gain", this->auto_gain); - dump_field(out, "volume_multiplier", this->volume_multiplier); + MessageDumpHelper helper(out, ESPHOME_PSTR("VoiceAssistantAudioSettings")); + dump_field(out, ESPHOME_PSTR("noise_suppression_level"), this->noise_suppression_level); + dump_field(out, ESPHOME_PSTR("auto_gain"), this->auto_gain); + dump_field(out, ESPHOME_PSTR("volume_multiplier"), this->volume_multiplier); return out.c_str(); } const char *VoiceAssistantRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "VoiceAssistantRequest"); - dump_field(out, "start", this->start); - dump_field(out, "conversation_id", this->conversation_id); - dump_field(out, "flags", this->flags); - out.append(" audio_settings: "); + MessageDumpHelper helper(out, ESPHOME_PSTR("VoiceAssistantRequest")); + dump_field(out, ESPHOME_PSTR("start"), this->start); + dump_field(out, ESPHOME_PSTR("conversation_id"), this->conversation_id); + dump_field(out, ESPHOME_PSTR("flags"), this->flags); + out.append(2, ' ').append_p(ESPHOME_PSTR("audio_settings")).append(": "); this->audio_settings.dump_to(out); out.append("\n"); - dump_field(out, "wake_word_phrase", this->wake_word_phrase); + dump_field(out, ESPHOME_PSTR("wake_word_phrase"), this->wake_word_phrase); return out.c_str(); } const char *VoiceAssistantResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "VoiceAssistantResponse"); - dump_field(out, "port", this->port); - dump_field(out, "error", this->error); + MessageDumpHelper helper(out, ESPHOME_PSTR("VoiceAssistantResponse")); + dump_field(out, ESPHOME_PSTR("port"), this->port); + dump_field(out, ESPHOME_PSTR("error"), this->error); return out.c_str(); } const char *VoiceAssistantEventData::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "VoiceAssistantEventData"); - dump_field(out, "name", this->name); - dump_field(out, "value", this->value); + MessageDumpHelper helper(out, ESPHOME_PSTR("VoiceAssistantEventData")); + dump_field(out, ESPHOME_PSTR("name"), this->name); + dump_field(out, ESPHOME_PSTR("value"), this->value); return out.c_str(); } const char *VoiceAssistantEventResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "VoiceAssistantEventResponse"); - dump_field(out, "event_type", static_cast(this->event_type)); + MessageDumpHelper helper(out, ESPHOME_PSTR("VoiceAssistantEventResponse")); + dump_field(out, ESPHOME_PSTR("event_type"), static_cast(this->event_type)); for (const auto &it : this->data) { - out.append(" data: "); + out.append(4, ' ').append_p(ESPHOME_PSTR("data")).append(": "); it.dump_to(out); out.append("\n"); } return out.c_str(); } const char *VoiceAssistantAudio::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "VoiceAssistantAudio"); - dump_bytes_field(out, "data", this->data, this->data_len); - dump_field(out, "end", this->end); + MessageDumpHelper helper(out, ESPHOME_PSTR("VoiceAssistantAudio")); + dump_bytes_field(out, ESPHOME_PSTR("data"), this->data, this->data_len); + dump_field(out, ESPHOME_PSTR("end"), this->end); return out.c_str(); } const char *VoiceAssistantTimerEventResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "VoiceAssistantTimerEventResponse"); - dump_field(out, "event_type", static_cast(this->event_type)); - dump_field(out, "timer_id", this->timer_id); - dump_field(out, "name", this->name); - dump_field(out, "total_seconds", this->total_seconds); - dump_field(out, "seconds_left", this->seconds_left); - dump_field(out, "is_active", this->is_active); + MessageDumpHelper helper(out, ESPHOME_PSTR("VoiceAssistantTimerEventResponse")); + dump_field(out, ESPHOME_PSTR("event_type"), static_cast(this->event_type)); + dump_field(out, ESPHOME_PSTR("timer_id"), this->timer_id); + dump_field(out, ESPHOME_PSTR("name"), this->name); + dump_field(out, ESPHOME_PSTR("total_seconds"), this->total_seconds); + dump_field(out, ESPHOME_PSTR("seconds_left"), this->seconds_left); + dump_field(out, ESPHOME_PSTR("is_active"), this->is_active); return out.c_str(); } const char *VoiceAssistantAnnounceRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "VoiceAssistantAnnounceRequest"); - dump_field(out, "media_id", this->media_id); - dump_field(out, "text", this->text); - dump_field(out, "preannounce_media_id", this->preannounce_media_id); - dump_field(out, "start_conversation", this->start_conversation); + MessageDumpHelper helper(out, ESPHOME_PSTR("VoiceAssistantAnnounceRequest")); + dump_field(out, ESPHOME_PSTR("media_id"), this->media_id); + dump_field(out, ESPHOME_PSTR("text"), this->text); + dump_field(out, ESPHOME_PSTR("preannounce_media_id"), this->preannounce_media_id); + dump_field(out, ESPHOME_PSTR("start_conversation"), this->start_conversation); return out.c_str(); } const char *VoiceAssistantAnnounceFinished::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "VoiceAssistantAnnounceFinished"); - dump_field(out, "success", this->success); + MessageDumpHelper helper(out, ESPHOME_PSTR("VoiceAssistantAnnounceFinished")); + dump_field(out, ESPHOME_PSTR("success"), this->success); return out.c_str(); } const char *VoiceAssistantWakeWord::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "VoiceAssistantWakeWord"); - dump_field(out, "id", this->id); - dump_field(out, "wake_word", this->wake_word); + MessageDumpHelper helper(out, ESPHOME_PSTR("VoiceAssistantWakeWord")); + dump_field(out, ESPHOME_PSTR("id"), this->id); + dump_field(out, ESPHOME_PSTR("wake_word"), this->wake_word); for (const auto &it : this->trained_languages) { - dump_field(out, "trained_languages", it, 4); + dump_field(out, ESPHOME_PSTR("trained_languages"), it, 4); } return out.c_str(); } const char *VoiceAssistantExternalWakeWord::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "VoiceAssistantExternalWakeWord"); - dump_field(out, "id", this->id); - dump_field(out, "wake_word", this->wake_word); + MessageDumpHelper helper(out, ESPHOME_PSTR("VoiceAssistantExternalWakeWord")); + dump_field(out, ESPHOME_PSTR("id"), this->id); + dump_field(out, ESPHOME_PSTR("wake_word"), this->wake_word); for (const auto &it : this->trained_languages) { - dump_field(out, "trained_languages", it, 4); + dump_field(out, ESPHOME_PSTR("trained_languages"), it, 4); } - dump_field(out, "model_type", this->model_type); - dump_field(out, "model_size", this->model_size); - dump_field(out, "model_hash", this->model_hash); - dump_field(out, "url", this->url); + dump_field(out, ESPHOME_PSTR("model_type"), this->model_type); + dump_field(out, ESPHOME_PSTR("model_size"), this->model_size); + dump_field(out, ESPHOME_PSTR("model_hash"), this->model_hash); + dump_field(out, ESPHOME_PSTR("url"), this->url); return out.c_str(); } const char *VoiceAssistantConfigurationRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "VoiceAssistantConfigurationRequest"); + MessageDumpHelper helper(out, ESPHOME_PSTR("VoiceAssistantConfigurationRequest")); for (const auto &it : this->external_wake_words) { - out.append(" external_wake_words: "); + out.append(4, ' ').append_p(ESPHOME_PSTR("external_wake_words")).append(": "); it.dump_to(out); out.append("\n"); } return out.c_str(); } const char *VoiceAssistantConfigurationResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "VoiceAssistantConfigurationResponse"); + MessageDumpHelper helper(out, ESPHOME_PSTR("VoiceAssistantConfigurationResponse")); for (const auto &it : this->available_wake_words) { - out.append(" available_wake_words: "); + out.append(4, ' ').append_p(ESPHOME_PSTR("available_wake_words")).append(": "); it.dump_to(out); out.append("\n"); } for (const auto &it : *this->active_wake_words) { - dump_field(out, "active_wake_words", it, 4); + dump_field(out, ESPHOME_PSTR("active_wake_words"), it, 4); } - dump_field(out, "max_active_wake_words", this->max_active_wake_words); + dump_field(out, ESPHOME_PSTR("max_active_wake_words"), this->max_active_wake_words); return out.c_str(); } const char *VoiceAssistantSetConfiguration::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "VoiceAssistantSetConfiguration"); + MessageDumpHelper helper(out, ESPHOME_PSTR("VoiceAssistantSetConfiguration")); for (const auto &it : this->active_wake_words) { - dump_field(out, "active_wake_words", it, 4); + dump_field(out, ESPHOME_PSTR("active_wake_words"), it, 4); } return out.c_str(); } #endif #ifdef USE_ALARM_CONTROL_PANEL const char *ListEntitiesAlarmControlPanelResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesAlarmControlPanelResponse"); - dump_field(out, "object_id", this->object_id); - dump_field(out, "key", this->key); - dump_field(out, "name", this->name); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesAlarmControlPanelResponse")); + dump_field(out, ESPHOME_PSTR("object_id"), this->object_id); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("name"), this->name); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon); + dump_field(out, ESPHOME_PSTR("icon"), this->icon); #endif - dump_field(out, "disabled_by_default", this->disabled_by_default); - dump_field(out, "entity_category", static_cast(this->entity_category)); - dump_field(out, "supported_features", this->supported_features); - dump_field(out, "requires_code", this->requires_code); - dump_field(out, "requires_code_to_arm", this->requires_code_to_arm); + dump_field(out, ESPHOME_PSTR("disabled_by_default"), this->disabled_by_default); + dump_field(out, ESPHOME_PSTR("entity_category"), static_cast(this->entity_category)); + dump_field(out, ESPHOME_PSTR("supported_features"), this->supported_features); + dump_field(out, ESPHOME_PSTR("requires_code"), this->requires_code); + dump_field(out, ESPHOME_PSTR("requires_code_to_arm"), this->requires_code_to_arm); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *AlarmControlPanelStateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "AlarmControlPanelStateResponse"); - dump_field(out, "key", this->key); - dump_field(out, "state", static_cast(this->state)); + MessageDumpHelper helper(out, ESPHOME_PSTR("AlarmControlPanelStateResponse")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("state"), static_cast(this->state)); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *AlarmControlPanelCommandRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "AlarmControlPanelCommandRequest"); - dump_field(out, "key", this->key); - dump_field(out, "command", static_cast(this->command)); - dump_field(out, "code", this->code); + MessageDumpHelper helper(out, ESPHOME_PSTR("AlarmControlPanelCommandRequest")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("command"), static_cast(this->command)); + dump_field(out, ESPHOME_PSTR("code"), this->code); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } #endif #ifdef USE_TEXT const char *ListEntitiesTextResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesTextResponse"); - dump_field(out, "object_id", this->object_id); - dump_field(out, "key", this->key); - dump_field(out, "name", this->name); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesTextResponse")); + dump_field(out, ESPHOME_PSTR("object_id"), this->object_id); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("name"), this->name); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon); + dump_field(out, ESPHOME_PSTR("icon"), this->icon); #endif - dump_field(out, "disabled_by_default", this->disabled_by_default); - dump_field(out, "entity_category", static_cast(this->entity_category)); - dump_field(out, "min_length", this->min_length); - dump_field(out, "max_length", this->max_length); - dump_field(out, "pattern", this->pattern); - dump_field(out, "mode", static_cast(this->mode)); + dump_field(out, ESPHOME_PSTR("disabled_by_default"), this->disabled_by_default); + dump_field(out, ESPHOME_PSTR("entity_category"), static_cast(this->entity_category)); + dump_field(out, ESPHOME_PSTR("min_length"), this->min_length); + dump_field(out, ESPHOME_PSTR("max_length"), this->max_length); + dump_field(out, ESPHOME_PSTR("pattern"), this->pattern); + dump_field(out, ESPHOME_PSTR("mode"), static_cast(this->mode)); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *TextStateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "TextStateResponse"); - dump_field(out, "key", this->key); - dump_field(out, "state", this->state); - dump_field(out, "missing_state", this->missing_state); + MessageDumpHelper helper(out, ESPHOME_PSTR("TextStateResponse")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("state"), this->state); + dump_field(out, ESPHOME_PSTR("missing_state"), this->missing_state); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *TextCommandRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "TextCommandRequest"); - dump_field(out, "key", this->key); - dump_field(out, "state", this->state); + MessageDumpHelper helper(out, ESPHOME_PSTR("TextCommandRequest")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("state"), this->state); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } #endif #ifdef USE_DATETIME_DATE const char *ListEntitiesDateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesDateResponse"); - dump_field(out, "object_id", this->object_id); - dump_field(out, "key", this->key); - dump_field(out, "name", this->name); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesDateResponse")); + dump_field(out, ESPHOME_PSTR("object_id"), this->object_id); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("name"), this->name); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon); + dump_field(out, ESPHOME_PSTR("icon"), this->icon); #endif - dump_field(out, "disabled_by_default", this->disabled_by_default); - dump_field(out, "entity_category", static_cast(this->entity_category)); + dump_field(out, ESPHOME_PSTR("disabled_by_default"), this->disabled_by_default); + dump_field(out, ESPHOME_PSTR("entity_category"), static_cast(this->entity_category)); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *DateStateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "DateStateResponse"); - dump_field(out, "key", this->key); - dump_field(out, "missing_state", this->missing_state); - dump_field(out, "year", this->year); - dump_field(out, "month", this->month); - dump_field(out, "day", this->day); + MessageDumpHelper helper(out, ESPHOME_PSTR("DateStateResponse")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("missing_state"), this->missing_state); + dump_field(out, ESPHOME_PSTR("year"), this->year); + dump_field(out, ESPHOME_PSTR("month"), this->month); + dump_field(out, ESPHOME_PSTR("day"), this->day); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *DateCommandRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "DateCommandRequest"); - dump_field(out, "key", this->key); - dump_field(out, "year", this->year); - dump_field(out, "month", this->month); - dump_field(out, "day", this->day); + MessageDumpHelper helper(out, ESPHOME_PSTR("DateCommandRequest")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("year"), this->year); + dump_field(out, ESPHOME_PSTR("month"), this->month); + dump_field(out, ESPHOME_PSTR("day"), this->day); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } #endif #ifdef USE_DATETIME_TIME const char *ListEntitiesTimeResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesTimeResponse"); - dump_field(out, "object_id", this->object_id); - dump_field(out, "key", this->key); - dump_field(out, "name", this->name); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesTimeResponse")); + dump_field(out, ESPHOME_PSTR("object_id"), this->object_id); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("name"), this->name); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon); + dump_field(out, ESPHOME_PSTR("icon"), this->icon); #endif - dump_field(out, "disabled_by_default", this->disabled_by_default); - dump_field(out, "entity_category", static_cast(this->entity_category)); + dump_field(out, ESPHOME_PSTR("disabled_by_default"), this->disabled_by_default); + dump_field(out, ESPHOME_PSTR("entity_category"), static_cast(this->entity_category)); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *TimeStateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "TimeStateResponse"); - dump_field(out, "key", this->key); - dump_field(out, "missing_state", this->missing_state); - dump_field(out, "hour", this->hour); - dump_field(out, "minute", this->minute); - dump_field(out, "second", this->second); + MessageDumpHelper helper(out, ESPHOME_PSTR("TimeStateResponse")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("missing_state"), this->missing_state); + dump_field(out, ESPHOME_PSTR("hour"), this->hour); + dump_field(out, ESPHOME_PSTR("minute"), this->minute); + dump_field(out, ESPHOME_PSTR("second"), this->second); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *TimeCommandRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "TimeCommandRequest"); - dump_field(out, "key", this->key); - dump_field(out, "hour", this->hour); - dump_field(out, "minute", this->minute); - dump_field(out, "second", this->second); + MessageDumpHelper helper(out, ESPHOME_PSTR("TimeCommandRequest")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("hour"), this->hour); + dump_field(out, ESPHOME_PSTR("minute"), this->minute); + dump_field(out, ESPHOME_PSTR("second"), this->second); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } #endif #ifdef USE_EVENT const char *ListEntitiesEventResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesEventResponse"); - dump_field(out, "object_id", this->object_id); - dump_field(out, "key", this->key); - dump_field(out, "name", this->name); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesEventResponse")); + dump_field(out, ESPHOME_PSTR("object_id"), this->object_id); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("name"), this->name); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon); + dump_field(out, ESPHOME_PSTR("icon"), this->icon); #endif - dump_field(out, "disabled_by_default", this->disabled_by_default); - dump_field(out, "entity_category", static_cast(this->entity_category)); - dump_field(out, "device_class", this->device_class); + dump_field(out, ESPHOME_PSTR("disabled_by_default"), this->disabled_by_default); + dump_field(out, ESPHOME_PSTR("entity_category"), static_cast(this->entity_category)); + dump_field(out, ESPHOME_PSTR("device_class"), this->device_class); for (const auto &it : *this->event_types) { - dump_field(out, "event_types", it, 4); + dump_field(out, ESPHOME_PSTR("event_types"), it, 4); } #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *EventResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "EventResponse"); - dump_field(out, "key", this->key); - dump_field(out, "event_type", this->event_type); + MessageDumpHelper helper(out, ESPHOME_PSTR("EventResponse")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("event_type"), this->event_type); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } #endif #ifdef USE_VALVE const char *ListEntitiesValveResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesValveResponse"); - dump_field(out, "object_id", this->object_id); - dump_field(out, "key", this->key); - dump_field(out, "name", this->name); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesValveResponse")); + dump_field(out, ESPHOME_PSTR("object_id"), this->object_id); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("name"), this->name); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon); + dump_field(out, ESPHOME_PSTR("icon"), this->icon); #endif - dump_field(out, "disabled_by_default", this->disabled_by_default); - dump_field(out, "entity_category", static_cast(this->entity_category)); - dump_field(out, "device_class", this->device_class); - dump_field(out, "assumed_state", this->assumed_state); - dump_field(out, "supports_position", this->supports_position); - dump_field(out, "supports_stop", this->supports_stop); + dump_field(out, ESPHOME_PSTR("disabled_by_default"), this->disabled_by_default); + dump_field(out, ESPHOME_PSTR("entity_category"), static_cast(this->entity_category)); + dump_field(out, ESPHOME_PSTR("device_class"), this->device_class); + dump_field(out, ESPHOME_PSTR("assumed_state"), this->assumed_state); + dump_field(out, ESPHOME_PSTR("supports_position"), this->supports_position); + dump_field(out, ESPHOME_PSTR("supports_stop"), this->supports_stop); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *ValveStateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ValveStateResponse"); - dump_field(out, "key", this->key); - dump_field(out, "position", this->position); - dump_field(out, "current_operation", static_cast(this->current_operation)); + MessageDumpHelper helper(out, ESPHOME_PSTR("ValveStateResponse")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("position"), this->position); + dump_field(out, ESPHOME_PSTR("current_operation"), static_cast(this->current_operation)); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *ValveCommandRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ValveCommandRequest"); - dump_field(out, "key", this->key); - dump_field(out, "has_position", this->has_position); - dump_field(out, "position", this->position); - dump_field(out, "stop", this->stop); + MessageDumpHelper helper(out, ESPHOME_PSTR("ValveCommandRequest")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("has_position"), this->has_position); + dump_field(out, ESPHOME_PSTR("position"), this->position); + dump_field(out, ESPHOME_PSTR("stop"), this->stop); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } #endif #ifdef USE_DATETIME_DATETIME const char *ListEntitiesDateTimeResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesDateTimeResponse"); - dump_field(out, "object_id", this->object_id); - dump_field(out, "key", this->key); - dump_field(out, "name", this->name); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesDateTimeResponse")); + dump_field(out, ESPHOME_PSTR("object_id"), this->object_id); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("name"), this->name); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon); + dump_field(out, ESPHOME_PSTR("icon"), this->icon); #endif - dump_field(out, "disabled_by_default", this->disabled_by_default); - dump_field(out, "entity_category", static_cast(this->entity_category)); + dump_field(out, ESPHOME_PSTR("disabled_by_default"), this->disabled_by_default); + dump_field(out, ESPHOME_PSTR("entity_category"), static_cast(this->entity_category)); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *DateTimeStateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "DateTimeStateResponse"); - dump_field(out, "key", this->key); - dump_field(out, "missing_state", this->missing_state); - dump_field(out, "epoch_seconds", this->epoch_seconds); + MessageDumpHelper helper(out, ESPHOME_PSTR("DateTimeStateResponse")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("missing_state"), this->missing_state); + dump_field(out, ESPHOME_PSTR("epoch_seconds"), this->epoch_seconds); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *DateTimeCommandRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "DateTimeCommandRequest"); - dump_field(out, "key", this->key); - dump_field(out, "epoch_seconds", this->epoch_seconds); + MessageDumpHelper helper(out, ESPHOME_PSTR("DateTimeCommandRequest")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("epoch_seconds"), this->epoch_seconds); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } #endif #ifdef USE_UPDATE const char *ListEntitiesUpdateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesUpdateResponse"); - dump_field(out, "object_id", this->object_id); - dump_field(out, "key", this->key); - dump_field(out, "name", this->name); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesUpdateResponse")); + dump_field(out, ESPHOME_PSTR("object_id"), this->object_id); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("name"), this->name); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon); + dump_field(out, ESPHOME_PSTR("icon"), this->icon); #endif - dump_field(out, "disabled_by_default", this->disabled_by_default); - dump_field(out, "entity_category", static_cast(this->entity_category)); - dump_field(out, "device_class", this->device_class); + dump_field(out, ESPHOME_PSTR("disabled_by_default"), this->disabled_by_default); + dump_field(out, ESPHOME_PSTR("entity_category"), static_cast(this->entity_category)); + dump_field(out, ESPHOME_PSTR("device_class"), this->device_class); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *UpdateStateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "UpdateStateResponse"); - dump_field(out, "key", this->key); - dump_field(out, "missing_state", this->missing_state); - dump_field(out, "in_progress", this->in_progress); - dump_field(out, "has_progress", this->has_progress); - dump_field(out, "progress", this->progress); - dump_field(out, "current_version", this->current_version); - dump_field(out, "latest_version", this->latest_version); - dump_field(out, "title", this->title); - dump_field(out, "release_summary", this->release_summary); - dump_field(out, "release_url", this->release_url); + MessageDumpHelper helper(out, ESPHOME_PSTR("UpdateStateResponse")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("missing_state"), this->missing_state); + dump_field(out, ESPHOME_PSTR("in_progress"), this->in_progress); + dump_field(out, ESPHOME_PSTR("has_progress"), this->has_progress); + dump_field(out, ESPHOME_PSTR("progress"), this->progress); + dump_field(out, ESPHOME_PSTR("current_version"), this->current_version); + dump_field(out, ESPHOME_PSTR("latest_version"), this->latest_version); + dump_field(out, ESPHOME_PSTR("title"), this->title); + dump_field(out, ESPHOME_PSTR("release_summary"), this->release_summary); + dump_field(out, ESPHOME_PSTR("release_url"), this->release_url); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *UpdateCommandRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "UpdateCommandRequest"); - dump_field(out, "key", this->key); - dump_field(out, "command", static_cast(this->command)); + MessageDumpHelper helper(out, ESPHOME_PSTR("UpdateCommandRequest")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("command"), static_cast(this->command)); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } #endif #ifdef USE_ZWAVE_PROXY const char *ZWaveProxyFrame::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ZWaveProxyFrame"); - dump_bytes_field(out, "data", this->data, this->data_len); + MessageDumpHelper helper(out, ESPHOME_PSTR("ZWaveProxyFrame")); + dump_bytes_field(out, ESPHOME_PSTR("data"), this->data, this->data_len); return out.c_str(); } const char *ZWaveProxyRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ZWaveProxyRequest"); - dump_field(out, "type", static_cast(this->type)); - dump_bytes_field(out, "data", this->data, this->data_len); + MessageDumpHelper helper(out, ESPHOME_PSTR("ZWaveProxyRequest")); + dump_field(out, ESPHOME_PSTR("type"), static_cast(this->type)); + dump_bytes_field(out, ESPHOME_PSTR("data"), this->data, this->data_len); return out.c_str(); } #endif #ifdef USE_INFRARED const char *ListEntitiesInfraredResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesInfraredResponse"); - dump_field(out, "object_id", this->object_id); - dump_field(out, "key", this->key); - dump_field(out, "name", this->name); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesInfraredResponse")); + dump_field(out, ESPHOME_PSTR("object_id"), this->object_id); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("name"), this->name); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon); + dump_field(out, ESPHOME_PSTR("icon"), this->icon); #endif - dump_field(out, "disabled_by_default", this->disabled_by_default); - dump_field(out, "entity_category", static_cast(this->entity_category)); + dump_field(out, ESPHOME_PSTR("disabled_by_default"), this->disabled_by_default); + dump_field(out, ESPHOME_PSTR("entity_category"), static_cast(this->entity_category)); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif - dump_field(out, "capabilities", this->capabilities); + dump_field(out, ESPHOME_PSTR("capabilities"), this->capabilities); return out.c_str(); } #endif #ifdef USE_IR_RF const char *InfraredRFTransmitRawTimingsRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "InfraredRFTransmitRawTimingsRequest"); + MessageDumpHelper helper(out, ESPHOME_PSTR("InfraredRFTransmitRawTimingsRequest")); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif - dump_field(out, "key", this->key); - dump_field(out, "carrier_frequency", this->carrier_frequency); - dump_field(out, "repeat_count", this->repeat_count); - out.append(" timings: "); - out.append("packed buffer ["); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("carrier_frequency"), this->carrier_frequency); + dump_field(out, ESPHOME_PSTR("repeat_count"), this->repeat_count); + out.append(2, ' ').append_p(ESPHOME_PSTR("timings")).append(": "); + out.append_p(ESPHOME_PSTR("packed buffer [")); append_uint(out, this->timings_count_); - out.append(" values, "); + out.append_p(ESPHOME_PSTR(" values, ")); append_uint(out, this->timings_length_); - out.append(" bytes]\n"); + out.append_p(ESPHOME_PSTR(" bytes]\n")); return out.c_str(); } const char *InfraredRFReceiveEvent::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "InfraredRFReceiveEvent"); + MessageDumpHelper helper(out, ESPHOME_PSTR("InfraredRFReceiveEvent")); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif - dump_field(out, "key", this->key); + dump_field(out, ESPHOME_PSTR("key"), this->key); for (const auto &it : *this->timings) { - dump_field(out, "timings", it, 4); + dump_field(out, ESPHOME_PSTR("timings"), it, 4); } return out.c_str(); } #endif #ifdef USE_SERIAL_PROXY const char *SerialProxyConfigureRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "SerialProxyConfigureRequest"); - dump_field(out, "instance", this->instance); - dump_field(out, "baudrate", this->baudrate); - dump_field(out, "flow_control", this->flow_control); - dump_field(out, "parity", static_cast(this->parity)); - dump_field(out, "stop_bits", this->stop_bits); - dump_field(out, "data_size", this->data_size); + MessageDumpHelper helper(out, ESPHOME_PSTR("SerialProxyConfigureRequest")); + dump_field(out, ESPHOME_PSTR("instance"), this->instance); + dump_field(out, ESPHOME_PSTR("baudrate"), this->baudrate); + dump_field(out, ESPHOME_PSTR("flow_control"), this->flow_control); + dump_field(out, ESPHOME_PSTR("parity"), static_cast(this->parity)); + dump_field(out, ESPHOME_PSTR("stop_bits"), this->stop_bits); + dump_field(out, ESPHOME_PSTR("data_size"), this->data_size); return out.c_str(); } const char *SerialProxyDataReceived::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "SerialProxyDataReceived"); - dump_field(out, "instance", this->instance); - dump_bytes_field(out, "data", this->data_ptr_, this->data_len_); + MessageDumpHelper helper(out, ESPHOME_PSTR("SerialProxyDataReceived")); + dump_field(out, ESPHOME_PSTR("instance"), this->instance); + dump_bytes_field(out, ESPHOME_PSTR("data"), this->data_ptr_, this->data_len_); return out.c_str(); } const char *SerialProxyWriteRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "SerialProxyWriteRequest"); - dump_field(out, "instance", this->instance); - dump_bytes_field(out, "data", this->data, this->data_len); + MessageDumpHelper helper(out, ESPHOME_PSTR("SerialProxyWriteRequest")); + dump_field(out, ESPHOME_PSTR("instance"), this->instance); + dump_bytes_field(out, ESPHOME_PSTR("data"), this->data, this->data_len); return out.c_str(); } const char *SerialProxySetModemPinsRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "SerialProxySetModemPinsRequest"); - dump_field(out, "instance", this->instance); - dump_field(out, "line_states", this->line_states); + MessageDumpHelper helper(out, ESPHOME_PSTR("SerialProxySetModemPinsRequest")); + dump_field(out, ESPHOME_PSTR("instance"), this->instance); + dump_field(out, ESPHOME_PSTR("line_states"), this->line_states); return out.c_str(); } const char *SerialProxyGetModemPinsRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "SerialProxyGetModemPinsRequest"); - dump_field(out, "instance", this->instance); + MessageDumpHelper helper(out, ESPHOME_PSTR("SerialProxyGetModemPinsRequest")); + dump_field(out, ESPHOME_PSTR("instance"), this->instance); return out.c_str(); } const char *SerialProxyGetModemPinsResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "SerialProxyGetModemPinsResponse"); - dump_field(out, "instance", this->instance); - dump_field(out, "line_states", this->line_states); + MessageDumpHelper helper(out, ESPHOME_PSTR("SerialProxyGetModemPinsResponse")); + dump_field(out, ESPHOME_PSTR("instance"), this->instance); + dump_field(out, ESPHOME_PSTR("line_states"), this->line_states); return out.c_str(); } const char *SerialProxyRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "SerialProxyRequest"); - dump_field(out, "instance", this->instance); - dump_field(out, "type", static_cast(this->type)); + MessageDumpHelper helper(out, ESPHOME_PSTR("SerialProxyRequest")); + dump_field(out, ESPHOME_PSTR("instance"), this->instance); + dump_field(out, ESPHOME_PSTR("type"), static_cast(this->type)); return out.c_str(); } const char *SerialProxyRequestResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "SerialProxyRequestResponse"); - dump_field(out, "instance", this->instance); - dump_field(out, "type", static_cast(this->type)); - dump_field(out, "status", static_cast(this->status)); - dump_field(out, "error_message", this->error_message); + MessageDumpHelper helper(out, ESPHOME_PSTR("SerialProxyRequestResponse")); + dump_field(out, ESPHOME_PSTR("instance"), this->instance); + dump_field(out, ESPHOME_PSTR("type"), static_cast(this->type)); + dump_field(out, ESPHOME_PSTR("status"), static_cast(this->status)); + dump_field(out, ESPHOME_PSTR("error_message"), this->error_message); return out.c_str(); } #endif #ifdef USE_BLUETOOTH_PROXY const char *BluetoothSetConnectionParamsRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothSetConnectionParamsRequest"); - dump_field(out, "address", this->address); - dump_field(out, "min_interval", this->min_interval); - dump_field(out, "max_interval", this->max_interval); - dump_field(out, "latency", this->latency); - dump_field(out, "timeout", this->timeout); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothSetConnectionParamsRequest")); + dump_field(out, ESPHOME_PSTR("address"), this->address); + dump_field(out, ESPHOME_PSTR("min_interval"), this->min_interval); + dump_field(out, ESPHOME_PSTR("max_interval"), this->max_interval); + dump_field(out, ESPHOME_PSTR("latency"), this->latency); + dump_field(out, ESPHOME_PSTR("timeout"), this->timeout); return out.c_str(); } const char *BluetoothSetConnectionParamsResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothSetConnectionParamsResponse"); - dump_field(out, "address", this->address); - dump_field(out, "error", this->error); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothSetConnectionParamsResponse")); + dump_field(out, ESPHOME_PSTR("address"), this->address); + dump_field(out, ESPHOME_PSTR("error"), this->error); return out.c_str(); } #endif diff --git a/esphome/components/api/api_pb2_service.cpp b/esphome/components/api/api_pb2_service.cpp index d86cf912db..b41233eddd 100644 --- a/esphome/components/api/api_pb2_service.cpp +++ b/esphome/components/api/api_pb2_service.cpp @@ -9,8 +9,8 @@ namespace esphome::api { static const char *const TAG = "api.service"; #ifdef HAS_PROTO_MESSAGE_DUMP -void APIServerConnectionBase::log_send_message_(const char *name, const char *dump) { - ESP_LOGVV(TAG, "send_message %s: %s", name, dump); +void APIServerConnectionBase::log_send_message_(const LogString *name, const char *dump) { + ESP_LOGVV(TAG, "send_message %s: %s", LOG_STR_ARG(name), dump); } void APIServerConnectionBase::log_receive_message_(const LogString *name, const ProtoMessage &msg) { DumpBuffer dump_buf; diff --git a/esphome/components/api/api_pb2_service.h b/esphome/components/api/api_pb2_service.h index 4925a6497a..6ff988902f 100644 --- a/esphome/components/api/api_pb2_service.h +++ b/esphome/components/api/api_pb2_service.h @@ -12,7 +12,7 @@ class APIServerConnectionBase { public: #ifdef HAS_PROTO_MESSAGE_DUMP protected: - void log_send_message_(const char *name, const char *dump); + void log_send_message_(const LogString *name, const char *dump); void log_receive_message_(const LogString *name, const ProtoMessage &msg); void log_receive_message_(const LogString *name); diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index d6f9c947d7..95a79105a3 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -5,6 +5,7 @@ #include "esphome/core/component.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" +#include "esphome/core/progmem.h" #include "esphome/core/string_ref.h" #include @@ -400,6 +401,23 @@ class DumpBuffer { return *this; } + /// Append a PROGMEM string (flash-safe on ESP8266, regular append on other platforms) + DumpBuffer &append_p(const char *str) { + if (str) { +#ifdef USE_ESP8266 + append_p_esp8266(str); +#else + append_impl_(str, strlen(str)); +#endif + } + return *this; + } + +#ifdef USE_ESP8266 + /// Out-of-line ESP8266 PROGMEM append to avoid inlining strlen_P/memcpy_P at every call site + void append_p_esp8266(const char *str); +#endif + const char *c_str() const { return buf_; } size_t size() const { return pos_; } @@ -445,7 +463,7 @@ class ProtoMessage { uint32_t calculate_size() const { return 0; } #ifdef HAS_PROTO_MESSAGE_DUMP virtual const char *dump_to(DumpBuffer &out) const = 0; - virtual const char *message_name() const { return "unknown"; } + virtual const LogString *message_name() const { return LOG_STR("unknown"); } #endif #ifndef USE_HOST diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 0bb569fdb5..81ea93caf4 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -248,7 +248,7 @@ class TypeInfo(ABC): @property def dump_content(self) -> str: # Default implementation - subclasses can override if they need special handling - return f'dump_field(out, "{self.name}", {self.dump_field_value(f"this->{self.field_name}")});' + return f'dump_field(out, ESPHOME_PSTR("{self.name}"), {self.dump_field_value(f"this->{self.field_name}")});' @abstractmethod def dump(self, name: str) -> str: @@ -665,14 +665,14 @@ class StringType(TypeInfo): def dump_content(self) -> str: # For SOURCE_CLIENT only, use std::string if not self._needs_encode: - return f'dump_field(out, "{self.name}", this->{self.field_name});' + return f'dump_field(out, ESPHOME_PSTR("{self.name}"), this->{self.field_name});' # For SOURCE_SERVER, use StringRef with _ref_ suffix if not self._needs_decode: - return f'dump_field(out, "{self.name}", this->{self.field_name}_ref_);' + return f'dump_field(out, ESPHOME_PSTR("{self.name}"), this->{self.field_name}_ref_);' # For SOURCE_BOTH, we need custom logic - o = f'out.append(" {self.name}: ");\n' + o = f'out.append(2, \' \').append_p(ESPHOME_PSTR("{self.name}")).append(": ");\n' o += self.dump(f"this->{self.field_name}") + "\n" o += 'out.append("\\n");' return o @@ -745,7 +745,7 @@ class MessageType(TypeInfo): @property def dump_content(self) -> str: - o = f'out.append(" {self.name}: ");\n' + o = f'out.append(2, \' \').append_p(ESPHOME_PSTR("{self.name}")).append(": ");\n' o += f"this->{self.field_name}.dump_to(out);\n" o += 'out.append("\\n");' return o @@ -831,7 +831,7 @@ class BytesType(TypeInfo): # For SOURCE_CLIENT only, always use std::string if not self._needs_encode: return ( - f'dump_bytes_field(out, "{self.name}", ' + f'dump_bytes_field(out, ESPHOME_PSTR("{self.name}"), ' f"reinterpret_cast(this->{self.field_name}.data()), " f"this->{self.field_name}.size());" ) @@ -839,17 +839,17 @@ class BytesType(TypeInfo): # For SOURCE_SERVER, always use pointer/length if not self._needs_decode: return ( - f'dump_bytes_field(out, "{self.name}", ' + f'dump_bytes_field(out, ESPHOME_PSTR("{self.name}"), ' f"this->{self.field_name}_ptr_, this->{self.field_name}_len_);" ) # For SOURCE_BOTH, check if pointer is set (sending) or use string (received) return ( f"if (this->{self.field_name}_ptr_ != nullptr) {{\n" - f' dump_bytes_field(out, "{self.name}", ' + f' dump_bytes_field(out, ESPHOME_PSTR("{self.name}"), ' f"this->{self.field_name}_ptr_, this->{self.field_name}_len_);\n" f"}} else {{\n" - f' dump_bytes_field(out, "{self.name}", ' + f' dump_bytes_field(out, ESPHOME_PSTR("{self.name}"), ' f"reinterpret_cast(this->{self.field_name}.data()), " f"this->{self.field_name}.size());\n" f"}}" @@ -928,7 +928,7 @@ class PointerToBytesBufferType(PointerToBufferTypeBase): @property def dump_content(self) -> str: return ( - f'dump_bytes_field(out, "{self.name}", ' + f'dump_bytes_field(out, ESPHOME_PSTR("{self.name}"), ' f"this->{self.field_name}, this->{self.field_name}_len);" ) @@ -976,7 +976,7 @@ class PointerToStringBufferType(PointerToBufferTypeBase): @property def dump_content(self) -> str: - return f'dump_field(out, "{self.name}", this->{self.field_name});' + return f'dump_field(out, ESPHOME_PSTR("{self.name}"), this->{self.field_name});' def get_size_calculation(self, name: str, force: bool = False) -> str: return f"size += ProtoSize::calc_length({self.calculate_field_id_size()}, this->{self.field_name}.size());" @@ -1036,12 +1036,12 @@ class PackedBufferTypeInfo(TypeInfo): def dump_content(self) -> str: """Dump shows buffer info but not decoded values.""" return ( - f'out.append(" {self.name}: ");\n' - + 'out.append("packed buffer [");\n' + f'out.append(2, \' \').append_p(ESPHOME_PSTR("{self.name}")).append(": ");\n' + + 'out.append_p(ESPHOME_PSTR("packed buffer ["));\n' + f"append_uint(out, this->{self.field_name}_count_);\n" - + 'out.append(" values, ");\n' + + 'out.append_p(ESPHOME_PSTR(" values, "));\n' + f"append_uint(out, this->{self.field_name}_length_);\n" - + 'out.append(" bytes]\\n");' + + 'out.append_p(ESPHOME_PSTR(" bytes]\\n"));' ) def dump(self, name: str) -> str: @@ -1134,7 +1134,7 @@ class FixedArrayBytesType(TypeInfo): @property def dump_content(self) -> str: return ( - f'dump_bytes_field(out, "{self.name}", ' + f'dump_bytes_field(out, ESPHOME_PSTR("{self.name}"), ' f"this->{self.field_name}, this->{self.field_name}_len);" ) @@ -1204,7 +1204,7 @@ class EnumType(TypeInfo): return f"buffer.{self.encode_func}({self.number}, static_cast(this->{self.field_name}));" def dump(self, name: str) -> str: - return f"out.append(proto_enum_to_string<{self.cpp_type}>({name}));" + return f"out.append_p(proto_enum_to_string<{self.cpp_type}>({name}));" def dump_field_value(self, value: str) -> str: # Enums need explicit cast for the template @@ -1326,15 +1326,15 @@ def _generate_array_dump_content( # Check if underlying type can use dump_field if is_const_char_ptr: # Special case for const char* - use it directly - o += f' dump_field(out, "{name}", it, 4);\n' + o += f' dump_field(out, ESPHOME_PSTR("{name}"), it, 4);\n' elif ti.can_use_dump_field(): # For types that have dump_field overloads, use them with extra indent # std::vector iterators return proxy objects, need explicit cast value_expr = "static_cast(it)" if is_bool else ti.dump_field_value("it") - o += f' dump_field(out, "{name}", {value_expr}, 4);\n' + o += f' dump_field(out, ESPHOME_PSTR("{name}"), {value_expr}, 4);\n' else: # For complex types (messages, bytes), use the old pattern - o += f' out.append(" {name}: ");\n' + o += f' out.append(4, \' \').append_p(ESPHOME_PSTR("{name}")).append(": ");\n' o += indent(ti.dump("it")) + "\n" o += ' out.append("\\n");\n' o += "}" @@ -1543,9 +1543,9 @@ class FixedArrayWithLengthRepeatedType(FixedArrayRepeatedType): o = f"for (uint16_t i = 0; i < this->{self.field_name}_len; i++) {{\n" # Check if underlying type can use dump_field if self._ti.can_use_dump_field(): - o += f' dump_field(out, "{self.name}", {self._ti.dump_field_value(f"this->{self.field_name}[i]")}, 4);\n' + o += f' dump_field(out, ESPHOME_PSTR("{self.name}"), {self._ti.dump_field_value(f"this->{self.field_name}[i]")}, 4);\n' else: - o += f' out.append(" {self.name}: ");\n' + o += f' out.append(4, \' \').append_p(ESPHOME_PSTR("{self.name}")).append(": ");\n' o += indent(self._ti.dump(f"this->{self.field_name}[i]")) + "\n" o += ' out.append("\\n");\n' o += "}" @@ -2023,9 +2023,9 @@ def build_enum_type(desc, enum_ifdef_map) -> tuple[str, str, str]: dump_cpp += " switch (value) {\n" for v in desc.value: dump_cpp += f" case enums::{v.name}:\n" - dump_cpp += f' return "{v.name}";\n' + dump_cpp += f' return ESPHOME_PSTR("{v.name}");\n' dump_cpp += " default:\n" - dump_cpp += ' return "UNKNOWN";\n' + dump_cpp += ' return ESPHOME_PSTR("UNKNOWN");\n' dump_cpp += " }\n" dump_cpp += "}\n" @@ -2107,7 +2107,7 @@ def build_message_type( public_content.append("#ifdef HAS_PROTO_MESSAGE_DUMP") snake_name = camel_to_snake(desc.name) public_content.append( - f'const char *message_name() const override {{ return "{snake_name}"; }}' + f'const LogString *message_name() const override {{ return LOG_STR("{snake_name}"); }}' ) public_content.append("#endif") @@ -2315,12 +2315,12 @@ def build_message_type( if dump: # Always use MessageDumpHelper for consistent output formatting dump_impl += "\n" - dump_impl += f' MessageDumpHelper helper(out, "{desc.name}");\n' + dump_impl += f' MessageDumpHelper helper(out, ESPHOME_PSTR("{desc.name}"));\n' dump_impl += indent("\n".join(dump)) + "\n" dump_impl += " return out.c_str();\n" else: dump_impl += "\n" - dump_impl += f' out.append("{desc.name} {{}}");\n' + dump_impl += f' out.append_p(ESPHOME_PSTR("{desc.name} {{}}"));\n' dump_impl += " return out.c_str();\n" dump_impl += "}\n" @@ -2707,6 +2707,7 @@ namespace esphome::api { dump_cpp += """\ #include "api_pb2.h" #include "esphome/core/helpers.h" +#include "esphome/core/progmem.h" #include @@ -2714,6 +2715,21 @@ namespace esphome::api { namespace esphome::api { +#ifdef USE_ESP8266 +// Out-of-line to avoid inlining strlen_P/memcpy_P at every call site +void DumpBuffer::append_p_esp8266(const char *str) { + size_t len = strlen_P(str); + size_t space = CAPACITY - 1 - pos_; + if (len > space) + len = space; + if (len > 0) { + memcpy_P(buf_ + pos_, str, len); + pos_ += len; + buf_[pos_] = '\\0'; + } +} +#endif + // Helper function to append a quoted string, handling empty StringRef static inline void append_quoted_string(DumpBuffer &out, const StringRef &ref) { out.append("'"); @@ -2724,8 +2740,9 @@ static inline void append_quoted_string(DumpBuffer &out, const StringRef &ref) { } // Common helpers for dump_field functions +// field_name is a PROGMEM pointer (flash on ESP8266, regular pointer on other platforms) static inline void append_field_prefix(DumpBuffer &out, const char *field_name, int indent) { - out.append(indent, ' ').append(field_name).append(": "); + out.append(indent, ' ').append_p(field_name).append(": "); } static inline void append_uint(DumpBuffer &out, uint32_t value) { @@ -2733,10 +2750,11 @@ static inline void append_uint(DumpBuffer &out, uint32_t value) { } // RAII helper for message dump formatting +// message_name is a PROGMEM pointer (flash on ESP8266, regular pointer on other platforms) class MessageDumpHelper { public: MessageDumpHelper(DumpBuffer &out, const char *message_name) : out_(out) { - out_.append(message_name); + out_.append_p(message_name); out_.append(" {\\n"); } ~MessageDumpHelper() { out_.append(" }"); } @@ -2746,6 +2764,10 @@ class MessageDumpHelper { }; // Helper functions to reduce code duplication in dump methods +// field_name parameters are PROGMEM pointers (flash on ESP8266, regular pointers on other platforms) +// Not all overloads are used in every build (depends on enabled components) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wunused-function" static void dump_field(DumpBuffer &out, const char *field_name, int32_t value, int indent = 2) { append_field_prefix(out, field_name, indent); out.set_pos(buf_append_printf(out.data(), DumpBuffer::CAPACITY, out.pos(), "%" PRId32 "\\n", value)); @@ -2790,21 +2812,23 @@ static void dump_field(DumpBuffer &out, const char *field_name, const char *valu out.append("\\n"); } -template -static void dump_field(DumpBuffer &out, const char *field_name, T value, int indent = 2) { +// proto_enum_to_string returns PROGMEM pointers, so use append_p +template static void dump_field(DumpBuffer &out, const char *field_name, T value, int indent = 2) { append_field_prefix(out, field_name, indent); - out.append(proto_enum_to_string(value)); + out.append_p(proto_enum_to_string(value)); out.append("\\n"); } // Helper for bytes fields - uses stack buffer to avoid heap allocation // Buffer sized for 160 bytes of data (480 chars with separators) to fit typical log buffer +// field_name is a PROGMEM pointer (flash on ESP8266, regular pointer on other platforms) static void dump_bytes_field(DumpBuffer &out, const char *field_name, const uint8_t *data, size_t len, int indent = 2) { char hex_buf[format_hex_pretty_size(160)]; append_field_prefix(out, field_name, indent); format_hex_pretty_to(hex_buf, data, len); out.append(hex_buf).append("\\n"); } +#pragma GCC diagnostic pop """ @@ -2977,7 +3001,7 @@ static const char *const TAG = "api.service"; # Add logging helper method declarations hpp += "#ifdef HAS_PROTO_MESSAGE_DUMP\n" hpp += " protected:\n" - hpp += " void log_send_message_(const char *name, const char *dump);\n" + hpp += " void log_send_message_(const LogString *name, const char *dump);\n" hpp += ( " void log_receive_message_(const LogString *name, const ProtoMessage &msg);\n" ) @@ -2990,10 +3014,8 @@ static const char *const TAG = "api.service"; # Add logging helper method implementations to cpp cpp += "#ifdef HAS_PROTO_MESSAGE_DUMP\n" - cpp += ( - f"void {class_name}::log_send_message_(const char *name, const char *dump) {{\n" - ) - cpp += ' ESP_LOGVV(TAG, "send_message %s: %s", name, dump);\n' + cpp += f"void {class_name}::log_send_message_(const LogString *name, const char *dump) {{\n" + cpp += ' ESP_LOGVV(TAG, "send_message %s: %s", LOG_STR_ARG(name), dump);\n' cpp += "}\n" cpp += f"void {class_name}::log_receive_message_(const LogString *name, const ProtoMessage &msg) {{\n" cpp += " DumpBuffer dump_buf;\n" From 13d3968d9b9220fd777b953aac8efbbba1819013 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Mar 2026 13:41:09 -1000 Subject: [PATCH 122/166] [api] Avoid heap allocation in PSK update timeout lambda (#14921) Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- esphome/components/api/api_server.cpp | 28 ++++++++++++++++++--------- esphome/components/api/api_server.h | 4 +++- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 17d69405ad..1151bc5983 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -46,10 +46,8 @@ void APIServer::setup() { #ifndef USE_API_NOISE_PSK_FROM_YAML // Only load saved PSK if not set from YAML - SavedNoisePsk noise_pref_saved{}; - if (this->noise_pref_.load(&noise_pref_saved)) { + if (this->load_and_apply_noise_psk_()) { ESP_LOGD(TAG, "Loaded saved Noise PSK"); - this->set_noise_psk(noise_pref_saved.psk); } #endif #endif @@ -514,7 +512,7 @@ void APIServer::set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeo #ifdef USE_API_NOISE bool APIServer::update_noise_psk_(const SavedNoisePsk &new_psk, const LogString *save_log_msg, - const LogString *fail_log_msg, const psk_t &active_psk, bool make_active) { + const LogString *fail_log_msg, bool make_active) { if (!this->noise_pref_.save(&new_psk)) { ESP_LOGW(TAG, "%s", LOG_STR_ARG(fail_log_msg)); return false; @@ -526,9 +524,14 @@ bool APIServer::update_noise_psk_(const SavedNoisePsk &new_psk, const LogString } ESP_LOGD(TAG, "%s", LOG_STR_ARG(save_log_msg)); if (make_active) { - this->set_timeout(100, [this, active_psk]() { + this->set_timeout(100, [this]() { + // Re-read the PSK from preferences rather than capturing the 32-byte array + // in the lambda (which would exceed std::function SBO and heap-allocate). + if (!this->load_and_apply_noise_psk_()) { + ESP_LOGW(TAG, "Failed to load saved PSK for activation"); + return; + } ESP_LOGW(TAG, "Disconnecting all clients to reset PSK"); - this->set_noise_psk(active_psk); for (auto &c : this->clients_) { DisconnectRequest req; c->send_message(req); @@ -538,6 +541,14 @@ bool APIServer::update_noise_psk_(const SavedNoisePsk &new_psk, const LogString return true; } +bool APIServer::load_and_apply_noise_psk_() { + SavedNoisePsk saved{}; + if (!this->noise_pref_.load(&saved)) + return false; + this->set_noise_psk(saved.psk); + return true; +} + bool APIServer::save_noise_psk(psk_t psk, bool make_active) { #ifdef USE_API_NOISE_PSK_FROM_YAML // When PSK is set from YAML, this function should never be called @@ -552,7 +563,7 @@ bool APIServer::save_noise_psk(psk_t psk, bool make_active) { } SavedNoisePsk new_saved_psk{psk}; - return this->update_noise_psk_(new_saved_psk, LOG_STR("Noise PSK saved"), LOG_STR("Failed to save Noise PSK"), psk, + return this->update_noise_psk_(new_saved_psk, LOG_STR("Noise PSK saved"), LOG_STR("Failed to save Noise PSK"), make_active); #endif } @@ -564,8 +575,7 @@ bool APIServer::clear_noise_psk(bool make_active) { return false; #else SavedNoisePsk empty_psk{}; - psk_t empty{}; - return this->update_noise_psk_(empty_psk, LOG_STR("Noise PSK cleared"), LOG_STR("Failed to clear Noise PSK"), empty, + return this->update_noise_psk_(empty_psk, LOG_STR("Noise PSK cleared"), LOG_STR("Failed to clear Noise PSK"), make_active); #endif } diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index ccba6deb00..65076879a2 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -239,7 +239,9 @@ class APIServer final : public Component, #ifdef USE_API_NOISE bool update_noise_psk_(const SavedNoisePsk &new_psk, const LogString *save_log_msg, const LogString *fail_log_msg, - const psk_t &active_psk, bool make_active); + bool make_active); + // Load saved PSK from preferences and apply it. Returns true on success. + bool load_and_apply_noise_psk_(); #endif // USE_API_NOISE #ifdef USE_API_HOMEASSISTANT_STATES // Helper methods to reduce code duplication From a3d9854704a7c448908847f95ea5179e54094547 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Mar 2026 13:56:36 -1000 Subject: [PATCH 123/166] [gpio] Remove redundant last_state_ and pack GPIOBinarySensor fields (#15113) --- .../gpio/binary_sensor/gpio_binary_sensor.cpp | 26 +++++++++---------- .../gpio/binary_sensor/gpio_binary_sensor.h | 20 +++++++------- 2 files changed, 22 insertions(+), 24 deletions(-) diff --git a/esphome/components/gpio/binary_sensor/gpio_binary_sensor.cpp b/esphome/components/gpio/binary_sensor/gpio_binary_sensor.cpp index 38ebbc90e4..39b1a2f713 100644 --- a/esphome/components/gpio/binary_sensor/gpio_binary_sensor.cpp +++ b/esphome/components/gpio/binary_sensor/gpio_binary_sensor.cpp @@ -23,9 +23,8 @@ static const LogString *gpio_mode_to_string(bool use_interrupt) { void IRAM_ATTR GPIOBinarySensorStore::gpio_intr(GPIOBinarySensorStore *arg) { bool new_state = arg->isr_pin_.digital_read(); - if (new_state != arg->last_state_) { + if (new_state != arg->state_) { arg->state_ = new_state; - arg->last_state_ = new_state; arg->changed_ = true; // Wake up the component from its disabled loop state if (arg->component_ != nullptr) { @@ -34,28 +33,27 @@ void IRAM_ATTR GPIOBinarySensorStore::gpio_intr(GPIOBinarySensorStore *arg) { } } -void GPIOBinarySensorStore::setup(InternalGPIOPin *pin, gpio::InterruptType type, Component *component) { +void GPIOBinarySensorStore::setup(InternalGPIOPin *pin, Component *component) { pin->setup(); this->isr_pin_ = pin->to_isr(); this->component_ = component; // Read initial state - this->last_state_ = pin->digital_read(); - this->state_ = this->last_state_; + this->state_ = pin->digital_read(); // Attach interrupt - from this point on, any changes will be caught by the interrupt - pin->attach_interrupt(&GPIOBinarySensorStore::gpio_intr, this, type); + pin->attach_interrupt(&GPIOBinarySensorStore::gpio_intr, this, this->interrupt_type_); } void GPIOBinarySensor::setup() { - if (this->use_interrupt_ && !this->pin_->is_internal()) { + if (this->store_.use_interrupt_ && !this->pin_->is_internal()) { ESP_LOGD(TAG, "GPIO is not internal, falling back to polling mode"); - this->use_interrupt_ = false; + this->store_.use_interrupt_ = false; } - if (this->use_interrupt_) { + if (this->store_.use_interrupt_) { auto *internal_pin = static_cast(this->pin_); - this->store_.setup(internal_pin, this->interrupt_type_, this); + this->store_.setup(internal_pin, this); this->publish_initial_state(this->store_.get_state()); } else { this->pin_->setup(); @@ -66,14 +64,14 @@ void GPIOBinarySensor::setup() { void GPIOBinarySensor::dump_config() { LOG_BINARY_SENSOR("", "GPIO Binary Sensor", this); LOG_PIN(" Pin: ", this->pin_); - ESP_LOGCONFIG(TAG, " Mode: %s", LOG_STR_ARG(gpio_mode_to_string(this->use_interrupt_))); - if (this->use_interrupt_) { - ESP_LOGCONFIG(TAG, " Interrupt Type: %s", LOG_STR_ARG(interrupt_type_to_string(this->interrupt_type_))); + ESP_LOGCONFIG(TAG, " Mode: %s", LOG_STR_ARG(gpio_mode_to_string(this->store_.use_interrupt_))); + if (this->store_.use_interrupt_) { + ESP_LOGCONFIG(TAG, " Interrupt Type: %s", LOG_STR_ARG(interrupt_type_to_string(this->store_.interrupt_type_))); } } void GPIOBinarySensor::loop() { - if (this->use_interrupt_) { + if (this->store_.use_interrupt_) { if (this->store_.is_changed()) { // Clear the flag immediately to minimize the window where we might miss changes this->store_.clear_changed(); diff --git a/esphome/components/gpio/binary_sensor/gpio_binary_sensor.h b/esphome/components/gpio/binary_sensor/gpio_binary_sensor.h index 8b1cc29613..24efc2a0e6 100644 --- a/esphome/components/gpio/binary_sensor/gpio_binary_sensor.h +++ b/esphome/components/gpio/binary_sensor/gpio_binary_sensor.h @@ -8,10 +8,10 @@ namespace esphome { namespace gpio { -// Store class for ISR data (no vtables, ISR-safe) +// Store class for ISR data and configuration (no vtables, ISR-safe) class GPIOBinarySensorStore { public: - void setup(InternalGPIOPin *pin, gpio::InterruptType type, Component *component); + void setup(InternalGPIOPin *pin, Component *component); static void gpio_intr(GPIOBinarySensorStore *arg); @@ -32,11 +32,13 @@ class GPIOBinarySensorStore { } protected: + friend class GPIOBinarySensor; ISRInternalGPIOPin isr_pin_; - volatile bool state_{false}; - volatile bool last_state_{false}; - volatile bool changed_{false}; Component *component_{nullptr}; // Pointer to the component for enable_loop_soon_any_context() + volatile bool state_{false}; + volatile bool changed_{false}; + bool use_interrupt_{true}; + gpio::InterruptType interrupt_type_{gpio::INTERRUPT_ANY_EDGE}; }; class GPIOBinarySensor final : public binary_sensor::BinarySensor, public Component { @@ -44,9 +46,9 @@ class GPIOBinarySensor final : public binary_sensor::BinarySensor, public Compon // No destructor needed: ESPHome components are created at boot and live forever. // Interrupts are only detached on reboot when memory is cleared anyway. - void set_pin(GPIOPin *pin) { pin_ = pin; } - void set_use_interrupt(bool use_interrupt) { use_interrupt_ = use_interrupt; } - void set_interrupt_type(gpio::InterruptType type) { interrupt_type_ = type; } + void set_pin(GPIOPin *pin) { this->pin_ = pin; } + void set_use_interrupt(bool use_interrupt) { this->store_.use_interrupt_ = use_interrupt; } + void set_interrupt_type(gpio::InterruptType type) { this->store_.interrupt_type_ = type; } // ========== INTERNAL METHODS ========== // (In most use cases you won't need these) /// Setup pin @@ -59,8 +61,6 @@ class GPIOBinarySensor final : public binary_sensor::BinarySensor, public Compon protected: GPIOPin *pin_; - bool use_interrupt_{true}; - gpio::InterruptType interrupt_type_{gpio::INTERRUPT_ANY_EDGE}; GPIOBinarySensorStore store_; }; From 8ad8f89e504bb93273ff6e703c6113458ae43536 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Mar 2026 13:56:53 -1000 Subject: [PATCH 124/166] [light] Reorder LightState fields to eliminate padding (#15112) --- esphome/components/light/light_state.h | 32 +++++++++++++------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/esphome/components/light/light_state.h b/esphome/components/light/light_state.h index b8d72cc832..ab7f2e4df8 100644 --- a/esphome/components/light/light_state.h +++ b/esphome/components/light/light_state.h @@ -322,22 +322,6 @@ class LightState : public EntityBase, public Component { FixedVector effects_; /// Object used to store the persisted values of the light. ESPPreferenceObject rtc_; - /// Value for storing the index of the currently active effect. 0 if no effect is active - uint32_t active_effect_index_{}; - /// Default transition length for all transitions in ms. - uint32_t default_transition_length_{}; - /// Transition length to use for flash transitions. - uint32_t flash_transition_length_{}; - /// Gamma correction factor for the light. - float gamma_correct_{}; -#ifdef USE_LIGHT_GAMMA_LUT - const uint16_t *gamma_table_{nullptr}; -#endif // USE_LIGHT_GAMMA_LUT - - /// Whether the light value should be written in the next cycle. - bool next_write_{true}; - // for effects, true if a transformer (transition) is active. - bool is_transformer_active_ = false; /** Listeners for remote values changes. * @@ -361,6 +345,22 @@ class LightState : public EntityBase, public Component { /// Initial state of the light. optional initial_state_{}; + /// Value for storing the index of the currently active effect. 0 if no effect is active + uint32_t active_effect_index_{}; + /// Default transition length for all transitions in ms. + uint32_t default_transition_length_{}; + /// Transition length to use for flash transitions. + uint32_t flash_transition_length_{}; + /// Gamma correction factor for the light. + float gamma_correct_{}; +#ifdef USE_LIGHT_GAMMA_LUT + const uint16_t *gamma_table_{nullptr}; +#endif // USE_LIGHT_GAMMA_LUT + + /// Whether the light value should be written in the next cycle. + bool next_write_{true}; + // for effects, true if a transformer (transition) is active. + bool is_transformer_active_{false}; /// Restore mode of the light. LightRestoreMode restore_mode_; }; From 69911c3db1828e3d7e9447cf92f10d728eaaa543 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Mar 2026 13:58:36 -1000 Subject: [PATCH 125/166] [wifi] Reduce ESP8266 roaming scan dwell time to match ESP32 (#15127) --- .../components/wifi/wifi_component_esp8266.cpp | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index f2fabb9080..03800cc3a9 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -664,11 +664,22 @@ bool WiFiComponent::wifi_scan_start_(bool passive) { config.show_hidden = 1; #if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 4, 0) config.scan_type = passive ? WIFI_SCAN_TYPE_PASSIVE : WIFI_SCAN_TYPE_ACTIVE; + // Use shorter dwell times for roaming scans - we only need to detect strong + // nearby APs, not do a thorough survey. This also reduces off-channel time + // which can cause Beacon Timeout disconnects on some APs. + // Roaming times match the ESP32 IDF scan defaults. + static constexpr uint32_t SCAN_PASSIVE_DEFAULT_MS = 500; + static constexpr uint32_t SCAN_PASSIVE_ROAMING_MS = 300; + static constexpr uint32_t SCAN_ACTIVE_MIN_DEFAULT_MS = 400; + static constexpr uint32_t SCAN_ACTIVE_MAX_DEFAULT_MS = 500; + static constexpr uint32_t SCAN_ACTIVE_MIN_ROAMING_MS = 100; + static constexpr uint32_t SCAN_ACTIVE_MAX_ROAMING_MS = 300; + bool roaming = this->roaming_state_ == RoamingState::SCANNING; if (passive) { - config.scan_time.passive = 500; + config.scan_time.passive = roaming ? SCAN_PASSIVE_ROAMING_MS : SCAN_PASSIVE_DEFAULT_MS; } else { - config.scan_time.active.min = 400; - config.scan_time.active.max = 500; + config.scan_time.active.min = roaming ? SCAN_ACTIVE_MIN_ROAMING_MS : SCAN_ACTIVE_MIN_DEFAULT_MS; + config.scan_time.active.max = roaming ? SCAN_ACTIVE_MAX_ROAMING_MS : SCAN_ACTIVE_MAX_DEFAULT_MS; } #endif bool ret = wifi_station_scan(&config, &WiFiComponent::s_wifi_scan_done_callback); From df4318505f79ce12c15bd848c3a89ae59aa1c9e9 Mon Sep 17 00:00:00 2001 From: Javier Peletier Date: Tue, 24 Mar 2026 01:28:04 +0100 Subject: [PATCH 126/166] [substitutions] refactor substitute() as a pure function (package refactor part 3) (#15031) Co-authored-by: J. Nick Koston --- esphome/components/packages/__init__.py | 9 +- esphome/components/substitutions/__init__.py | 93 ++++++++------------ tests/unit_tests/test_substitutions.py | 46 ++++++++-- 3 files changed, 82 insertions(+), 66 deletions(-) diff --git a/esphome/components/packages/__init__.py b/esphome/components/packages/__init__.py index 793cb946dd..f9bdb677a7 100644 --- a/esphome/components/packages/__init__.py +++ b/esphome/components/packages/__init__.py @@ -300,9 +300,14 @@ def do_packages_pass(config: dict, skip_update: bool = False) -> dict: context_vars = package_config.vars if CONF_PACKAGES in package_config or CONF_URL in package_config: # Remote package definition: eagerly resolve before PACKAGE_SCHEMA validation. - from esphome.components.substitutions import substitute_context_vars + from esphome.components.substitutions import ContextVars, substitute - substitute_context_vars(package_config, context_vars) + package_config = substitute( + package_config, + [], + ContextVars(context_vars), + strict_undefined=False, + ) package_config = PACKAGE_SCHEMA(package_config) if isinstance(package_config, str): return package_config # Jinja string, skip processing diff --git a/esphome/components/substitutions/__init__.py b/esphome/components/substitutions/__init__.py index ecee816ce9..aab1712b65 100644 --- a/esphome/components/substitutions/__init__.py +++ b/esphome/components/substitutions/__init__.py @@ -81,12 +81,6 @@ def _restore_data_base(value: Any, orig_value: ESPHomeDataBase) -> ESPHomeDataBa return value -def _try_substitute(value: Any, context: ContextVars) -> Any: - """Substitute variables in value, returning the result or the original if unchanged.""" - result = _substitute_item(value, [], context, strict_undefined=True) - return result if result is not None else value - - def _resolve_var(name: str, context_vars: ContextVars) -> Any: """Look up a substitution variable, falling back to the resolver callback.""" sub = context_vars.get(name, Missing) @@ -253,7 +247,7 @@ def _push_context( if value is Missing: return Missing try: - value = _try_substitute(value, resolver_context) + value = substitute(value, [], resolver_context, True) except UndefinedError as err: unresolvables[key] = (value, err) return Missing @@ -297,68 +291,51 @@ def push_context( return parent_context -def _substitute_item( +def substitute( item: Any, path: SubstitutionPath, parent_context: ContextVars, strict_undefined: bool, errors: ErrList | None = None, -) -> Any | None: - """Recursively substitute variables in a config item. +) -> Any: + """Returns a recursively substituted version of `item`.""" - Walks dicts, lists, strings, Lambdas, Extend, and Remove nodes, - replacing variable references with values from context_vars. - Mutates containers in-place; returns a replacement value for - strings/scalars, or None if the item was unchanged. - """ + if isinstance(item, ESPLiteralValue): + return item # do not substitute inside literal blocks - def _walk(item: Any, path: SubstitutionPath, parent_ctx: ContextVars) -> Any | None: - if isinstance(item, ESPLiteralValue): - return None # do not substitute inside literal blocks + # Push the current item's context onto the context stack + context_vars = push_context(item, parent_context, errors) - ctx = push_context(item, parent_ctx, errors) + result = item - if isinstance(item, list): - for idx, it in enumerate(item): - sub = _walk(it, path + [idx], ctx) - if sub is not None: - item[idx] = sub - elif isinstance(item, dict): - replace_keys: list[tuple[str, Any]] = [] - for k, v in item.items(): - if path or k != CONF_SUBSTITUTIONS: - sub = _walk(k, path + [k], ctx) - if sub is not None: - replace_keys.append((k, sub)) - sub = _walk(v, path + [k], ctx) - if sub is not None: - item[k] = sub - for old, new in replace_keys: - if str(new) == str(old): - item[new] = item[old] - else: - item[new] = merge_config(item.get(new), item.get(old)) - del item[old] - elif isinstance(item, str): - sub = _expand_substitutions(item, path, ctx, strict_undefined, errors) - if not isinstance(sub, str) or sub != item: - return sub - elif isinstance(item, (core.Lambda, Extend, Remove)) and item.value: - sub = _expand_substitutions(item.value, path, ctx, strict_undefined, errors) - if sub != item.value: - item.value = sub - return None + if isinstance(item, list): + result = [ + substitute(it, path + [i], context_vars, strict_undefined, errors) + for i, it in enumerate(item) + ] - return _walk(item, path, parent_context) + elif isinstance(item, dict): + result = OrderedDict() + for k, v in item.items(): + v = substitute(v, path + [k], context_vars, strict_undefined, errors) + k = substitute(k, path + [k], context_vars, strict_undefined, errors) + result[k] = merge_config(result.get(k), v) + elif isinstance(item, str): + result = _expand_substitutions( + item, path, context_vars, strict_undefined, errors + ) -def substitute_context_vars(node: Any, context_vars: dict[str, Any]) -> None: - """Eagerly substitute context vars into a config node in-place. + elif isinstance(item, (core.Lambda, Extend, Remove)) and item.value: + value = _expand_substitutions( + item.value, path, context_vars, strict_undefined, errors + ) + if item.value != value: + result = type(item)(value) - Undefined variables are silently ignored — this is used before - the main substitution pass when not all variables are visible yet. - """ - _substitute_item(node, [], ContextVars(context_vars), strict_undefined=False) + if isinstance(item, ESPHomeDataBase): + result = make_data_base(result, item) + return result def _warn_unresolved_variables(errors: ErrList) -> None: @@ -387,7 +364,7 @@ def do_substitution_pass( Extracts the ``substitutions:`` block, merges in any command-line overrides, resolves inter-variable dependencies, then walks the config tree replacing all ``$var`` / ``${expr}`` references. - Returns the (mutated) config dict with resolved substitutions + Returns a new config dict with resolved substitutions restored at the front. """ # Extract substitutions from config, overriding with substitutions coming from command line: @@ -415,7 +392,7 @@ def do_substitution_pass( errors: ErrList = [] # Collect undefined errors during substitution parent_context, substitutions = _push_context(substitutions, ContextVars(), errors) - _substitute_item(config, [], parent_context, False, errors) + config = substitute(config, [], parent_context, False, errors) if errors: _warn_unresolved_variables(errors) diff --git a/tests/unit_tests/test_substitutions.py b/tests/unit_tests/test_substitutions.py index db46a27dfb..30478f9521 100644 --- a/tests/unit_tests/test_substitutions.py +++ b/tests/unit_tests/test_substitutions.py @@ -550,8 +550,8 @@ def test_lambda_substitution() -> None: "lambda": lam, } ) - substitutions.do_substitution_pass(config) - assert lam.value == "return 42;" + config = substitutions.do_substitution_pass(config) + assert config["lambda"].value == "return 42;" def test_lambda_no_substitution_unchanged() -> None: @@ -564,8 +564,8 @@ def test_lambda_no_substitution_unchanged() -> None: "lambda": lam, } ) - substitutions.do_substitution_pass(config) - assert lam.value is original_value + config = substitutions.do_substitution_pass(config) + assert config["lambda"].value is original_value def test_extend_substitution() -> None: @@ -577,8 +577,42 @@ def test_extend_substitution() -> None: "sensor": ext, } ) - substitutions.do_substitution_pass(config) - assert ext.value == "my_sensor" + config = substitutions.do_substitution_pass(config) + assert config["sensor"].value == "my_sensor" + + +def test_substitute_does_not_mutate_input() -> None: + """substitute() must return a new tree without modifying the original.""" + inner_list = ["${var}", "static"] + inner_dict = OrderedDict({"key": "${var}"}) + lam = Lambda("return ${var};") + config = OrderedDict( + { + "a_list": inner_list, + "a_dict": inner_dict, + "a_lambda": lam, + "plain": "${var}", + } + ) + context = substitutions.ContextVars({"var": "replaced"}) + result = substitutions.substitute(config, [], context, strict_undefined=True) + + # Result has substitutions applied + assert result["plain"] == "replaced" + assert result["a_list"] == ["replaced", "static"] + assert result["a_dict"]["key"] == "replaced" + assert result["a_lambda"].value == "return replaced;" + + # Original input is untouched + assert config["plain"] == "${var}" + assert inner_list == ["${var}", "static"] + assert inner_dict["key"] == "${var}" + assert lam.value == "return ${var};" + + # Containers are new objects, not the originals + assert result["a_list"] is not inner_list + assert result["a_dict"] is not inner_dict + assert result["a_lambda"] is not lam def test_do_substitution_pass_substitutions_must_be_mapping_from_config() -> None: From fe2c4e47bfc3c622179b6d006703151a3ee7f39f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Mar 2026 14:40:02 -1000 Subject: [PATCH 127/166] [sensor] Deprecate .raw_state, guard raw_callback_ behind USE_SENSOR_FILTER (#15094) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- esphome/components/haier/hon_climate.cpp | 2 +- .../nextion/sensor/nextion_sensor.cpp | 4 --- esphome/components/sensor/sensor.cpp | 10 +++++-- esphome/components/sensor/sensor.h | 28 ++++++++++++++----- 4 files changed, 30 insertions(+), 14 deletions(-) diff --git a/esphome/components/haier/hon_climate.cpp b/esphome/components/haier/hon_climate.cpp index 92defe560e..1cee95bf16 100644 --- a/esphome/components/haier/hon_climate.cpp +++ b/esphome/components/haier/hon_climate.cpp @@ -748,7 +748,7 @@ void HonClimate::update_sub_sensor_(SubSensorType type, float value) { if (type < SubSensorType::SUB_SENSOR_TYPE_COUNT) { size_t index = (size_t) type; if ((this->sub_sensors_[index] != nullptr) && - ((!this->sub_sensors_[index]->has_state()) || (this->sub_sensors_[index]->raw_state != value))) + ((!this->sub_sensors_[index]->has_state()) || (this->sub_sensors_[index]->get_raw_state() != value))) this->sub_sensors_[index]->publish_state(value); } } diff --git a/esphome/components/nextion/sensor/nextion_sensor.cpp b/esphome/components/nextion/sensor/nextion_sensor.cpp index 03b7261239..9ea12cf808 100644 --- a/esphome/components/nextion/sensor/nextion_sensor.cpp +++ b/esphome/components/nextion/sensor/nextion_sensor.cpp @@ -85,10 +85,6 @@ void NextionSensor::set_state(float state, bool publish, bool send_to_nextion) { } this->publish_state(published_state); - } else { - this->raw_state = state; - this->state = state; - this->set_has_state(true); } } this->update_component_settings(); diff --git a/esphome/components/sensor/sensor.cpp b/esphome/components/sensor/sensor.cpp index b4e59dfeb5..aad7f86dcf 100644 --- a/esphome/components/sensor/sensor.cpp +++ b/esphome/components/sensor/sensor.cpp @@ -40,7 +40,10 @@ const LogString *state_class_to_string(StateClass state_class) { return StateClassStrings::get_log_str(static_cast(state_class), 0); } +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" Sensor::Sensor() : state(NAN), raw_state(NAN) {} +#pragma GCC diagnostic pop int8_t Sensor::get_accuracy_decimals() { if (this->sensor_flags_.has_accuracy_override) @@ -63,8 +66,13 @@ StateClass Sensor::get_state_class() { } void Sensor::publish_state(float state) { +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" this->raw_state = state; +#pragma GCC diagnostic pop +#ifdef USE_SENSOR_FILTER this->raw_callback_.call(state); +#endif ESP_LOGV(TAG, "'%s': Received new state %f", this->name_.c_str(), state); @@ -110,8 +118,6 @@ void Sensor::clear_filters() { this->filter_list_ = nullptr; } #endif // USE_SENSOR_FILTER -float Sensor::get_state() const { return this->state; } -float Sensor::get_raw_state() const { return this->raw_state; } void Sensor::internal_send_state_to_frontend(float state) { this->set_has_state(true); diff --git a/esphome/components/sensor/sensor.h b/esphome/components/sensor/sensor.h index b3bd962036..f4ea4af985 100644 --- a/esphome/components/sensor/sensor.h +++ b/esphome/components/sensor/sensor.h @@ -95,9 +95,14 @@ class Sensor : public EntityBase { #endif /// Getter-syntax for .state. - float get_state() const; + float get_state() const { return this->state; } /// Getter-syntax for .raw_state - float get_raw_state() const; + float get_raw_state() const { +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" + return this->raw_state; +#pragma GCC diagnostic pop + } /** Publish a new state to the front-end. * @@ -113,8 +118,14 @@ class Sensor : public EntityBase { /// Add a callback that will be called every time a filtered value arrives. template void add_on_state_callback(F &&callback) { this->callback_.add(std::forward(callback)); } /// Add a callback that will be called every time the sensor sends a raw value. + /// When USE_SENSOR_FILTER is not enabled, delegates to the regular callback + /// since raw state equals filtered state without filter support compiled in. template void add_on_raw_state_callback(F &&callback) { +#ifdef USE_SENSOR_FILTER this->raw_callback_.add(std::forward(callback)); +#else + this->callback_.add(std::forward(callback)); +#endif } /** This member variable stores the last state that has passed through all filters. @@ -126,17 +137,20 @@ class Sensor : public EntityBase { */ float state; - /** This member variable stores the current raw state of the sensor, without any filters applied. - * - * Unlike .state,this will be updated immediately when publish_state is called. - */ +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" + /// @deprecated Use get_raw_state() instead. This member will be removed in ESPHome 2026.10.0. + ESPDEPRECATED("Use get_raw_state() instead of .raw_state. Will be removed in 2026.10.0", "2026.4.0") float raw_state; +#pragma GCC diagnostic pop void internal_send_state_to_frontend(float state); protected: +#ifdef USE_SENSOR_FILTER LazyCallbackManager raw_callback_; ///< Storage for raw state callbacks. - LazyCallbackManager callback_; ///< Storage for filtered state callbacks. +#endif + LazyCallbackManager callback_; ///< Storage for filtered state callbacks. #ifdef USE_SENSOR_FILTER Filter *filter_list_{nullptr}; ///< Store all active filters. From 793813790a36d782a726b9b9258b3403ce08c34a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Mar 2026 15:52:39 -1000 Subject: [PATCH 128/166] [api] Precompute tag bytes for forced varint and length-delimited fields (#15067) --- esphome/components/api/api_pb2.cpp | 10 ++-- esphome/components/api/proto.h | 11 +++++ script/api_protobuf/api_protobuf.py | 75 +++++++++++++++++++++++++++++ 3 files changed, 93 insertions(+), 3 deletions(-) diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 61b034c7ea..f77f4df545 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -2249,10 +2249,14 @@ bool SubscribeBluetoothLEAdvertisementsRequest::decode_varint(uint32_t field_id, return true; } void BluetoothLERawAdvertisement::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_uint64(1, this->address, true); - buffer.encode_sint32(2, this->rssi, true); + buffer.write_raw_byte(8); + buffer.encode_varint_raw_64(this->address); + buffer.write_raw_byte(16); + buffer.encode_varint_raw(encode_zigzag32(this->rssi)); buffer.encode_uint32(3, this->address_type); - buffer.encode_bytes(4, this->data, this->data_len, true); + buffer.write_raw_byte(34); + buffer.encode_varint_raw(this->data_len); + buffer.encode_raw(this->data, this->data_len); } uint32_t BluetoothLERawAdvertisement::calculate_size() const { uint32_t size = 0; diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index 95a79105a3..b629018a91 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -229,6 +229,17 @@ class ProtoWriteBuffer { * Following https://protobuf.dev/programming-guides/encoding/#structure */ void encode_field_raw(uint32_t field_id, uint32_t type) { this->encode_varint_raw((field_id << 3) | type); } + /// Write a single precomputed tag byte. Tag must be < 128. + inline void write_raw_byte(uint8_t b) ESPHOME_ALWAYS_INLINE { + this->debug_check_bounds_(1); + *this->pos_++ = b; + } + /// Write raw bytes to the buffer (no tag, no length prefix). + inline void encode_raw(const void *data, size_t len) ESPHOME_ALWAYS_INLINE { + this->debug_check_bounds_(len); + std::memcpy(this->pos_, data, len); + this->pos_ += len; + } /// Write a precomputed tag byte + 32-bit value in one operation. /// Tag must be a single-byte varint (< 128). No zero check. inline void write_tag_and_fixed32(uint8_t tag, uint32_t value) ESPHOME_ALWAYS_INLINE { diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 81ea93caf4..f2a11141af 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -221,8 +221,58 @@ class TypeInfo(ABC): decode_64bit = None + # Mapping from encode_func to raw encode expression template. + # When a forced field has a single-byte tag, the code generator emits + # write_raw_byte(tag) + raw encode instead of the full encode_* method, + # eliminating the zero-check branch and encode_field_raw indirection. + # {value} is replaced with the actual field expression. + RAW_ENCODE_MAP: dict[str, str] = { + "encode_uint32": "buffer.encode_varint_raw({value});", + "encode_uint64": "buffer.encode_varint_raw_64({value});", + "encode_sint32": "buffer.encode_varint_raw(encode_zigzag32({value}));", + "encode_sint64": "buffer.encode_varint_raw_64(encode_zigzag64({value}));", + "encode_int64": "buffer.encode_varint_raw_64(static_cast({value}));", + "encode_bool": "buffer.write_raw_byte({value} ? 0x01 : 0x00);", + } + + def _encode_with_precomputed_tag(self, value_expr: str) -> str | None: + """Try to emit a precomputed-tag encode for a forced field. + + Returns the raw encode string if the tag is a single byte and the + encode_func has a known raw equivalent, or None otherwise. + """ + if not self.force: + return None + tag = self.calculate_tag() + if tag >= 128: + return None + raw_expr = self.RAW_ENCODE_MAP.get(self.encode_func) + if raw_expr is None: + return None + return f"buffer.write_raw_byte({tag});\n{raw_expr.format(value=value_expr)}" + + def _encode_bytes_with_precomputed_tag( + self, data_expr: str, len_expr: str + ) -> str | None: + """Try to emit a precomputed-tag encode for a forced bytes/string field. + + Returns the raw encode string if the tag is a single byte, or None. + """ + if not self.force: + return None + tag = self.calculate_tag() + if tag >= 128: + return None + return ( + f"buffer.write_raw_byte({tag});\n" + f"buffer.encode_varint_raw({len_expr});\n" + f"buffer.encode_raw({data_expr}, {len_expr});" + ) + @property def encode_content(self) -> str: + if result := self._encode_with_precomputed_tag(f"this->{self.field_name}"): + return result if self.force: return f"buffer.{self.encode_func}({self.number}, this->{self.field_name}, true);" return f"buffer.{self.encode_func}({self.number}, this->{self.field_name});" @@ -635,6 +685,11 @@ class StringType(TypeInfo): @property def encode_content(self) -> str: # Use the StringRef + if result := self._encode_bytes_with_precomputed_tag( + f"this->{self.field_name}_ref_.c_str()", + f"this->{self.field_name}_ref_.size()", + ): + return result if self.force: return f"buffer.encode_string({self.number}, this->{self.field_name}_ref_, true);" return f"buffer.encode_string({self.number}, this->{self.field_name}_ref_);" @@ -801,6 +856,10 @@ class BytesType(TypeInfo): @property def encode_content(self) -> str: + if result := self._encode_bytes_with_precomputed_tag( + f"this->{self.field_name}_ptr_", f"this->{self.field_name}_len_" + ): + return result if self.force: return f"buffer.encode_bytes({self.number}, this->{self.field_name}_ptr_, this->{self.field_name}_len_, true);" return f"buffer.encode_bytes({self.number}, this->{self.field_name}_ptr_, this->{self.field_name}_len_);" @@ -908,6 +967,10 @@ class PointerToBytesBufferType(PointerToBufferTypeBase): @property def encode_content(self) -> str: + if result := self._encode_bytes_with_precomputed_tag( + f"this->{self.field_name}", f"this->{self.field_name}_len" + ): + return result if self.force: return f"buffer.encode_bytes({self.number}, this->{self.field_name}, this->{self.field_name}_len, true);" return f"buffer.encode_bytes({self.number}, this->{self.field_name}, this->{self.field_name}_len);" @@ -957,6 +1020,10 @@ class PointerToStringBufferType(PointerToBufferTypeBase): @property def encode_content(self) -> str: + if result := self._encode_bytes_with_precomputed_tag( + f"this->{self.field_name}.c_str()", f"this->{self.field_name}.size()" + ): + return result if self.force: return ( f"buffer.encode_string({self.number}, this->{self.field_name}, true);" @@ -1124,6 +1191,10 @@ class FixedArrayBytesType(TypeInfo): @property def encode_content(self) -> str: + if result := self._encode_bytes_with_precomputed_tag( + f"this->{self.field_name}", f"this->{self.field_name}_len" + ): + return result if self.force: return f"buffer.encode_bytes({self.number}, this->{self.field_name}, this->{self.field_name}_len, true);" return f"buffer.encode_bytes({self.number}, this->{self.field_name}, this->{self.field_name}_len);" @@ -1199,6 +1270,10 @@ class EnumType(TypeInfo): @property def encode_content(self) -> str: + if result := self._encode_with_precomputed_tag( + f"static_cast(this->{self.field_name})" + ): + return result if self.force: return f"buffer.{self.encode_func}({self.number}, static_cast(this->{self.field_name}), true);" return f"buffer.{self.encode_func}({self.number}, static_cast(this->{self.field_name}));" From 7eddf429ea3810f4e1b019b13c20f768de936411 Mon Sep 17 00:00:00 2001 From: Javier Peletier Date: Tue, 24 Mar 2026 10:57:22 +0100 Subject: [PATCH 129/166] [substitutions] speed up config loading: substitutions pass and `!include` redesign (package refactor part 4) (#12126) Co-authored-by: J. Nick Koston --- esphome/components/packages/__init__.py | 406 ++++++++++++------ esphome/config.py | 6 +- tests/component_tests/packages/test_init.py | 4 +- .../component_tests/packages/test_packages.py | 178 +++++++- .../06-remote_packages.approved.yaml | 21 +- .../06-remote_packages.input.yaml | 22 +- .../07-package_merging.approved.yaml | 2 - .../08-include_hierarchy.approved.yaml | 49 +++ .../08-include_hierarchy.input.yaml | 16 + .../10-dynamic_packages.approved.yaml | 69 +++ .../10-dynamic_packages.input.yaml | 62 +++ .../substitutions/level1_package.yaml | 21 + .../substitutions/level2_package.yaml | 21 + .../substitutions/level3_package.yaml | 16 + .../fixtures/substitutions/package2.yaml | 10 + tests/unit_tests/test_substitutions.py | 4 +- tests/unit_tests/test_yaml_util.py | 42 +- 17 files changed, 781 insertions(+), 168 deletions(-) create mode 100644 tests/unit_tests/fixtures/substitutions/08-include_hierarchy.approved.yaml create mode 100644 tests/unit_tests/fixtures/substitutions/08-include_hierarchy.input.yaml create mode 100644 tests/unit_tests/fixtures/substitutions/10-dynamic_packages.approved.yaml create mode 100644 tests/unit_tests/fixtures/substitutions/10-dynamic_packages.input.yaml create mode 100644 tests/unit_tests/fixtures/substitutions/level1_package.yaml create mode 100644 tests/unit_tests/fixtures/substitutions/level2_package.yaml create mode 100644 tests/unit_tests/fixtures/substitutions/level3_package.yaml create mode 100644 tests/unit_tests/fixtures/substitutions/package2.yaml diff --git a/esphome/components/packages/__init__.py b/esphome/components/packages/__init__.py index f9bdb677a7..1a6df84fe0 100644 --- a/esphome/components/packages/__init__.py +++ b/esphome/components/packages/__init__.py @@ -6,6 +6,7 @@ from pathlib import Path from typing import Any from esphome import git, yaml_util +from esphome.components.substitutions import ContextVars, push_context, substitute from esphome.components.substitutions.jinja import has_jinja from esphome.config_helpers import Remove, merge_config import esphome.config_validation as cv @@ -32,43 +33,44 @@ _LOGGER = logging.getLogger(__name__) DOMAIN = CONF_PACKAGES -def validate_has_jinja(value: Any): - if not isinstance(value, str) or not has_jinja(value): - raise cv.Invalid("string does not contain Jinja syntax") - return value +def is_remote_package(package_config: dict) -> bool: + """Returns True if the package_config is a remote package definition.""" + return CONF_URL in package_config -def valid_package_contents(allow_jinja: bool = True) -> Callable[[Any], dict]: - """Returns a validator that checks if a package_config that will be merged looks as - much as possible to a valid config to fail early on obvious mistakes.""" +def valid_package_contents(package_config: dict) -> dict: + """Validate that a package looks like a plausible ESPHome config fragment. - def validator(package_config: dict) -> dict: - if isinstance(package_config, dict): - if CONF_URL in package_config: - # If a URL key is found, then make sure the config conforms to a remote package schema: - return REMOTE_PACKAGE_SCHEMA(package_config) - - # Validate manually since Voluptuous would regenerate dicts and lose metadata - # such as ESPHomeDataBase - 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, Remove)): - continue # e.g. script: [], psram: !remove, logger: {level: debug} - if v is None: - continue # e.g. web_server: - if allow_jinja and isinstance(v, str) and has_jinja(v): - # e.g: remote package shorthand: - # package_name: github://esphome/repo/file.yaml@${ branch }, or: - # switch: ${ expression that evals to a switch } - continue - - raise cv.Invalid("Invalid component content in package definition") - return package_config + Rejects non-dict values, remote package schemas (which should have been + handled earlier), non-string keys, and scalar values that aren't Jinja + expressions. This is a lightweight check to catch obvious mistakes before + full component validation runs later. + """ + if not isinstance(package_config, dict): raise cv.Invalid("Package contents must be a dict") - return validator + if is_remote_package(package_config): + # Package contents must not contain a root `url:` key + raise cv.Invalid("Remote package schema not expected here") + + # Validate manually since Voluptuous would regenerate dicts and lose metadata + # such as ESPHomeDataBase + 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, 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 }, or: + # switch: ${ expression that evals to a switch } + continue + + raise cv.Invalid("Invalid component content in package definition") + return package_config def expand_file_to_files(config: dict): @@ -105,7 +107,7 @@ def validate_source_shorthand(value): return REMOTE_PACKAGE_SCHEMA(conf) -def deprecate_single_package(config): +def deprecate_single_package(config: dict) -> dict: _LOGGER.warning( """ Including a single package under `packages:`, i.e., `packages: !include mypackage.yaml` is deprecated. @@ -158,10 +160,7 @@ REMOTE_PACKAGE_SCHEMA = cv.All( PACKAGE_SCHEMA = cv.Any( # A package definition is either: validate_source_shorthand, # A git URL shorthand string that expands to a remote package schema, or REMOTE_PACKAGE_SCHEMA, # a valid remote package schema, or - validate_has_jinja, # a Jinja string that may resolve to a package, or - valid_package_contents( - allow_jinja=True - ), # Something that at least looks like an actual package, e.g. {wifi:{ssid: xxx}} + valid_package_contents, # Something that at least looks like an actual package, e.g. {wifi:{ssid: xxx}} # which will have to be fully validated later as per each component's schema. ) @@ -179,7 +178,15 @@ CONFIG_SCHEMA = cv.Any( # under `packages:` we can have either: def _process_remote_package(config: dict, skip_update: bool = False) -> dict: - # When skip_update is True, use NEVER_REFRESH to prevent updates + """Clone/update a git repo and load the YAML files listed in the package definition. + + Returns ``{"packages": {: , ...}}`` so the caller + can recurse into the loaded packages. Each loaded YAML node is tagged + with any ``vars:`` from the file entry via :func:`yaml_util.add_context`. + + If loading fails after cloning, attempts a revert and retry in case + a prior cached checkout is stale. + """ actual_refresh = git.NEVER_REFRESH if skip_update else config[CONF_REFRESH] repo_dir, revert = git.clone_or_update( url=config[CONF_URL], @@ -189,7 +196,7 @@ def _process_remote_package(config: dict, skip_update: bool = False) -> dict: username=config.get(CONF_USERNAME), password=config.get(CONF_PASSWORD), ) - files = [] + files: list[dict[str, Any]] = [] if base_path := config.get(CONF_PATH): repo_dir = repo_dir / base_path @@ -200,126 +207,255 @@ def _process_remote_package(config: dict, skip_update: bool = False) -> dict: else: files.append(file) - def get_packages(files) -> dict: - packages = {} + def _load_package_yaml(yaml_file: Path, filename: str) -> dict: + """Load a YAML file from a remote package, validating min_version.""" + try: + new_yaml = yaml_util.load_yaml(yaml_file) + except EsphomeError as e: + raise cv.Invalid( + f"{filename} is not a valid YAML file." + f" Please check the file contents.\n{e}" + ) from e + esphome_config = new_yaml.get(CONF_ESPHOME) or {} + min_version = esphome_config.get(CONF_MIN_VERSION) + if min_version is not None and cv.Version.parse(min_version) > cv.Version.parse( + ESPHOME_VERSION + ): + raise cv.Invalid( + f"Current ESPHome Version is too old to use" + f" this package: {ESPHOME_VERSION} < {min_version}" + ) + return new_yaml + + def get_packages(files: list[dict[str, Any]]) -> dict: + packages: dict[str, Any] = {} for idx, file in enumerate(files): filename = file[CONF_PATH] yaml_file: Path = repo_dir / filename - vars = file.get(CONF_VARS, {}) - if not yaml_file.is_file(): raise cv.Invalid( f"{filename} does not exist in repository", path=[CONF_FILES, idx, CONF_PATH], ) - - try: - new_yaml = yaml_util.load_yaml(yaml_file) - if ( - CONF_ESPHOME in new_yaml - and CONF_MIN_VERSION in new_yaml[CONF_ESPHOME] - ): - min_version = new_yaml[CONF_ESPHOME][CONF_MIN_VERSION] - if cv.Version.parse(min_version) > cv.Version.parse( - ESPHOME_VERSION - ): - raise cv.Invalid( - f"Current ESPHome Version is too old to use this package: {ESPHOME_VERSION} < {min_version}" - ) - new_yaml = yaml_util.add_context(new_yaml, vars or None) - packages[f"{filename}{idx}"] = new_yaml - except EsphomeError as e: - raise cv.Invalid( - f"{filename} is not a valid YAML file. Please check the file contents.\n{e}" - ) from e + new_yaml = _load_package_yaml(yaml_file, filename) + new_yaml = yaml_util.add_context(new_yaml, file.get(CONF_VARS)) + packages[f"{filename}{idx}"] = new_yaml return packages - packages = None - error = "" - - try: - packages = get_packages(files) - except cv.Invalid as e: - error = e + if revert is not None: + # If loading fails, the cached checkout may be stale — revert and retry once. try: - if revert is not None: - revert() - packages = get_packages(files) - except cv.Invalid as er: - error = er + return {CONF_PACKAGES: get_packages(files)} + except cv.Invalid: + revert() + try: + return {CONF_PACKAGES: get_packages(files)} + except cv.Invalid as err: + raise cv.Invalid(f"Failed to load packages. {err}", path=err.path) from err - if packages is None: - raise cv.Invalid(f"Failed to load packages. {error}", path=error.path) + return {CONF_PACKAGES: get_packages(files)} - return {"packages": packages} + +def _walk_package_dict( + packages: dict, + callback: Callable[[dict, ContextVars | None], dict], + context: ContextVars | None, +) -> cv.Invalid | None: + """Iterate a packages dict in reverse priority order, invoking callback on each entry. + + Returns ``None`` on success, or the first :class:`cv.Invalid` error if a callback fails. + """ + for package_name, package_config in reversed(packages.items()): + with cv.prepend_path(package_name): + try: + packages[package_name] = callback(package_config, context) + except cv.Invalid as err: + return err + return None + + +def _walk_package_list( + packages: list, + callback: Callable[[dict, ContextVars | None], dict], + context: ContextVars | None, +) -> None: + """Iterate a packages list in reverse priority order, invoking callback on each entry.""" + for idx in reversed(range(len(packages))): + with cv.prepend_path(idx): + packages[idx] = callback(packages[idx], context) def _walk_packages( - config: dict, callback: Callable[[dict], dict], validate_deprecated: bool = True + config: dict, + callback: Callable[[dict, ContextVars | None], dict], + context: ContextVars | None = None, + validate_deprecated: bool = True, ) -> dict: + """Walks the packages structure in priority order, invoking ``callback`` on each package definition found. + + This function only iterates over the immediate ``packages:`` entries in *config*. + If packages may contain nested ``packages:`` keys, the *callback* is responsible + for recursing by calling ``_walk_packages`` on the returned package config. + """ if CONF_PACKAGES not in config: return config packages = config[CONF_PACKAGES] - # The following block and `validate_deprecated` parameter can be safely removed - # once single-package deprecation is effective - if validate_deprecated: - packages = CONFIG_SCHEMA(packages) + if not isinstance(packages, (dict, list)): + raise cv.Invalid( + f"Packages must be a key to value mapping or list, got {type(packages)} instead" + ) with cv.prepend_path(CONF_PACKAGES): - if isinstance(packages, dict): - for package_name, package_config in reversed(packages.items()): - with cv.prepend_path(package_name): - package_config = callback(package_config) - packages[package_name] = _walk_packages(package_config, callback) - elif isinstance(packages, list): - for idx in reversed(range(len(packages))): - with cv.prepend_path(idx): - package_config = callback(packages[idx]) - packages[idx] = _walk_packages(package_config, callback) - else: - raise cv.Invalid( - f"Packages must be a key to value mapping or list, got {type(packages)} instead" - ) + if not isinstance(packages, dict): + _walk_package_list(packages, callback, context) + elif (result := _walk_package_dict(packages, callback, context)) is not None: + if not validate_deprecated: + raise result + # Fallback: treat the dict as a single deprecated package. + # Note: this catches *any* cv.Invalid from the callback, which may + # mask real validation errors in named package dicts. + # This block can be removed once the single-package + # deprecation period (2026.7.0) is over. + config[CONF_PACKAGES] = [packages] + return _walk_packages(deprecate_single_package(config), callback, context) + config[CONF_PACKAGES] = packages return config -def do_packages_pass(config: dict, skip_update: bool = False) -> dict: - """Processes, downloads and validates all packages in the config. - Also extracts and merges all substitutions found in packages into the main config substitutions. +def _substitute_package_definition( + package_config: dict | str, context_vars: ContextVars | None +) -> dict | str: + """Substitute variables in a package definition string or remote package dict. + + Only substitutes strings and remote package dicts (URLs, refs, paths). + Local package contents are left untouched — they will be substituted + later during the main substitution pass. + """ + if isinstance(package_config, str) or ( + isinstance(package_config, dict) and is_remote_package(package_config) + ): + package_config = substitute( + item=package_config, + path=[], + parent_context=context_vars or ContextVars(), + strict_undefined=False, + ) + return package_config + + +def _update_substitutions_context( + parent_context: UserDict, + package_substitutions: dict[str, Any], +) -> None: + """Resolve and add new substitutions to the parent context. + + Skips keys already present (higher-priority sources win). + String values are substituted against the current context so that + cross-references between substitutions are expanded when possible. + """ + for key, value in package_substitutions.items(): + if key in parent_context: + continue + if not isinstance(value, str): + parent_context[key] = value + continue + parent_context[key] = substitute( + item=value, + path=[CONF_SUBSTITUTIONS, key], + parent_context=ContextVars(parent_context), + strict_undefined=False, + ) + + +class _PackageProcessor: + """Stateful processor that resolves packages and collects substitutions. + + Packages are processed highest-priority first (later-declared before + earlier-declared) so that their substitutions are available when + resolving lower-priority package definitions. For each entry: + + 1. Substitute variables in remote package definitions (URLs, refs, paths). + 2. Validate against ``PACKAGE_SCHEMA`` and download remote packages. + 3. Extract ``substitutions:`` and merge into the shared context + (higher-priority packages win on conflicts). + 4. Recurse into any nested ``packages:`` keys. + + Command-line substitutions take the highest priority and are never overridden. + """ + + def __init__( + self, + substitutions: UserDict, + command_line_substitutions: dict[str, Any] | None, + skip_update: bool, + ) -> None: + self.substitutions = substitutions + self.parent_context = UserDict(command_line_substitutions or {}) + self.skip_update = skip_update + + def resolve_package( + self, package_config: dict | str, context_vars: ContextVars | None + ) -> dict: + """Substitute variables in the definition and fetch remote packages. + + The input may be a ``str`` (git shorthand or Jinja expression) or a + ``dict`` (remote or local package). After ``PACKAGE_SCHEMA`` validation + the result is always a ``dict``. + """ + package_config = _substitute_package_definition(package_config, context_vars) + package_config = PACKAGE_SCHEMA(package_config) + if is_remote_package(package_config): + package_config = _process_remote_package(package_config, self.skip_update) + return package_config + + def collect_substitutions(self, package_config: dict) -> None: + """Extract substitutions from a package and merge into the shared context.""" + if subs := package_config.pop(CONF_SUBSTITUTIONS, {}): + self.substitutions.data = merge_config(subs, self.substitutions.data) + _update_substitutions_context(self.parent_context, subs) + + def process_package( + self, package_config: dict | str, context_vars: ContextVars | None + ) -> dict: + """Resolve a single package and recurse into any nested packages.""" + package_config = self.resolve_package(package_config, context_vars) + self.collect_substitutions(package_config) + + if CONF_PACKAGES not in package_config: + return package_config + + # Push context from !include vars on the package root and on the packages key + context_vars = push_context(package_config, context_vars) + context_vars = push_context(package_config[CONF_PACKAGES], context_vars) + return _walk_packages(package_config, self.process_package, context_vars) + + +def do_packages_pass( + config: dict, + *, + command_line_substitutions: dict[str, Any] | None = None, + skip_update: bool = False, +) -> dict: + """Load, validate, and flatten all packages in the config. + + Returns the config with all packages loaded in-place (but not yet merged) + and a consolidated ``substitutions:`` block restored at the front. """ if CONF_PACKAGES not in config: return config substitutions = UserDict(config.pop(CONF_SUBSTITUTIONS, {})) + processor = _PackageProcessor( + substitutions, command_line_substitutions, skip_update + ) + _update_substitutions_context(processor.parent_context, substitutions) - def process_package_callback(package_config: dict) -> dict: - """This will be called for each package found in the config.""" - if isinstance(package_config, yaml_util.ConfigContext): - context_vars = package_config.vars - if CONF_PACKAGES in package_config or CONF_URL in package_config: - # Remote package definition: eagerly resolve before PACKAGE_SCHEMA validation. - from esphome.components.substitutions import ContextVars, substitute - - package_config = substitute( - package_config, - [], - ContextVars(context_vars), - strict_undefined=False, - ) - package_config = PACKAGE_SCHEMA(package_config) - if isinstance(package_config, str): - return package_config # Jinja string, skip processing - if CONF_URL in package_config: - package_config = _process_remote_package(package_config, skip_update) - # Extract substitutions from the package and merge them into the main substitutions: - substitutions.data = merge_config( - package_config.pop(CONF_SUBSTITUTIONS, {}), substitutions.data - ) - return package_config - - _walk_packages(config, process_package_callback) + context_vars = push_context( + config[CONF_PACKAGES], ContextVars(processor.parent_context) + ) + _walk_packages(config, processor.process_package, context_vars) if substitutions: config[CONF_SUBSTITUTIONS] = substitutions.data @@ -328,19 +464,27 @@ def do_packages_pass(config: dict, skip_update: bool = False) -> dict: def merge_packages(config: dict) -> dict: - """Merges all packages into the main config and removes the `packages:` key.""" + """Flatten the ``packages:`` tree into the main config. + + Collects every package (including nested ones) into a flat list in + priority order, then merges them into *config* using :func:`merge_config`. + Higher-priority packages (declared later) override lower-priority ones. + + The ``packages:`` key is removed from the returned config. + Must be called after :func:`do_packages_pass` has resolved all packages. + """ if CONF_PACKAGES not in config: return config # Build flat list of all package configs to merge in priority order: merge_list: list[dict] = [] - validate_package = valid_package_contents(allow_jinja=False) - - def process_package_callback(package_config: dict) -> dict: + def process_package_callback( + package_config: dict, context: ContextVars | None + ) -> dict: """This will be called for each package found in the config.""" - merge_list.append(validate_package(package_config)) - return package_config + merge_list.append(package_config) + return _walk_packages(package_config, process_package_callback) _walk_packages(config, process_package_callback, validate_deprecated=False) # Merge all packages into the main config: diff --git a/esphome/config.py b/esphome/config.py index b80aaf3700..7a6feea3d3 100644 --- a/esphome/config.py +++ b/esphome/config.py @@ -989,7 +989,11 @@ def validate_config( result.add_output_path([CONF_PACKAGES], CONF_PACKAGES) try: - config = do_packages_pass(config, skip_update=skip_external_update) + config = do_packages_pass( + config, + command_line_substitutions=command_line_substitutions, + skip_update=skip_external_update, + ) except vol.Invalid as err: result.update(config) result.add_error(err) diff --git a/tests/component_tests/packages/test_init.py b/tests/component_tests/packages/test_init.py index 779244e2ed..fd30c2433f 100644 --- a/tests/component_tests/packages/test_init.py +++ b/tests/component_tests/packages/test_init.py @@ -69,7 +69,7 @@ def test_packages_skip_update_false( } # Call with skip_update=False (default) - do_packages_pass(config, skip_update=False) + do_packages_pass(config, command_line_substitutions={}, skip_update=False) # Verify clone_or_update was called with actual refresh value mock_clone_or_update.assert_called_once() @@ -104,7 +104,7 @@ def test_packages_default_no_skip( } # Call without skip_update parameter - do_packages_pass(config) + do_packages_pass(config, command_line_substitutions={}) # Verify clone_or_update was called with actual refresh value mock_clone_or_update.assert_called_once() diff --git a/tests/component_tests/packages/test_packages.py b/tests/component_tests/packages/test_packages.py index 60dc0dccda..0893c7dcbb 100644 --- a/tests/component_tests/packages/test_packages.py +++ b/tests/component_tests/packages/test_packages.py @@ -37,6 +37,7 @@ from esphome.const import ( ) from esphome.core import CORE from esphome.util import OrderedDict +from esphome.yaml_util import add_context # Test strings TEST_DEVICE_NAME = "test_device_name" @@ -70,7 +71,7 @@ def fixture_basic_esphome(): def packages_pass(config): - """Wrapper around packages_pass that also resolves Extend and Remove.""" + """Passes the config through the packages processing steps.""" config = do_packages_pass(config) config = do_substitution_pass(config) config = merge_packages(config) @@ -705,6 +706,85 @@ def test_remote_packages_with_files_list( assert actual == expected +@patch("esphome.yaml_util.load_yaml") +@patch("pathlib.Path.is_file") +@patch("esphome.git.clone_or_update") +def test_remote_packages_with_files_list_and_substitutions( + mock_clone_or_update, mock_is_file, mock_load_yaml +) -> None: + """ + Ensures that packages are loaded as mixed list of dictionary and strings + """ + # Mock the response from git.clone_or_update + mock_revert = MagicMock() + mock_clone_or_update.return_value = (Path("/tmp/noexists"), mock_revert) + + # Mock the response from pathlib.Path.is_file + mock_is_file.return_value = True + + # Mock the response from esphome.yaml_util.load_yaml + mock_load_yaml.side_effect = [ + OrderedDict( + { + CONF_SENSOR: [ + { + CONF_PLATFORM: TEST_SENSOR_PLATFORM_1, + CONF_NAME: TEST_SENSOR_NAME_1, + } + ] + } + ), + OrderedDict( + { + CONF_SENSOR: [ + { + CONF_PLATFORM: TEST_SENSOR_PLATFORM_1, + CONF_NAME: TEST_SENSOR_NAME_2, + } + ] + } + ), + ] + + # Define the input config + config = { + CONF_PACKAGES: { + "package1": add_context( + { + CONF_URL: r"${url}", + CONF_REF: r"${branch}", + CONF_FILES: [ + {CONF_PATH: r"$file"}, + "sensor2.yaml", + ], + CONF_REFRESH: "1d", + }, + { + "branch": "main", + "file": TEST_YAML_FILENAME, + "url": "https://github.com/esphome/non-existant-repo", + }, + ) + } + } + + expected = { + CONF_SENSOR: [ + { + CONF_PLATFORM: TEST_SENSOR_PLATFORM_1, + CONF_NAME: TEST_SENSOR_NAME_1, + }, + { + CONF_PLATFORM: TEST_SENSOR_PLATFORM_1, + CONF_NAME: TEST_SENSOR_NAME_2, + }, + ] + } + + actual = packages_pass(config) + assert actual == expected + + @patch("esphome.yaml_util.load_yaml") @patch("pathlib.Path.is_file") @patch("esphome.git.clone_or_update") @@ -906,7 +986,7 @@ def test_packages_merge_substitutions() -> None: }, } - actual = do_packages_pass(config) + actual = do_packages_pass(config, command_line_substitutions={}) assert actual == expected @@ -970,33 +1050,107 @@ def test_package_merge() -> None: assert actual == expected +def test_packages_invalid_type_raises() -> None: + """Packages that are not a dict or list raise cv.Invalid.""" + config = { + CONF_PACKAGES: "not_a_dict_or_list", + } + with pytest.raises( + cv.Invalid, match="Packages must be a key to value mapping or list" + ): + do_packages_pass(config) + + @pytest.mark.parametrize( "invalid_package", [ 6, "some string", - ["some string"], - None, True, - {"some_component": 8}, - {3: 2}, - {"some_component": r"${unevaluated expression}"}, ], ) -def test_package_merge_invalid(invalid_package) -> None: - """ - Tests that trying to merge an invalid package raises an error. - """ +def test_invalid_package_contents_rejected(invalid_package: object) -> None: + """Invalid package contents are rejected by PACKAGE_SCHEMA during do_packages_pass.""" config = { CONF_PACKAGES: { "some_package": invalid_package, }, } - with pytest.raises(cv.Invalid): + do_packages_pass(config) + + +@pytest.mark.xfail( + reason="Deprecated single-package fallback swallows these errors. " + "Remove xfail when single-package deprecation is removed (2026.7.0).", + strict=True, +) +@pytest.mark.parametrize( + "invalid_package", + [ + None, + ["some string"], + {"some_component": 8}, + {3: 2}, + ], +) +def test_invalid_package_contents_masked_by_deprecation( + invalid_package: object, +) -> None: + """These invalid packages are swallowed by the deprecated single-package fallback.""" + config = { + CONF_PACKAGES: { + "some_package": invalid_package, + }, + } + with pytest.raises(cv.Invalid): + do_packages_pass(config) + + +def test_merge_packages_invalid_nested_type_raises() -> None: + """Invalid nested packages type during merge raises cv.Invalid.""" + config = { + CONF_PACKAGES: { + "pkg": { + CONF_PACKAGES: "invalid", + }, + }, + } + with pytest.raises( + cv.Invalid, match="Packages must be a key to value mapping or list" + ): merge_packages(config) +@patch("esphome.yaml_util.load_yaml") +@patch("pathlib.Path.is_file") +@patch("esphome.git.clone_or_update") +def test_remote_packages_no_revert( + mock_clone_or_update, mock_is_file, mock_load_yaml +) -> None: + """Remote packages with revert=None load without retry logic.""" + mock_clone_or_update.return_value = (Path("/tmp/noexists"), None) + mock_is_file.return_value = True + mock_load_yaml.return_value = OrderedDict( + {CONF_SENSOR: [{CONF_PLATFORM: TEST_SENSOR_PLATFORM_1, CONF_NAME: "test"}]} + ) + + config = { + CONF_PACKAGES: { + "pkg": { + CONF_URL: "https://github.com/esphome/repo", + CONF_REF: "main", + CONF_FILES: [{CONF_PATH: "file.yaml"}], + CONF_REFRESH: "1d", + } + } + } + actual = packages_pass(config) + assert actual[CONF_SENSOR] == [ + {CONF_PLATFORM: TEST_SENSOR_PLATFORM_1, CONF_NAME: "test"} + ] + + def test_raw_config_contains_merged_esphome_from_package(tmp_path) -> None: """Test that CORE.raw_config contains esphome section from merged package. 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 0fffbfb7cb..300cd85950 100644 --- a/tests/unit_tests/fixtures/substitutions/06-remote_packages.approved.yaml +++ b/tests/unit_tests/fixtures/substitutions/06-remote_packages.approved.yaml @@ -1,7 +1,3 @@ -substitutions: - x: 10 - y: 20 - z: 30 values_from_repo1_main: - package_name: package1 x: 3 @@ -28,3 +24,20 @@ values_from_repo1_main: y: 20 z: 5 volume: 1000 + - package_name: package6 + x: 12 + y: 13 + z: 5 + volume: 780 + - package_name: default + x: 10 + y: 20 + z: 5 + volume: 1000 +substitutions: + x: 10 + y: 20 + z: 30 + my_repo: repo1 + my_file: file1 + my_ref: main 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 772860bf19..c61eeab28d 100644 --- a/tests/unit_tests/fixtures/substitutions/06-remote_packages.input.yaml +++ b/tests/unit_tests/fixtures/substitutions/06-remote_packages.input.yaml @@ -2,16 +2,26 @@ substitutions: x: 10 y: 20 z: 30 + my_repo: default_repo + my_file: default_file + my_ref: main + +# The following key is only used by the test framework +# to simulate command line substitutions +command_line_substitutions: + my_repo: repo1 + my_file: file1 + packages: package1: url: https://github.com/esphome/repo1 + ref: main 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: @@ -41,3 +51,13 @@ packages: repo: repo1 file: file1.yaml ref: main + package6: + url: https://github.com/esphome/${my_repo} + ref: ${my_ref} + files: + - path: ${my_file + ".yaml"} + vars: + package_name: package6 + x: 12 + y: 13 + package7: github://esphome/${my_repo}/${my_file + ".yaml"}@${my_ref} diff --git a/tests/unit_tests/fixtures/substitutions/07-package_merging.approved.yaml b/tests/unit_tests/fixtures/substitutions/07-package_merging.approved.yaml index 867889b7bc..9e62fcae86 100644 --- a/tests/unit_tests/fixtures/substitutions/07-package_merging.approved.yaml +++ b/tests/unit_tests/fixtures/substitutions/07-package_merging.approved.yaml @@ -37,8 +37,6 @@ substitutions: - id: component8 value: 8 fancy_package: - substitutions: - fancy_subst: 42 fancy_component: *id001 pin: 12 some_switches: *id002 diff --git a/tests/unit_tests/fixtures/substitutions/08-include_hierarchy.approved.yaml b/tests/unit_tests/fixtures/substitutions/08-include_hierarchy.approved.yaml new file mode 100644 index 0000000000..fce47b01bf --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/08-include_hierarchy.approved.yaml @@ -0,0 +1,49 @@ +substitutions: + a: 10 + b: 20 + x: 79 +test_list: + - level1: + a: 10 + b: 20 + c: 10 + d: 20 + e: ${e} + f: ${f} + g: ${g} + h: ${h} + i: ${i} + j: ${j} + x: 80 + y: 40 + level2: + - level2: + a: 10 + b: 20 + c: 10 + d: 20 + e: 20 + f: 40 + g: ${g} + h: ${h} + i: ${i} + j: ${j} + x: 81 + y: 40 + level3: + - level3: + a: 10 + b: 20 + c: 10 + d: 20 + e: 20 + f: 40 + g: 100 + h: 200 + i: 30 + j: ${undefined_variable} + x: 82 + y: 40 + - a: 10 + b: 20 + x: 79 diff --git a/tests/unit_tests/fixtures/substitutions/08-include_hierarchy.input.yaml b/tests/unit_tests/fixtures/substitutions/08-include_hierarchy.input.yaml new file mode 100644 index 0000000000..6997ef56c1 --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/08-include_hierarchy.input.yaml @@ -0,0 +1,16 @@ +substitutions: + a: 10 + b: 20 + x: 79 + +test_list: + - !include + file: level1_package.yaml + vars: + x: ${x+1} + y: ${d*2} + c: ${a} + d: ${b} + - a: ${a} + b: ${b} + x: ${x} diff --git a/tests/unit_tests/fixtures/substitutions/10-dynamic_packages.approved.yaml b/tests/unit_tests/fixtures/substitutions/10-dynamic_packages.approved.yaml new file mode 100644 index 0000000000..ec2ae711bb --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/10-dynamic_packages.approved.yaml @@ -0,0 +1,69 @@ +substitutions: + a: from base config + b: from package3 + c: from nested package4 + nested_package: + nested_package_test_list: + - a: from base config + - b: from package3 + - c: from nested package4 + package1: + package1_test_list: + - a: from base config + - b: from package3 + - c: from nested package4 + package2: + package2_test_list: + - a: from package2 vars + - b: from package3 + - c: from nested package4 + package3: + package3_test_list: + - a: from base config + - b: from package3 + - c: from nested package4 + package4: + packages: + - nested_package_test_list: + - a: from base config + - b: from package3 + - c: from nested package4 + package_map: + package1: + package1_test_list: + - a: from base config + - b: from package3 + - c: from nested package4 + package2: + package2_test_list: + - a: from package2 vars + - b: from package3 + - c: from nested package4 + package3: &id001 + package3_test_list: + - a: from base config + - b: from package3 + - c: from nested package4 + selected_package_number: 3 + selected_package_name: package3 + selected_package: *id001 +base_test_list: + - a: from base config + - b: from package3 + - c: from nested package4 +package1_test_list: + - a: from base config + - b: from package3 + - c: from nested package4 +package2_test_list: + - a: from package2 vars + - b: from package3 + - c: from nested package4 +package3_test_list: + - a: from base config + - b: from package3 + - c: from nested package4 +nested_package_test_list: + - a: from base config + - b: from package3 + - c: from nested package4 diff --git a/tests/unit_tests/fixtures/substitutions/10-dynamic_packages.input.yaml b/tests/unit_tests/fixtures/substitutions/10-dynamic_packages.input.yaml new file mode 100644 index 0000000000..800e3cc7a8 --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/10-dynamic_packages.input.yaml @@ -0,0 +1,62 @@ +command_line_substitutions: + selected_package_number: 3 + +substitutions: + a: from base config + + package1: &p1 + substitutions: + a: from package1 + b: from package1 + c: from package1 + package1_test_list: + - a: ${ a } + - b: ${ b } + - c: ${ c } + + package2: &p2 !include + file: package2.yaml + vars: + a: from package2 vars + + package3: &p3 + substitutions: + a: from package3 + b: from package3 + c: from package3 + package3_test_list: + - a: ${ a } + - b: ${ b } + - c: ${ c } + + package4: + substitutions: + nested_package: + substitutions: + c: from nested package4 + nested_package_test_list: + - a: ${ a } + - b: ${ b } + - c: ${ c } + packages: + - ${ nested_package } + + package_map: + package1: *p1 + package2: *p2 + package3: *p3 + + selected_package_number: 2 # will be overridden by command line substitutions + selected_package_name: package${ selected_package_number } + selected_package: ${ package_map[selected_package_name] } + +packages: + - ${ package1 } + - ${ package2 } + - ${ selected_package } + - ${ package4 } + +base_test_list: + - a: ${ a } + - b: ${ b } + - c: ${ c } diff --git a/tests/unit_tests/fixtures/substitutions/level1_package.yaml b/tests/unit_tests/fixtures/substitutions/level1_package.yaml new file mode 100644 index 0000000000..d8a994b7b4 --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/level1_package.yaml @@ -0,0 +1,21 @@ +# this file is included by 07-include_hierarchy.input.yaml +level1: + a: ${a} # top-level substitution + b: ${b} # top-level substitution + c: ${c} # from vars when including + d: ${d} # from vars when including + e: ${e} # undefined at this level + f: ${f} # undefined at this level + g: ${g} # undefined at this level + h: ${h} # undefined at this level + i: ${i} # undefined at this level + j: ${j} # undefined at this level + x: ${x} # from vars when including, calculated + y: ${y} # from vars when including, calculated + level2: + - !include + file: level2_package.yaml + vars: + e: ${c*2} + f: ${d*2} + x: ${x+1} diff --git a/tests/unit_tests/fixtures/substitutions/level2_package.yaml b/tests/unit_tests/fixtures/substitutions/level2_package.yaml new file mode 100644 index 0000000000..460135553a --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/level2_package.yaml @@ -0,0 +1,21 @@ +# this file is included by level1_package.yaml +level2: + a: ${a} # top-level substitution + b: ${b} # top-level substitution + c: ${c} # visible from level1 vars + d: ${d} # visible from level1 vars + e: ${e} # from vars when including + f: ${f} # from vars when including + g: ${g} # undefined at this level + h: ${h} # undefined at this level + i: ${i} # undefined at this level + j: ${j} # undefined at this level + x: ${x} # from vars when including, calculated + y: ${y} # from vars when including, calculated + level3: + - !include + file: level3_package.yaml + vars: + g: ${e*5} + h: ${f*5} + x: ${x+1} diff --git a/tests/unit_tests/fixtures/substitutions/level3_package.yaml b/tests/unit_tests/fixtures/substitutions/level3_package.yaml new file mode 100644 index 0000000000..b16ed5fcf6 --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/level3_package.yaml @@ -0,0 +1,16 @@ +# this file is included by level2_package.yaml +defaults: + i: 30 +level3: + a: ${a} # top-level substitution + b: ${b} # top-level substitution + c: ${c} # visible from level1 vars + d: ${d} # visible from level1 vars + e: ${e} # visible from level2 vars + f: ${f} # visible from level2 vars + g: ${g} # from vars when including + h: ${h} # from vars when including + i: ${i} # Should take the default value of 30 + j: ${undefined_variable} # Does not exist, should be output as-is + x: ${x} # from vars when including, calculated + y: ${y} # from vars when including, calculated diff --git a/tests/unit_tests/fixtures/substitutions/package2.yaml b/tests/unit_tests/fixtures/substitutions/package2.yaml new file mode 100644 index 0000000000..998cd74c52 --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/package2.yaml @@ -0,0 +1,10 @@ +# included from 10-dynamic_packages.input.yaml +substitutions: + a: from package2 # must not override base config's a + # b not defined here, won't override package1's b + c: from package2 # will override package1's c + +package2_test_list: + - a: ${ a } + - b: ${ b } + - c: ${ c } diff --git a/tests/unit_tests/test_substitutions.py b/tests/unit_tests/test_substitutions.py index 30478f9521..c7b0bbcf7c 100644 --- a/tests/unit_tests/test_substitutions.py +++ b/tests/unit_tests/test_substitutions.py @@ -143,7 +143,9 @@ def test_substitutions_fixtures( command_line_substitutions = config.pop("command_line_substitutions", None) - config = do_packages_pass(config) + config = do_packages_pass( + config, command_line_substitutions=command_line_substitutions + ) config = substitutions.do_substitution_pass(config, command_line_substitutions) diff --git a/tests/unit_tests/test_yaml_util.py b/tests/unit_tests/test_yaml_util.py index 35a4bc3707..667b593819 100644 --- a/tests/unit_tests/test_yaml_util.py +++ b/tests/unit_tests/test_yaml_util.py @@ -98,13 +98,15 @@ def test_construct_secret_missing(fixture_path: Path, tmp_path: Path) -> None: """Test that missing secrets raise proper errors.""" # Create a YAML file with a secret that doesn't exist test_yaml = tmp_path / "test.yaml" - test_yaml.write_text(""" + test_yaml.write_text( + """ esphome: name: test wifi: password: !secret nonexistent_secret -""") +""" + ) # Create an empty secrets file secrets_yaml = tmp_path / "secrets.yaml" @@ -118,10 +120,12 @@ def test_construct_secret_no_secrets_file(tmp_path: Path) -> None: """Test that missing secrets.yaml file raises proper error.""" # Create a YAML file with a secret but no secrets.yaml test_yaml = tmp_path / "test.yaml" - test_yaml.write_text(""" + test_yaml.write_text( + """ wifi: password: !secret some_secret -""") +""" + ) # Mock CORE.config_path to avoid NoneType error with ( @@ -140,10 +144,12 @@ def test_construct_secret_fallback_to_main_config_dir( subdir.mkdir() test_yaml = subdir / "test.yaml" - test_yaml.write_text(""" + test_yaml.write_text( + """ wifi: password: !secret test_secret -""") +""" + ) # Create secrets.yaml in the main directory main_secrets = tmp_path / "secrets.yaml" @@ -164,9 +170,11 @@ def test_construct_include_dir_named(fixture_path: Path, tmp_path: Path) -> None # Create test YAML that uses include_dir_named test_yaml = dst_dir / "test_include_named.yaml" - test_yaml.write_text(""" + test_yaml.write_text( + """ sensor: !include_dir_named named_dir -""") +""" + ) actual = yaml_util.load_yaml(test_yaml) actual_sensor = actual["sensor"] @@ -199,9 +207,11 @@ def test_construct_include_dir_named_empty_dir(tmp_path: Path) -> None: empty_dir.mkdir() test_yaml = tmp_path / "test.yaml" - test_yaml.write_text(""" + test_yaml.write_text( + """ sensor: !include_dir_named empty_dir -""") +""" + ) actual = yaml_util.load_yaml(test_yaml) @@ -231,9 +241,11 @@ def test_construct_include_dir_named_with_dots(tmp_path: Path) -> None: hidden_subfile.write_text("key: hidden_subfile_value") test_yaml = tmp_path / "test.yaml" - test_yaml.write_text(""" + test_yaml.write_text( + """ test: !include_dir_named test_dir -""") +""" + ) actual = yaml_util.load_yaml(test_yaml) @@ -255,9 +267,11 @@ def test_find_files_recursive(fixture_path: Path, tmp_path: Path) -> None: # This indirectly tests _find_files by using include_dir_named test_yaml = dst_dir / "test_include_recursive.yaml" - test_yaml.write_text(""" + test_yaml.write_text( + """ all_sensors: !include_dir_named named_dir -""") +""" + ) actual = yaml_util.load_yaml(test_yaml) From b3390d40fb959808fcc520bff17c4a1350f98420 Mon Sep 17 00:00:00 2001 From: Diorcet Yann Date: Tue, 24 Mar 2026 19:31:42 +0100 Subject: [PATCH 130/166] [core] Fix cg.add_define propagation to dependencies in native ESP-IDF builds (#15137) --- esphome/build_gen/espidf.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/esphome/build_gen/espidf.py b/esphome/build_gen/espidf.py index 9df9b1069c..01923baaac 100644 --- a/esphome/build_gen/espidf.py +++ b/esphome/build_gen/espidf.py @@ -53,6 +53,13 @@ def get_project_cmakelists() -> str: variant = get_esp32_variant() idf_target = variant.lower().replace("-", "") + # Extract compile definitions from build flags (-DXXX -> XXX) + compile_defs = [flag for flag in CORE.build_flags if flag.startswith("-D")] + extra_compile_options = "\n".join( + f'idf_build_set_property(COMPILE_OPTIONS "{compile_def}" APPEND)' + for compile_def in compile_defs + ) + return f"""\ # Auto-generated by ESPHome cmake_minimum_required(VERSION 3.16) @@ -61,6 +68,9 @@ set(IDF_TARGET {idf_target}) set(EXTRA_COMPONENT_DIRS ${{CMAKE_SOURCE_DIR}}/src) include($ENV{{IDF_PATH}}/tools/cmake/project.cmake) + +{extra_compile_options} + project({CORE.name}) """ @@ -70,10 +80,6 @@ def get_component_cmakelists(minimal: bool = False) -> str: idf_requires = [] if minimal else (get_available_components() or []) requires_str = " ".join(idf_requires) - # Extract compile definitions from build flags (-DXXX -> XXX) - compile_defs = [flag[2:] for flag in CORE.build_flags if flag.startswith("-D")] - compile_defs_str = "\n ".join(sorted(compile_defs)) if compile_defs else "" - # Extract compile options (-W flags, excluding linker flags) compile_opts = [ flag @@ -104,11 +110,6 @@ idf_component_register( # Apply C++ standard target_compile_features(${{COMPONENT_LIB}} PUBLIC cxx_std_20) -# ESPHome compile definitions -target_compile_definitions(${{COMPONENT_LIB}} PUBLIC - {compile_defs_str} -) - # ESPHome compile options target_compile_options(${{COMPONENT_LIB}} PUBLIC {compile_opts_str} From 3cd50f0495564c14f35b55448332079874682f12 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 24 Mar 2026 15:31:08 -0400 Subject: [PATCH 131/166] [ci] Block new CONF_ constants from being added to esphome/const.py (#15145) --- script/ci-custom.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/script/ci-custom.py b/script/ci-custom.py index 25a0cf2127..7d0680a491 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -525,6 +525,29 @@ def lint_constants_usage(): return errs +# Maximum allowed CONF_ constants in esphome/const.py. +# This file is frozen — new constants go in esphome/components/const/__init__.py. +# Decrease this number when constants are moved out of const.py. +CONST_PY_MAX_CONF = 1011 + + +@lint_content_check(include=["esphome/const.py"]) +def lint_const_py_frozen(fname, content): + """Block new CONF_ constants from being added to esphome/const.py. + + New constants should go in esphome/components/const/__init__.py instead. + """ + count = sum(1 for line in content.splitlines() if line.startswith("CONF_")) + if count > CONST_PY_MAX_CONF: + return ( + "esphome/const.py is frozen. " + "Add new constants to esphome/components/const/__init__.py instead." + ) + if count < CONST_PY_MAX_CONF: + return f"CONST_PY_MAX_CONF in ci-custom.py should be updated to {count}." + return None + + def relative_cpp_search_text(fname: Path, content) -> str: parts = fname.parts integration = parts[2] From 55df21db516263a1f7b381035477a66cd84c5e8b Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 24 Mar 2026 15:44:28 -0400 Subject: [PATCH 132/166] [esp32] Default CPU frequency to maximum supported (#15143) --- esphome/components/esp32/__init__.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 1ecc270fd1..0e216485ac 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -379,12 +379,11 @@ FULL_CPU_FREQUENCIES = set(itertools.chain.from_iterable(CPU_FREQUENCIES.values( def set_core_data(config): cpu_frequency = config.get(CONF_CPU_FREQUENCY, None) variant = config[CONF_VARIANT] - # if not specified in config, set to 160MHz if supported, the fastest otherwise + # if not specified in config, default to the maximum supported frequency + # (ESP32-P4 engineering samples are limited to 360MHz, non-engineering can do 400MHz) if cpu_frequency is None: choices = CPU_FREQUENCIES[variant] - if "160MHZ" in choices: - cpu_frequency = "160MHZ" - elif "360MHZ" in choices: + if variant == VARIANT_ESP32P4 and config.get(CONF_ENGINEERING_SAMPLE): cpu_frequency = "360MHZ" else: cpu_frequency = choices[-1] From 22bc47da23a97187c74477fb282748d0935251aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20Bl=C3=A4se?= Date: Tue, 24 Mar 2026 20:57:58 +0100 Subject: [PATCH 133/166] [light] Fix incorrect mode change handling on transition to off (#15147) --- esphome/components/light/transformers.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/light/transformers.h b/esphome/components/light/transformers.h index b6e5e08f2b..61fe098ad7 100644 --- a/esphome/components/light/transformers.h +++ b/esphome/components/light/transformers.h @@ -27,7 +27,7 @@ class LightTransitionTransformer : public LightTransformer { } // When changing color mode, go through off state, as color modes are orthogonal and there can't be two active. - if (this->start_values_.get_color_mode() != this->target_values_.get_color_mode()) { + if (this->start_values_.get_color_mode() != this->end_values_.get_color_mode()) { this->changing_color_mode_ = true; this->intermediate_values_ = this->start_values_; this->intermediate_values_.set_state(false); @@ -39,8 +39,8 @@ class LightTransitionTransformer : public LightTransformer { // Halfway through, when intermediate state (off) is reached, flip it to the target, but remain off. if (this->changing_color_mode_ && p > 0.5f && - this->intermediate_values_.get_color_mode() != this->target_values_.get_color_mode()) { - this->intermediate_values_ = this->target_values_; + this->intermediate_values_.get_color_mode() != this->end_values_.get_color_mode()) { + this->intermediate_values_ = this->end_values_; this->intermediate_values_.set_state(false); } From 8751f348c8ab50db26b4cfbc5abcc1c4703ba739 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 24 Mar 2026 16:04:27 -0400 Subject: [PATCH 134/166] [sx127x] Fix FIFO read corruption (#15114) --- esphome/components/sx127x/sx127x.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/esphome/components/sx127x/sx127x.cpp b/esphome/components/sx127x/sx127x.cpp index 66957a7342..0fddfdccdb 100644 --- a/esphome/components/sx127x/sx127x.cpp +++ b/esphome/components/sx127x/sx127x.cpp @@ -38,14 +38,18 @@ void SX127x::write_register_(uint8_t reg, uint8_t value) { void SX127x::read_fifo_(std::vector &packet) { this->enable(); this->write_byte(REG_FIFO & 0x7F); - this->read_array(packet.data(), packet.size()); + for (auto &byte : packet) { + byte = this->transfer_byte(0x00); + } this->disable(); } void SX127x::write_fifo_(const std::vector &packet) { this->enable(); this->write_byte(REG_FIFO | 0x80); - this->write_array(packet.data(), packet.size()); + for (const auto &byte : packet) { + this->transfer_byte(byte); + } this->disable(); } From 13baf260505116b2f6e3ae8e1ed8fbcd18820b58 Mon Sep 17 00:00:00 2001 From: Diorcet Yann Date: Tue, 24 Mar 2026 21:26:21 +0100 Subject: [PATCH 135/166] [core] get_log_str: fix false-positive error on null-terminated strings with stricter compilers (#15136) Co-authored-by: J. Nick Koston Co-authored-by: J. Nick Koston Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston --- esphome/core/progmem.h | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/esphome/core/progmem.h b/esphome/core/progmem.h index 6c6a5252cf..031860e3a6 100644 --- a/esphome/core/progmem.h +++ b/esphome/core/progmem.h @@ -3,6 +3,7 @@ #include #include #include +#include #include "esphome/core/hal.h" // For PROGMEM definition @@ -104,7 +105,9 @@ struct LogString; static const char *get_(uint8_t idx, uint8_t fallback) { \ if (idx >= COUNT) \ idx = fallback; \ - return &BLOB[::esphome::progmem_read_byte(&OFFSETS[idx])]; \ + /* std::launder is used here to prevent the inter-procedural analysis that */ \ + /* causes the false positive that the string is not null terminated */ \ + return std::launder(&BLOB[::esphome::progmem_read_byte(&OFFSETS[idx])]); \ } \ static ::ProgmemStr get_progmem_str(uint8_t idx, uint8_t fallback) { \ return reinterpret_cast<::ProgmemStr>(get_(idx, fallback)); \ From 4ff85e2a1e0122f28a3b9d366e79b9fd112215f2 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 24 Mar 2026 19:48:17 -0400 Subject: [PATCH 136/166] [core] Fix clean-all to handle custom build paths (#15146) Co-authored-by: J. Nick Koston --- esphome/writer.py | 31 ++++++-- tests/unit_tests/test_writer.py | 124 ++++++++++++++++++++++++++++++++ 2 files changed, 151 insertions(+), 4 deletions(-) diff --git a/esphome/writer.py b/esphome/writer.py index 69a35d00e3..4aac16ffd4 100644 --- a/esphome/writer.py +++ b/esphome/writer.py @@ -18,7 +18,6 @@ from esphome.core import CORE, EsphomeError from esphome.helpers import ( copy_file_if_changed, cpp_string_escape, - get_str_env, is_ha_addon, read_file, rmtree, @@ -441,18 +440,42 @@ def clean_build(clear_pio_cache: bool = True): rmtree(cache_dir) +def _get_custom_build_dir(item: Path, data_dir: Path) -> Path | None: + """Parse a YAML config to find a custom build directory.""" + from esphome import yaml_util + + try: + raw = yaml_util.load_yaml(item) + except (EsphomeError, OSError) as e: + _LOGGER.debug("Could not parse %s to find build_path: %s", item, e) + return None + if not isinstance(raw, dict): + return None + esphome_conf = raw.get("esphome", {}) + if not isinstance(esphome_conf, dict): + return None + if build_path := esphome_conf.get("build_path"): + return data_dir / build_path + return None + + def clean_all(configuration: list[str]): data_dirs = [] for config in configuration: item = Path(config) if item.is_file() and item.suffix in (".yaml", ".yml"): - data_dirs.append(item.parent / ".esphome") + data_dir = item.parent / ".esphome" + data_dirs.append(data_dir) + if custom := _get_custom_build_dir(item, data_dir): + data_dirs.append(custom) else: data_dirs.append(item / ".esphome") if is_ha_addon(): data_dirs.append(Path("/data")) - if "ESPHOME_DATA_DIR" in os.environ: - data_dirs.append(Path(get_str_env("ESPHOME_DATA_DIR", None))) + if env_data_dir := os.environ.get("ESPHOME_DATA_DIR"): + data_dirs.append(Path(env_data_dir)) + if env_build_path := os.environ.get("ESPHOME_BUILD_PATH"): + data_dirs.append(Path(env_build_path)) # Clean build dir for dir in data_dirs: diff --git a/tests/unit_tests/test_writer.py b/tests/unit_tests/test_writer.py index 134b63df4a..6ace38a7d7 100644 --- a/tests/unit_tests/test_writer.py +++ b/tests/unit_tests/test_writer.py @@ -866,6 +866,130 @@ def test_clean_all_with_yaml_file( assert str(build_dir) in caplog.text +@patch("esphome.writer.CORE") +def test_clean_all_with_yaml_build_path( + mock_core: MagicMock, + tmp_path: Path, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test clean_all cleans absolute build_path specified in YAML config.""" + config_dir = tmp_path / "config" + config_dir.mkdir() + + # Create an absolute custom build path directory with contents + custom_build = tmp_path / "custom_build" + custom_build.mkdir() + (custom_build / "firmware.bin").write_text("x") + sub = custom_build / "subdir" + sub.mkdir() + (sub / "file.txt").write_text("x") + + yaml_file = config_dir / "test.yaml" + # Absolute build_path: data_dir / absolute = absolute (Python Path behavior) + yaml_file.write_text(f"esphome:\n name: test\n build_path: {custom_build}\n") + + # Also create the normal .esphome dir + build_dir = config_dir / ".esphome" + build_dir.mkdir() + (build_dir / "dummy.txt").write_text("x") + + from esphome.writer import clean_all + + with caplog.at_level("INFO"): + clean_all([str(yaml_file)]) + + # Both .esphome and custom build_path should be cleaned + assert build_dir.exists() + assert not (build_dir / "dummy.txt").exists() + assert custom_build.exists() + assert not (custom_build / "firmware.bin").exists() + assert not sub.exists() + + +@patch("esphome.writer.CORE") +def test_clean_all_with_yaml_parse_error( + mock_core: MagicMock, + tmp_path: Path, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test clean_all still cleans .esphome when YAML parse fails.""" + config_dir = tmp_path / "config" + config_dir.mkdir() + yaml_file = config_dir / "test.yaml" + yaml_file.write_text("invalid: yaml: content: [") + + build_dir = config_dir / ".esphome" + build_dir.mkdir() + (build_dir / "dummy.txt").write_text("x") + + from esphome.writer import clean_all + + with caplog.at_level("INFO"): + clean_all([str(yaml_file)]) + + # .esphome should still be cleaned despite YAML parse failure + assert build_dir.exists() + assert not (build_dir / "dummy.txt").exists() + + +@patch("esphome.writer.CORE") +def test_clean_all_with_env_build_path( + mock_core: MagicMock, + tmp_path: Path, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test clean_all cleans ESPHOME_BUILD_PATH directory.""" + config_dir = tmp_path / "config" + config_dir.mkdir() + + build_dir = config_dir / ".esphome" + build_dir.mkdir() + (build_dir / "dummy.txt").write_text("x") + + # Create env build path directory + env_build = tmp_path / "env_build" + env_build.mkdir() + (env_build / "firmware.bin").write_text("x") + + from esphome.writer import clean_all + + with ( + caplog.at_level("INFO"), + patch.dict(os.environ, {"ESPHOME_BUILD_PATH": str(env_build)}), + ): + clean_all([str(config_dir)]) + + # Both should be cleaned + assert not (build_dir / "dummy.txt").exists() + assert env_build.exists() + assert not (env_build / "firmware.bin").exists() + + +@patch("esphome.writer.CORE") +def test_clean_all_ignores_empty_env_vars( + mock_core: MagicMock, + tmp_path: Path, +) -> None: + """Test clean_all ignores empty ESPHOME_BUILD_PATH/ESPHOME_DATA_DIR.""" + config_dir = tmp_path / "config" + config_dir.mkdir() + + # Create a file in cwd that must NOT be cleaned + marker = tmp_path / "important.txt" + marker.write_text("do not delete") + + from esphome.writer import clean_all + + with patch.dict( + os.environ, + {"ESPHOME_BUILD_PATH": "", "ESPHOME_DATA_DIR": ""}, + ): + clean_all([str(config_dir)]) + + # Empty env vars must not cause cwd to be cleaned + assert marker.exists() + + @patch("esphome.writer.CORE") def test_clean_all( mock_core: MagicMock, From 752fe30332749f1608823753dcd65c20f91448e7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 24 Mar 2026 14:01:59 -1000 Subject: [PATCH 137/166] [api] Add descriptive message to status warning when waiting for client (#15148) --- esphome/components/api/api_server.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 1151bc5983..d9c3cc6846 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -108,7 +108,7 @@ void APIServer::setup() { this->last_connected_ = App.get_loop_component_start_time(); // Set warning status if reboot timeout is enabled if (this->reboot_timeout_ != 0) { - this->status_set_warning(); + this->status_set_warning(LOG_STR("waiting for client connection")); } } @@ -187,7 +187,7 @@ void APIServer::remove_client_(size_t client_index) { // Last client disconnected - set warning and start tracking for reboot timeout if (this->clients_.empty() && this->reboot_timeout_ != 0) { - this->status_set_warning(); + this->status_set_warning(LOG_STR("waiting for client connection")); this->last_connected_ = App.get_loop_component_start_time(); } From 9fb5b6aa158582ea35e968f9b8209f4f1849fe06 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 24 Mar 2026 14:03:18 -1000 Subject: [PATCH 138/166] [light] Replace initial_state storage with flash-resident callback (#15133) --- esphome/components/light/__init__.py | 12 +++++- esphome/components/light/light_state.cpp | 7 ++-- esphome/components/light/light_state.h | 10 +++-- .../fixtures/light_initial_state.yaml | 39 +++++++++++++++++++ tests/integration/test_light_initial_state.py | 38 ++++++++++++++++++ 5 files changed, 97 insertions(+), 9 deletions(-) create mode 100644 tests/integration/fixtures/light_initial_state.yaml create mode 100644 tests/integration/test_light_initial_state.py diff --git a/esphome/components/light/__init__.py b/esphome/components/light/__init__.py index 64452e4282..4090ca57c2 100644 --- a/esphome/components/light/__init__.py +++ b/esphome/components/light/__init__.py @@ -38,7 +38,7 @@ from esphome.const import ( CONF_WEB_SERVER, CONF_WHITE, ) -from esphome.core import CORE, ID, CoroPriority, HexInt, coroutine_with_priority +from esphome.core import CORE, ID, CoroPriority, HexInt, Lambda, coroutine_with_priority from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity from esphome.cpp_generator import MockObjClass @@ -262,6 +262,8 @@ async def setup_light_core_(light_var, config, output_var): cg.add(light_var.set_restore_mode(config[CONF_RESTORE_MODE])) if (initial_state_config := config.get(CONF_INITIAL_STATE)) is not None: + # Emit a stateless lambda that constructs the initial state — values live + # in flash as code, not stored in the LightState object (~40 bytes saved). initial_state = LightStateRTCState( initial_state_config.get(CONF_COLOR_MODE, ColorMode.UNKNOWN), initial_state_config.get(CONF_STATE, False), @@ -275,7 +277,13 @@ async def setup_light_core_(light_var, config, output_var): initial_state_config.get(CONF_COLD_WHITE, 1.0), initial_state_config.get(CONF_WARM_WHITE, 1.0), ) - cg.add(light_var.set_initial_state(initial_state)) + args = [(LightStateRTCState.operator("ref"), "s")] + lamb = await cg.process_lambda( + Lambda(f"s = {initial_state};"), + args, + return_type=cg.void, + ) + cg.add(light_var.set_initial_state(lamb)) if ( default_transition_length := config.get(CONF_DEFAULT_TRANSITION_LENGTH) diff --git a/esphome/components/light/light_state.cpp b/esphome/components/light/light_state.cpp index 1b736d84f6..bd778926d5 100644 --- a/esphome/components/light/light_state.cpp +++ b/esphome/components/light/light_state.cpp @@ -37,8 +37,9 @@ void LightState::setup() { auto call = this->make_call(); LightStateRTCState recovered{}; - if (this->initial_state_.has_value()) { - recovered = *this->initial_state_; + if (this->initial_state_callback_) { + this->initial_state_callback_(recovered); + this->initial_state_callback_ = nullptr; // One-shot — no longer needed } switch (this->restore_mode_) { case LIGHT_RESTORE_DEFAULT_OFF: @@ -195,7 +196,7 @@ void LightState::set_flash_transition_length(uint32_t flash_transition_length) { uint32_t LightState::get_flash_transition_length() const { return this->flash_transition_length_; } void LightState::set_gamma_correct(float gamma_correct) { this->gamma_correct_ = gamma_correct; } void LightState::set_restore_mode(LightRestoreMode restore_mode) { this->restore_mode_ = restore_mode; } -void LightState::set_initial_state(const LightStateRTCState &initial_state) { this->initial_state_ = initial_state; } +void LightState::set_initial_state(void (*callback)(LightStateRTCState &)) { this->initial_state_callback_ = callback; } bool LightState::supports_effects() { return !this->effects_.empty(); } const FixedVector &LightState::get_effects() const { return this->effects_; } void LightState::add_effects(const std::initializer_list &effects) { diff --git a/esphome/components/light/light_state.h b/esphome/components/light/light_state.h index ab7f2e4df8..5efc05358b 100644 --- a/esphome/components/light/light_state.h +++ b/esphome/components/light/light_state.h @@ -188,8 +188,9 @@ class LightState : public EntityBase, public Component { /// Set the restore mode of this light void set_restore_mode(LightRestoreMode restore_mode); - /// Set the initial state of this light - void set_initial_state(const LightStateRTCState &initial_state); + /// Set a callback to populate the initial state defaults during setup. + /// The callback is called once, then cleared. Values live in flash as code. + void set_initial_state(void (*callback)(LightStateRTCState &)); /// Return whether the light has any effects that meet the trait requirements. bool supports_effects(); @@ -342,8 +343,9 @@ class LightState : public EntityBase, public Component { */ std::unique_ptr> target_state_reached_listeners_; - /// Initial state of the light. - optional initial_state_{}; + /// Callback to populate initial state defaults — called once during setup, then cleared. + /// Values live in flash as function body; no per-instance data storage beyond this pointer. + void (*initial_state_callback_)(LightStateRTCState &){nullptr}; /// Value for storing the index of the currently active effect. 0 if no effect is active uint32_t active_effect_index_{}; diff --git a/tests/integration/fixtures/light_initial_state.yaml b/tests/integration/fixtures/light_initial_state.yaml new file mode 100644 index 0000000000..2654c76aa0 --- /dev/null +++ b/tests/integration/fixtures/light_initial_state.yaml @@ -0,0 +1,39 @@ +esphome: + name: light-initial-state-test +host: +api: # Port will be automatically injected +logger: + level: DEBUG + +output: + - platform: template + id: test_red + type: float + write_action: + - lambda: "" + - platform: template + id: test_green + type: float + write_action: + - lambda: "" + - platform: template + id: test_blue + type: float + write_action: + - lambda: "" + +light: + - platform: rgb + name: "Test Light" + id: test_light + red: test_red + green: test_green + blue: test_blue + restore_mode: ALWAYS_OFF + initial_state: + color_mode: RGB + state: true + brightness: 0.75 + red: 1.0 + green: 0.5 + blue: 0.0 diff --git a/tests/integration/test_light_initial_state.py b/tests/integration/test_light_initial_state.py new file mode 100644 index 0000000000..f1cd96dbf0 --- /dev/null +++ b/tests/integration/test_light_initial_state.py @@ -0,0 +1,38 @@ +"""Integration test for light initial_state configuration. + +Tests that the initial_state values are correctly applied at boot when +no saved preferences exist. The initial_state callback populates defaults +that the restore logic uses as a fallback. +""" + +import pytest + +from .state_utils import InitialStateHelper, require_entity +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_light_initial_state( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test that initial_state values are applied at boot.""" + async with run_compiled(yaml_config), api_client_connected() as client: + entities, _ = await client.list_entities_services() + light = require_entity(entities, "test_light") + + helper = InitialStateHelper(entities) + client.subscribe_states(helper.on_state_wrapper(lambda s: None)) + await helper.wait_for_initial_states() + + state = helper.initial_states[light.key] + + # restore_mode: ALWAYS_OFF overrides state to false + assert state.state is False + + # But the color values from initial_state should be applied + assert state.brightness == pytest.approx(0.75, abs=0.05) + assert state.red == pytest.approx(1.0, abs=0.01) + assert state.green == pytest.approx(0.5, abs=0.01) + assert state.blue == pytest.approx(0.0, abs=0.01) From b6aec4fa25bb9f7fdb09e9738385f5b8fed45267 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 24 Mar 2026 14:03:30 -1000 Subject: [PATCH 139/166] [ethernet] Add W5100 support for RP2040 (#15131) --- esphome/components/ethernet/__init__.py | 20 +++++++++----- .../components/ethernet/ethernet_component.h | 10 +++++++ .../ethernet/ethernet_component_rp2040.cpp | 26 ++++++++++++++----- esphome/core/defines.h | 1 + .../ethernet/common-w5100-rp2040.yaml | 18 +++++++++++++ .../ethernet/test-w5100.rp2040-ard.yaml | 1 + 6 files changed, 62 insertions(+), 14 deletions(-) create mode 100644 tests/components/ethernet/common-w5100-rp2040.yaml create mode 100644 tests/components/ethernet/test-w5100.rp2040-ard.yaml diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py index e17abfcc93..17459cabb6 100644 --- a/esphome/components/ethernet/__init__.py +++ b/esphome/components/ethernet/__init__.py @@ -115,6 +115,7 @@ ETHERNET_TYPES = { "JL1101": EthernetType.ETHERNET_TYPE_JL1101, "KSZ8081": EthernetType.ETHERNET_TYPE_KSZ8081, "KSZ8081RNA": EthernetType.ETHERNET_TYPE_KSZ8081RNA, + "W5100": EthernetType.ETHERNET_TYPE_W5100, "W5500": EthernetType.ETHERNET_TYPE_W5500, "OPENETH": EthernetType.ETHERNET_TYPE_OPENETH, "DM9051": EthernetType.ETHERNET_TYPE_DM9051, @@ -132,6 +133,7 @@ _PHY_TYPE_TO_DEFINE = { "JL1101": "USE_ETHERNET_JL1101", "KSZ8081": "USE_ETHERNET_KSZ8081", "KSZ8081RNA": "USE_ETHERNET_KSZ8081", + "W5100": "USE_ETHERNET_W5100", "W5500": "USE_ETHERNET_W5500", "DM9051": "USE_ETHERNET_DM9051", "LAN8670": "USE_ETHERNET_LAN8670", @@ -164,9 +166,15 @@ _IDF6_ETHERNET_COMPONENTS: dict[str, IDFRegistryComponent] = { # These types are always external IDF components (never built-in to ESP-IDF) _ALWAYS_EXTERNAL_IDF_COMPONENTS = {"LAN8670", "ENC28J60"} -SPI_ETHERNET_TYPES = ["W5500", "DM9051", "ENC28J60"] +# ESP32-only SPI ethernet types (W5100 is RP2040-only, no ESP-IDF driver) +SPI_ETHERNET_TYPES = {"W5500", "DM9051", "ENC28J60"} # RP2040-supported SPI ethernet types -RP2040_SPI_ETHERNET_TYPES = ["W5500", "ENC28J60"] +RP2040_SPI_ETHERNET_TYPES = {"W5100", "W5500", "ENC28J60"} +_RP2040_SPI_LIBRARIES = { + "W5100": "lwIP_w5100", + "W5500": "lwIP_w5500", + "ENC28J60": "lwIP_enc28j60", +} SPI_ETHERNET_DEFAULT_POLLING_INTERVAL = TimePeriodMilliseconds(milliseconds=10) emac_rmii_clock_mode_t = cg.global_ns.enum("emac_rmii_clock_mode_t") @@ -295,7 +303,7 @@ def _validate(config): ) elif CORE.is_rp2040 and config[CONF_TYPE] not in RP2040_SPI_ETHERNET_TYPES: raise cv.Invalid( - f"Only {', '.join(RP2040_SPI_ETHERNET_TYPES)} are supported on RP2040, " + f"Only {', '.join(sorted(RP2040_SPI_ETHERNET_TYPES))} are supported on RP2040, " f"not {config[CONF_TYPE]}" ) return config @@ -382,6 +390,7 @@ CONFIG_SCHEMA = cv.All( "JL1101": RMII_SCHEMA, "KSZ8081": RMII_SCHEMA, "KSZ8081RNA": RMII_SCHEMA, + "W5100": cv.All(SPI_SCHEMA, cv.only_on([Platform.RP2040])), "W5500": SPI_SCHEMA, "OPENETH": cv.All(BASE_SCHEMA, cv.only_on([Platform.ESP32])), "DM9051": SPI_SCHEMA, @@ -574,10 +583,7 @@ async def _to_code_rp2040(var: cg.Pvariable, config: ConfigType) -> None: cg.add(var.set_reset_pin(config[CONF_RESET_PIN])) cg.add_define("USE_ETHERNET_SPI") - if config[CONF_TYPE] == "ENC28J60": - cg.add_library("lwIP_enc28j60", None) - else: - cg.add_library("lwIP_w5500", None) + cg.add_library(_RP2040_SPI_LIBRARIES[config[CONF_TYPE]], None) def _final_validate_rmii_pins(config: ConfigType) -> None: diff --git a/esphome/components/ethernet/ethernet_component.h b/esphome/components/ethernet/ethernet_component.h index 4c85c39eb8..c6e37d01ea 100644 --- a/esphome/components/ethernet/ethernet_component.h +++ b/esphome/components/ethernet/ethernet_component.h @@ -25,6 +25,8 @@ extern "C" eth_esp32_emac_config_t eth_esp32_emac_default_config(void); #ifdef USE_RP2040 #if defined(USE_ETHERNET_W5500) #include +#elif defined(USE_ETHERNET_W5100) +#include #elif defined(USE_ETHERNET_ENC28J60) #include #else @@ -59,6 +61,7 @@ enum EthernetType : uint8_t { ETHERNET_TYPE_JL1101, ETHERNET_TYPE_KSZ8081, ETHERNET_TYPE_KSZ8081RNA, + ETHERNET_TYPE_W5100, ETHERNET_TYPE_W5500, ETHERNET_TYPE_OPENETH, ETHERNET_TYPE_DM9051, @@ -222,8 +225,15 @@ class EthernetComponent final : public Component { #ifdef USE_RP2040 static constexpr uint32_t LINK_CHECK_INTERVAL = 500; // ms between link/IP polls +#if defined(USE_ETHERNET_W5100) + static constexpr uint32_t RESET_DELAY_MS = 150; // W5100S PLL lock time +#else + static constexpr uint32_t RESET_DELAY_MS = 10; +#endif #if defined(USE_ETHERNET_W5500) Wiznet5500lwIP *eth_{nullptr}; +#elif defined(USE_ETHERNET_W5100) + Wiznet5100lwIP *eth_{nullptr}; #elif defined(USE_ETHERNET_ENC28J60) ENC28J60lwIP *eth_{nullptr}; #else diff --git a/esphome/components/ethernet/ethernet_component_rp2040.cpp b/esphome/components/ethernet/ethernet_component_rp2040.cpp index bd8c458985..9771bc59d5 100644 --- a/esphome/components/ethernet/ethernet_component_rp2040.cpp +++ b/esphome/components/ethernet/ethernet_component_rp2040.cpp @@ -31,12 +31,15 @@ void EthernetComponent::setup() { reset_pin.digital_write(false); delay(1); // NOLINT reset_pin.digital_write(true); - delay(10); // NOLINT - wait for chip to initialize after reset + // W5100S needs 150ms for PLL lock; W5500/ENC28J60 need ~10ms + delay(RESET_DELAY_MS); // NOLINT } // Create the SPI Ethernet device instance #if defined(USE_ETHERNET_W5500) this->eth_ = new Wiznet5500lwIP(this->cs_pin_, SPI, this->interrupt_pin_); // NOLINT +#elif defined(USE_ETHERNET_W5100) + this->eth_ = new Wiznet5100lwIP(this->cs_pin_, SPI, this->interrupt_pin_); // NOLINT #elif defined(USE_ETHERNET_ENC28J60) this->eth_ = new ENC28J60lwIP(this->cs_pin_, SPI, this->interrupt_pin_); // NOLINT #endif @@ -80,8 +83,8 @@ void EthernetComponent::setup() { // or via GPIO interrupt when one is provided. // Don't set started_ here — let the link polling in loop() set it - // when the W5500 link is actually up. Setting it prematurely causes - // a "Starting → Stopped → Starting" log sequence because the W5500 + // when the link is actually up. Setting it prematurely causes + // a "Starting → Stopped → Starting" log sequence because the chip // needs time after begin() before the PHY link is ready. } @@ -89,14 +92,21 @@ void EthernetComponent::loop() { // On RP2040, we need to poll connection state since there are no events. const uint32_t now = App.get_loop_component_start_time(); - // Throttle link/IP polling to avoid excessive SPI transactions from linkStatus() - // which reads the W5500 PHY register via SPI on every call. + // Throttle link/IP polling to avoid excessive SPI transactions. + // W5500/ENC28J60 read PHY register via SPI on every linkStatus() call. + // W5100 can't detect link state, so we skip the SPI read and assume link-up. // connected() reads netif->ip_addr without LwIPLock, but this is a single // 32-bit aligned read (atomic on ARM) — worst case is a one-iteration-stale // value, which is benign for polling. if (this->eth_ != nullptr && now - this->last_link_check_ >= LINK_CHECK_INTERVAL) { this->last_link_check_ = now; +#if defined(USE_ETHERNET_W5100) + // W5100 can't detect link (isLinkDetectable() returns false), so linkStatus() + // returns Unknown — assume link is up after successful begin() + bool link_up = true; +#else bool link_up = this->eth_->linkStatus() == LinkON; +#endif bool has_ip = this->eth_->connected(); if (!link_up) { @@ -171,6 +181,8 @@ void EthernetComponent::dump_config() { const char *type_str = "Unknown"; #if defined(USE_ETHERNET_W5500) type_str = "W5500"; +#elif defined(USE_ETHERNET_W5100) + type_str = "W5100"; #elif defined(USE_ETHERNET_ENC28J60) type_str = "ENC28J60"; #endif @@ -226,7 +238,7 @@ const char *EthernetComponent::get_eth_mac_address_pretty_into_buffer( } eth_duplex_t EthernetComponent::get_duplex_mode() { - // Both W5500 and ENC28J60 are full-duplex on RP2040 + // W5100, W5500, and ENC28J60 are full-duplex on RP2040 return ETH_DUPLEX_FULL; } @@ -235,7 +247,7 @@ eth_speed_t EthernetComponent::get_link_speed() { // ENC28J60 is 10Mbps only return ETH_SPEED_10M; #else - // W5500 is always 100Mbps + // W5100 and W5500 are 100Mbps return ETH_SPEED_100M; #endif } diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 996818c2e6..676ad3024f 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -284,6 +284,7 @@ #define USE_ETHERNET_SPI #define USE_ETHERNET_SPI_POLLING_SUPPORT #define USE_ETHERNET_OPENETH +#define USE_ETHERNET_W5100 #define USE_ETHERNET_W5500 #define USE_ETHERNET_DM9051 #define CONFIG_ETH_SPI_ETHERNET_W5500 1 diff --git a/tests/components/ethernet/common-w5100-rp2040.yaml b/tests/components/ethernet/common-w5100-rp2040.yaml new file mode 100644 index 0000000000..4c6d0313df --- /dev/null +++ b/tests/components/ethernet/common-w5100-rp2040.yaml @@ -0,0 +1,18 @@ +ethernet: + type: W5100 + clk_pin: 18 + mosi_pin: 19 + miso_pin: 16 + cs_pin: 17 + interrupt_pin: 21 + reset_pin: 20 + manual_ip: + static_ip: 192.168.178.56 + gateway: 192.168.178.1 + subnet: 255.255.255.0 + domain: .local + mac_address: "02:AA:BB:CC:DD:01" + on_connect: + - logger.log: "Ethernet connected!" + on_disconnect: + - logger.log: "Ethernet disconnected!" diff --git a/tests/components/ethernet/test-w5100.rp2040-ard.yaml b/tests/components/ethernet/test-w5100.rp2040-ard.yaml new file mode 100644 index 0000000000..e101f3112a --- /dev/null +++ b/tests/components/ethernet/test-w5100.rp2040-ard.yaml @@ -0,0 +1 @@ +<<: !include common-w5100-rp2040.yaml From f457b995f726d54581421f0a885fdab4b922cdb0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 24 Mar 2026 14:03:56 -1000 Subject: [PATCH 140/166] [datetime] Fix state_as_esptime() returning invalid timestamp (#15128) --- .../components/datetime/datetime_entity.cpp | 3 + esphome/core/time.cpp | 2 +- esphome/core/time.h | 18 ++++-- tests/components/time/posix_tz_parser.cpp | 56 ++++++++++++++++++- 4 files changed, 71 insertions(+), 8 deletions(-) diff --git a/esphome/components/datetime/datetime_entity.cpp b/esphome/components/datetime/datetime_entity.cpp index 730abb3ca8..fa50271f04 100644 --- a/esphome/components/datetime/datetime_entity.cpp +++ b/esphome/components/datetime/datetime_entity.cpp @@ -60,6 +60,9 @@ ESPTime DateTimeEntity::state_as_esptime() const { obj.year = this->year_; obj.month = this->month_; obj.day_of_month = this->day_; + obj.day_of_week = 0; + obj.day_of_year = 0; + obj.is_dst = false; obj.hour = this->hour_; obj.minute = this->minute_; obj.second = this->second_; diff --git a/esphome/core/time.cpp b/esphome/core/time.cpp index 6add82e7d1..650c61d37b 100644 --- a/esphome/core/time.cpp +++ b/esphome/core/time.cpp @@ -231,7 +231,7 @@ void ESPTime::increment_day() { void ESPTime::recalc_timestamp_utc(bool use_day_of_year) { time_t res = 0; - if (!this->fields_in_range()) { + if (!this->fields_in_range(false, use_day_of_year)) { this->timestamp = -1; return; } diff --git a/esphome/core/time.h b/esphome/core/time.h index 1716c51ffd..ed47432038 100644 --- a/esphome/core/time.h +++ b/esphome/core/time.h @@ -79,11 +79,19 @@ struct ESPTime { /// Check if this ESPTime is valid (all fields in range and year is greater than or equal to 2019) bool is_valid() const { return this->year >= 2019 && this->fields_in_range(); } - /// Check if all time fields of this ESPTime are in range. - bool fields_in_range() const { - return this->second < 61 && this->minute < 60 && this->hour < 24 && this->day_of_week > 0 && - this->day_of_week < 8 && this->day_of_year > 0 && this->day_of_year < 367 && this->month > 0 && - this->month < 13 && this->day_of_month > 0 && this->day_of_month <= days_in_month(this->month, this->year); + /// Check if time fields are in range. + /// @param check_day_of_week validate day_of_week (not always available when constructing from date/time fields) + /// @param check_day_of_year validate day_of_year (not always available when constructing from date/time fields) + bool fields_in_range(bool check_day_of_week = true, bool check_day_of_year = true) const { + bool valid = this->second < 61 && this->minute < 60 && this->hour < 24 && this->month > 0 && this->month < 13 && + this->day_of_month > 0 && this->day_of_month <= days_in_month(this->month, this->year); + if (check_day_of_week) { + valid = valid && this->day_of_week > 0 && this->day_of_week < 8; + } + if (check_day_of_year) { + valid = valid && this->day_of_year > 0 && this->day_of_year < 367; + } + return valid; } /** Convert a string to ESPTime struct as specified by the format argument. diff --git a/tests/components/time/posix_tz_parser.cpp b/tests/components/time/posix_tz_parser.cpp index d1747ef5b1..b7cf2a4afa 100644 --- a/tests/components/time/posix_tz_parser.cpp +++ b/tests/components/time/posix_tz_parser.cpp @@ -1036,8 +1036,6 @@ static time_t esptime_recalc_local(int year, int month, int day, int hour, int m t.hour = hour; t.minute = min; t.second = sec; - t.day_of_week = 1; // Placeholder for fields_in_range() - t.day_of_year = 1; t.recalc_timestamp_local(); return t.timestamp; } @@ -1187,6 +1185,60 @@ TEST(RecalcTimestampLocal, NonDefaultTransitionTime) { EXPECT_EQ(esp_result, libc_result); } +TEST(RecalcTimestampLocal, MinimalFieldsWithoutDayOfWeekOrYear) { + // Regression test for issue #15115: DateTimeEntity::state_as_esptime() constructs + // an ESPTime with only year/month/day/hour/minute/second set (no day_of_week or + // day_of_year). recalc_timestamp_local() must work without those fields. + const char *tz_str = "CET-1CEST,M3.5.0,M10.5.0"; + setenv("TZ", tz_str, 1); + tzset(); + time::ParsedTimezone tz{}; + ASSERT_TRUE(parse_posix_tz(tz_str, tz)); + set_global_tz(tz); + + // Construct ESPTime with only date/time fields (like state_as_esptime does) + ESPTime t{}; + t.year = 2026; + t.month = 3; + t.day_of_month = 20; + t.hour = 23; + t.minute = 14; + t.second = 55; + // day_of_week and day_of_year are deliberately left as 0 + t.recalc_timestamp_local(); + + // Must NOT return -1 (the bug: fields_in_range() rejected valid times) + EXPECT_NE(t.timestamp, -1); + + // Verify against libc + time_t libc_result = libc_mktime(2026, 3, 20, 23, 14, 55); + EXPECT_EQ(t.timestamp, libc_result); +} + +TEST(RecalcTimestampLocal, MinimalFieldsNoDST) { + // Same test but with a timezone that has no DST + const char *tz_str = "IST-5:30"; + setenv("TZ", tz_str, 1); + tzset(); + time::ParsedTimezone tz{}; + ASSERT_TRUE(parse_posix_tz(tz_str, tz)); + set_global_tz(tz); + + ESPTime t{}; + t.year = 2026; + t.month = 3; + t.day_of_month = 23; + t.hour = 10; + t.minute = 0; + t.second = 0; + t.recalc_timestamp_local(); + + EXPECT_NE(t.timestamp, -1); + + time_t libc_result = libc_mktime(2026, 3, 23, 10, 0, 0); + EXPECT_EQ(t.timestamp, libc_result); +} + TEST(RecalcTimestampLocal, YearBoundaryDST) { // Test southern hemisphere DST across year boundary // Australia/Sydney: DST active from October to April (spans Jan 1) From 238adbe008b5adbe60fca970cf96343e96a3dba9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 24 Mar 2026 14:04:17 -1000 Subject: [PATCH 141/166] [wifi] Fix roaming counter reset from delayed disconnect and successful retry (#15126) --- esphome/components/wifi/wifi_component.cpp | 66 +++++++++++++++++----- esphome/components/wifi/wifi_component.h | 6 ++ 2 files changed, 58 insertions(+), 14 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index e1d4b07471..2ed4b32a7a 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -287,18 +287,25 @@ bool CompactString::operator==(const StringRef &other) const { /// │ │ (counter reset to 0) │ │ (retry_connect called) │ /// │ └──────────────────────────────────┘ └───────────┬─────────────┘ /// │ │ │ -/// │ ↓ │ -/// │ ┌───────────────────────┐ │ -/// │ │ → IDLE │ │ -/// │ │ (counter preserved!) │ │ -/// │ └───────────────────────┘ │ +/// │ ┌─────────┴─────────┐ │ +/// │ ↓ ↓ │ +/// │ on target BSSID on other AP │ +/// │ │ │ │ +/// │ ↓ ↓ │ +/// │ ┌──────────────────┐ ┌────────────┐│ +/// │ │ → IDLE │ │ → IDLE ││ +/// │ │ (counter reset) │ │ (counter ││ +/// │ │ (roam worked!) │ │ preserved)││ +/// │ └──────────────────┘ └────────────┘│ /// │ │ /// │ Key behaviors: │ /// │ - After 3 checks: attempts >= 3, stop checking │ /// │ - Non-roaming disconnect: clear_roaming_state_() resets counter │ -/// │ - Disconnect during scan (SCANNING→RECONNECTING): counter preserved │ +/// │ - Disconnect during scan (SCANNING→RECONNECTING): counter preserved │ +/// │ - Disconnect after scan (within grace period): counter preserved │ /// │ - Roaming success (CONNECTING→IDLE): counter reset (can roam again) │ -/// │ - Roaming fail (RECONNECTING→IDLE): counter preserved (ping-pong) │ +/// │ - Roaming success via retry (on target BSSID): counter reset │ +/// │ - Roaming fail (RECONNECTING on other AP): counter preserved │ /// └──────────────────────────────────────────────────────────────────────┘ // Use if-chain instead of switch to avoid jump table in RODATA (wastes RAM on ESP8266) @@ -1576,17 +1583,33 @@ void WiFiComponent::check_connecting_finished(uint32_t now) { // Only preserve attempts if reconnecting after a failed roam attempt // This prevents ping-pong between APs when a roam target is unreachable if (this->roaming_state_ == RoamingState::CONNECTING) { - // Successful roam to better AP - reset attempts so we can roam again later + // Successful roam to better AP on first try - reset attempts so we can roam again later ESP_LOGD(TAG, "Roam successful"); this->roaming_attempts_ = 0; } else if (this->roaming_state_ == RoamingState::RECONNECTING) { - // Failed roam, reconnected via normal recovery - keep attempts to prevent ping-pong - ESP_LOGD(TAG, "Reconnected after failed roam (attempt %u/%u)", this->roaming_attempts_, ROAMING_MAX_ATTEMPTS); + // Check if we ended up on the roam target despite needing a retry + // (e.g., first connect failed but scan-based retry found and connected to the same better AP) + bssid_t current_bssid = this->wifi_bssid(); + if (this->roaming_target_bssid_ != bssid_t{} && current_bssid == this->roaming_target_bssid_) { + char bssid_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + format_mac_addr_upper(current_bssid.data(), bssid_buf); + ESP_LOGD(TAG, "Roam successful (via retry, attempt %u/%u) to %s", this->roaming_attempts_, ROAMING_MAX_ATTEMPTS, + bssid_buf); + this->roaming_attempts_ = 0; + } else if (this->roaming_target_bssid_ != bssid_t{}) { + // Failed roam to specific target, reconnected to different AP - keep attempts to prevent ping-pong + ESP_LOGD(TAG, "Reconnected after failed roam (attempt %u/%u)", this->roaming_attempts_, ROAMING_MAX_ATTEMPTS); + } else { + // Reconnected after scan-induced disconnect (no roam target) - keep attempts + ESP_LOGD(TAG, "Reconnected after roam scan (attempt %u/%u)", this->roaming_attempts_, ROAMING_MAX_ATTEMPTS); + } } else { // Normal connection (boot, credentials changed, etc.) this->roaming_attempts_ = 0; } this->roaming_state_ = RoamingState::IDLE; + this->roaming_target_bssid_ = {}; + this->roaming_scan_end_ = 0; // Clear all priority penalties - the next reconnect will happen when an AP disconnects, // which means the landscape has likely changed and previous tracked failures are stale @@ -2073,8 +2096,16 @@ void WiFiComponent::retry_connect() { ESP_LOGD(TAG, "Disconnected during roam scan (attempt %u/%u)", this->roaming_attempts_, ROAMING_MAX_ATTEMPTS); this->roaming_state_ = RoamingState::RECONNECTING; } else if (this->roaming_state_ == RoamingState::IDLE) { - // Not a roaming-triggered reconnect, reset state - this->clear_roaming_state_(); + // Check if a roaming scan recently completed - on ESP8266, going off-channel + // during scan can cause a delayed Beacon Timeout 8-20 seconds after scan finishes. + // Transition to RECONNECTING so the attempts counter is preserved on reconnect. + if (this->roaming_scan_end_ != 0 && millis() - this->roaming_scan_end_ < ROAMING_SCAN_GRACE_PERIOD) { + ESP_LOGD(TAG, "Disconnect after roam scan (attempt %u/%u)", this->roaming_attempts_, ROAMING_MAX_ATTEMPTS); + this->roaming_state_ = RoamingState::RECONNECTING; + } else { + // Not a roaming-triggered reconnect, reset state + this->clear_roaming_state_(); + } } // RECONNECTING: keep state and counter, still trying to reconnect @@ -2307,6 +2338,8 @@ bool WiFiScanResult::operator==(const WiFiScanResult &rhs) const { return this-> void WiFiComponent::clear_roaming_state_() { this->roaming_attempts_ = 0; this->roaming_last_check_ = 0; + this->roaming_scan_end_ = 0; + this->roaming_target_bssid_ = {}; this->roaming_state_ = RoamingState::IDLE; } @@ -2374,7 +2407,7 @@ void WiFiComponent::check_roaming_(uint32_t now) { // Guard: skip scan if signal is already good (no meaningful improvement possible) int8_t rssi = this->wifi_rssi(); if (rssi > ROAMING_GOOD_RSSI) { - ESP_LOGV(TAG, "Roam check skipped, signal good (%d dBm, attempt %u/%u)", rssi, this->roaming_attempts_, + ESP_LOGD(TAG, "Roam check skipped, signal good (%d dBm, attempt %u/%u)", rssi, this->roaming_attempts_, ROAMING_MAX_ATTEMPTS); return; } @@ -2388,6 +2421,9 @@ void WiFiComponent::process_roaming_scan_() { this->scan_done_ = false; // Default to IDLE - will be set to CONNECTING if we find a better AP this->roaming_state_ = RoamingState::IDLE; + // Record when scan completed so delayed disconnects (e.g., ESP8266 Beacon Timeout) + // can be attributed to the scan and avoid resetting the attempts counter + this->roaming_scan_end_ = millis(); // Get current connection info int8_t current_rssi = this->wifi_rssi(); @@ -2436,10 +2472,12 @@ void WiFiComponent::process_roaming_scan_() { WiFiAP roam_params = *selected; apply_scan_result_to_params(roam_params, *best); - this->release_scan_results_(); // Mark as roaming attempt - affects retry behavior if connection fails this->roaming_state_ = RoamingState::CONNECTING; + this->roaming_target_bssid_ = best->get_bssid(); // Must read before releasing scan results + + this->release_scan_results_(); // Connect directly - wifi_sta_connect_ handles disconnect internally this->start_connecting(roam_params); diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 718f4a6e12..99b23436f7 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -779,6 +779,10 @@ class WiFiComponent final : public Component { static constexpr int8_t ROAMING_MIN_IMPROVEMENT = 10; // dB static constexpr int8_t ROAMING_GOOD_RSSI = -49; // Skip scan if signal is excellent static constexpr uint8_t ROAMING_MAX_ATTEMPTS = 3; + // Grace period after roaming scan completes. If WiFi disconnects within this + // window (e.g., ESP8266 Beacon Timeout caused by going off-channel during scan), + // the disconnect is treated as roaming-related and the attempts counter is preserved. + static constexpr uint32_t ROAMING_SCAN_GRACE_PERIOD = 30 * 1000; // 30 seconds // 4-byte members float output_power_{NAN}; @@ -786,6 +790,7 @@ class WiFiComponent final : public Component { uint32_t last_connected_{0}; uint32_t reboot_timeout_{}; uint32_t roaming_last_check_{0}; + uint32_t roaming_scan_end_{0}; // Timestamp when last roaming scan completed #ifdef USE_WIFI_AP uint32_t ap_timeout_{}; #endif @@ -810,6 +815,7 @@ class WiFiComponent final : public Component { bool error_from_callback_{false}; RetryHiddenMode retry_hidden_mode_{RetryHiddenMode::BLIND_RETRY}; RoamingState roaming_state_{RoamingState::IDLE}; + bssid_t roaming_target_bssid_{}; // BSSID of the AP we're trying to roam to #if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE) WiFiPowerSaveMode configured_power_save_{WIFI_POWER_SAVE_NONE}; #endif From 9c9ae190ee040b7eb97f7e82d74e73b7ea6cd7d6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 24 Mar 2026 14:13:59 -1000 Subject: [PATCH 142/166] [core] Use compile-time HasElse parameter in IfAction (#15134) --- esphome/automation.py | 7 +++++-- esphome/core/base_automation.h | 27 ++++++++++++++------------- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/esphome/automation.py b/esphome/automation.py index 36ab30b654..17966dc782 100644 --- a/esphome/automation.py +++ b/esphome/automation.py @@ -413,13 +413,16 @@ async def if_action_to_code( template_arg: cg.TemplateArguments, args: TemplateArgsType, ) -> MockObj: + has_else = CONF_ELSE in config + # Prepend HasElse bool to template arguments: IfAction + if_template_arg = cg.TemplateArguments(has_else, *template_arg) cond_conf = next(el for el in config if el in (CONF_ANY, CONF_ALL, CONF_CONDITION)) condition = await build_condition(config[cond_conf], template_arg, args) - var = cg.new_Pvariable(action_id, template_arg, condition) + var = cg.new_Pvariable(action_id, if_template_arg, condition) if CONF_THEN in config: actions = await build_action_list(config[CONF_THEN], template_arg, args) cg.add(var.add_then(actions)) - if CONF_ELSE in config: + if has_else: actions = await build_action_list(config[CONF_ELSE], template_arg, args) cg.add(var.add_else(actions)) return var diff --git a/esphome/core/base_automation.h b/esphome/core/base_automation.h index 985f26e711..efcffa8824 100644 --- a/esphome/core/base_automation.h +++ b/esphome/core/base_automation.h @@ -264,7 +264,7 @@ template class WhileLoopContinuation : public Action { WhileAction *parent_; }; -template class IfAction : public Action { +template class IfAction : public Action { public: explicit IfAction(Condition *condition) : condition_(condition) {} @@ -273,27 +273,25 @@ template class IfAction : public Action { this->then_.add_action(new ContinuationAction(this)); } - void add_else(const std::initializer_list *> &actions) { + void add_else(const std::initializer_list *> &actions) requires(HasElse) { this->else_.add_actions(actions); this->else_.add_action(new ContinuationAction(this)); } void play_complex(const Ts &...x) override { this->num_running_++; - bool res = this->condition_->check(x...); - if (res) { - if (this->then_.empty()) { - this->play_next_(x...); - } else if (this->num_running_ > 0) { + if (this->condition_->check(x...)) { + if (!this->then_.empty() && this->num_running_ > 0) { this->then_.play(x...); + return; } - } else { - if (this->else_.empty()) { - this->play_next_(x...); - } else if (this->num_running_ > 0) { + } else if constexpr (HasElse) { + if (!this->else_.empty() && this->num_running_ > 0) { this->else_.play(x...); + return; } } + this->play_next_(x...); } void play(const Ts &...x) override { /* ignore - see play_complex */ @@ -301,13 +299,16 @@ template class IfAction : public Action { void stop() override { this->then_.stop(); - this->else_.stop(); + if constexpr (HasElse) { + this->else_.stop(); + } } protected: Condition *condition_; ActionList then_; - ActionList else_; + struct NoElse {}; + [[no_unique_address]] std::conditional_t, NoElse> else_; }; template class WhileAction : public Action { From 26e78c840ce4e35589245279e7d39f27039af851 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 24 Mar 2026 20:21:04 -0400 Subject: [PATCH 143/166] [wifi] Filter fast_connect by band_mode and use background scan for roaming (#15152) --- esphome/components/wifi/wifi_component.cpp | 8 ++++++++ esphome/components/wifi/wifi_component.h | 2 ++ esphome/components/wifi/wifi_component_esp_idf.cpp | 5 +++++ 3 files changed, 15 insertions(+) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 2ed4b32a7a..620d1a083d 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -2222,6 +2222,14 @@ bool WiFiComponent::load_fast_connect_settings_(WiFiAP ¶ms) { params.set_hidden(false); ESP_LOGD(TAG, "Loaded fast_connect settings"); +#if defined(USE_ESP32) && defined(SOC_WIFI_SUPPORT_5G) + if ((this->band_mode_ == WIFI_BAND_MODE_5G_ONLY && fast_connect_save.channel < FIRST_5GHZ_CHANNEL) || + (this->band_mode_ == WIFI_BAND_MODE_2G_ONLY && fast_connect_save.channel >= FIRST_5GHZ_CHANNEL)) { + ESP_LOGW(TAG, "Saved channel %u not allowed by band mode, ignoring fast_connect", fast_connect_save.channel); + this->selected_sta_index_ = -1; + return false; + } +#endif return true; } diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 99b23436f7..55e532c37d 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -774,6 +774,8 @@ class WiFiComponent final : public Component { SemaphoreHandle_t high_performance_semaphore_{nullptr}; #endif + static constexpr uint8_t FIRST_5GHZ_CHANNEL = 36; + // Post-connect roaming constants static constexpr uint32_t ROAMING_CHECK_INTERVAL = 5 * 60 * 1000; // 5 minutes static constexpr int8_t ROAMING_MIN_IMPROVEMENT = 10; // dB diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index 2866ec1513..1b80adc82e 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -987,6 +987,11 @@ bool WiFiComponent::wifi_scan_start_(bool passive) { config.scan_time.active.min = 100; config.scan_time.active.max = 300; } + // When scanning while connected (roaming), return to home channel between + // each scanned channel to maintain the connection (helps with BLE/WiFi coexistence) + if (this->roaming_state_ == RoamingState::SCANNING) { + config.coex_background_scan = true; + } esp_err_t err = esp_wifi_scan_start(&config, false); if (err != ESP_OK) { From 690dc324c97d753788491b1fa8c423776af2a1a9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 24 Mar 2026 14:52:37 -1000 Subject: [PATCH 144/166] [logger] Move task log buffer storage to BSS (#15153) --- esphome/components/logger/__init__.py | 20 +++++-------- esphome/components/logger/logger.cpp | 26 +++++------------ esphome/components/logger/logger.h | 13 +++------ .../logger/task_log_buffer_esp32.cpp | 18 +++--------- .../components/logger/task_log_buffer_esp32.h | 13 ++++----- .../logger/task_log_buffer_host.cpp | 17 +++-------- .../components/logger/task_log_buffer_host.h | 13 ++------- .../logger/task_log_buffer_libretiny.cpp | 29 +++++++------------ .../logger/task_log_buffer_libretiny.h | 12 ++++---- .../logger/task_log_buffer_zephyr.cpp | 13 ++++----- .../logger/task_log_buffer_zephyr.h | 10 ++++--- esphome/core/defines.h | 6 ++++ tests/benchmarks/components/main.cpp | 2 +- tests/components/main.cpp | 2 +- tests/dummy_main.cpp | 2 +- 15 files changed, 69 insertions(+), 127 deletions(-) diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index 4345e291a3..4144543b89 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -331,9 +331,8 @@ async def to_code(config: ConfigType) -> None: CORE.data.setdefault(CONF_LOGGER, {})[CONF_LEVEL] = level tx_buffer_size = config[CONF_TX_BUFFER_SIZE] cg.add_define("ESPHOME_LOGGER_TX_BUFFER_SIZE", tx_buffer_size) - # Determine task log buffer size and define USE_ESPHOME_TASK_LOG_BUFFER early - # so the constructor can allocate the buffer immediately, preventing a race - # where another task logs before the buffer is initialized. + # Determine task log buffer size. The buffer is a direct member of Logger + # (no separate heap allocation). task_log_buffer_size = 0 if CORE.is_esp32 or CORE.is_libretiny or CORE.is_nrf52: task_log_buffer_size = config[CONF_TASK_LOG_BUFFER_SIZE] @@ -341,16 +340,11 @@ async def to_code(config: ConfigType) -> None: task_log_buffer_size = 64 # Fixed 64 slots for host if task_log_buffer_size > 0: cg.add_define("USE_ESPHOME_TASK_LOG_BUFFER") - log = cg.new_Pvariable( - config[CONF_ID], - baud_rate, - task_log_buffer_size, - ) - else: - log = cg.new_Pvariable( - config[CONF_ID], - baud_rate, - ) + cg.add_define("ESPHOME_TASK_LOG_BUFFER_SIZE", task_log_buffer_size) + log = cg.new_Pvariable( + config[CONF_ID], + baud_rate, + ) if CORE.is_esp32 or CORE.is_host: cg.add(log.create_pthread_key()) # set_uart_selection() must be called before pre_setup() because diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index ceacded775..cd6543bfb8 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -83,7 +83,7 @@ void Logger::log_vprintf_non_main_thread_(uint8_t level, const char *tag, int li #ifdef USE_ESPHOME_TASK_LOG_BUFFER // For non-main threads/tasks, queue the message for callbacks message_sent = - this->log_buffer_->send_message_thread_safe(level, tag, static_cast(line), thread_name, format, args); + this->log_buffer_.send_message_thread_safe(level, tag, static_cast(line), thread_name, format, args); if (message_sent) { // Enable logger loop to process the buffered message // This is safe to call from any context including ISRs @@ -152,23 +152,13 @@ inline uint8_t Logger::level_for(const char *tag) { return this->current_level_; } -#ifdef USE_ESPHOME_TASK_LOG_BUFFER -Logger::Logger(uint32_t baud_rate, size_t task_log_buffer_size) : baud_rate_(baud_rate) { -#else Logger::Logger(uint32_t baud_rate) : baud_rate_(baud_rate) { -#endif #if defined(USE_ESP32) || defined(USE_LIBRETINY) this->main_task_ = xTaskGetCurrentTaskHandle(); #elif defined(USE_ZEPHYR) this->main_task_ = k_current_get(); #elif defined(USE_HOST) -this->main_thread_ = pthread_self(); -#endif -#ifdef USE_ESPHOME_TASK_LOG_BUFFER - // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) - allocated once, never freed - this->log_buffer_ = new logger::TaskLogBuffer(task_log_buffer_size); - // Note: we don't disable loop here because the component isn't registered with App yet. - // The loop self-disables on its first iteration when it finds no messages to process. + this->main_thread_ = pthread_self(); #endif } @@ -184,16 +174,16 @@ void Logger::loop() { void Logger::process_messages_() { #ifdef USE_ESPHOME_TASK_LOG_BUFFER // Process any buffered messages when available - if (this->log_buffer_->has_messages()) { + if (this->log_buffer_.has_messages()) { logger::TaskLogBuffer::LogMessage *message; uint16_t text_length; - while (this->log_buffer_->borrow_message_main_loop(message, text_length)) { + while (this->log_buffer_.borrow_message_main_loop(message, text_length)) { const char *thread_name = message->thread_name[0] != '\0' ? message->thread_name : nullptr; LogBuffer buf{this->tx_buffer_, ESPHOME_LOGGER_TX_BUFFER_SIZE}; this->format_buffered_message_and_notify_(message->level, message->tag, message->line, thread_name, message->text_data(), text_length, buf); // Release the message to allow other tasks to use it as soon as possible - this->log_buffer_->release_message_main_loop(); + this->log_buffer_.release_message_main_loop(); this->write_log_buffer_to_console_(buf); } } @@ -239,13 +229,11 @@ void Logger::dump_config() { this->baud_rate_, LOG_STR_ARG(get_uart_selection_())); #endif #ifdef USE_ESPHOME_TASK_LOG_BUFFER - if (this->log_buffer_) { #ifdef USE_HOST - ESP_LOGCONFIG(TAG, " Task Log Buffer Slots: %u", static_cast(this->log_buffer_->size())); + ESP_LOGCONFIG(TAG, " Task Log Buffer Slots: %u", static_cast(this->log_buffer_.size())); #else - ESP_LOGCONFIG(TAG, " Task Log Buffer Size: %u bytes", static_cast(this->log_buffer_->size())); + ESP_LOGCONFIG(TAG, " Task Log Buffer Size: %u bytes", static_cast(this->log_buffer_.size())); #endif - } #endif #ifdef USE_LOGGER_RUNTIME_TAG_LEVELS diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index c81b8e4e94..784cbea67e 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -143,11 +143,7 @@ enum UARTSelection : uint8_t { */ class Logger final : public Component { public: -#ifdef USE_ESPHOME_TASK_LOG_BUFFER - explicit Logger(uint32_t baud_rate, size_t task_log_buffer_size); -#else explicit Logger(uint32_t baud_rate); -#endif #if defined(USE_ESPHOME_TASK_LOG_BUFFER) || (defined(USE_ZEPHYR) && defined(USE_LOGGER_UART_SELECTION_USB_CDC)) void loop() override; #endif @@ -353,10 +349,6 @@ class Logger final : public Component { #ifdef USE_LOGGER_LEVEL_LISTENERS std::vector level_listeners_; // Log level change listeners #endif -#ifdef USE_ESPHOME_TASK_LOG_BUFFER - logger::TaskLogBuffer *log_buffer_{nullptr}; // Allocated once, never freed -#endif - // Group smaller types together at the end uint8_t current_level_{ESPHOME_LOG_LEVEL_VERY_VERBOSE}; #if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_ZEPHYR) @@ -374,8 +366,11 @@ class Logger final : public Component { bool global_recursion_guard_{false}; // Simple global recursion guard for single-task platforms #endif - // Large buffer placed last to keep frequently-accessed member offsets small + // Large buffers placed last to keep frequently-accessed member offsets small char tx_buffer_[ESPHOME_LOGGER_TX_BUFFER_SIZE + 1]; // +1 for null terminator +#ifdef USE_ESPHOME_TASK_LOG_BUFFER + logger::TaskLogBuffer log_buffer_; // Embedded in Logger (no separate heap allocation) +#endif // --- get_thread_name_ overloads (per-platform) --- diff --git a/esphome/components/logger/task_log_buffer_esp32.cpp b/esphome/components/logger/task_log_buffer_esp32.cpp index e747ddc4d8..cb97f5504f 100644 --- a/esphome/components/logger/task_log_buffer_esp32.cpp +++ b/esphome/components/logger/task_log_buffer_esp32.cpp @@ -1,33 +1,23 @@ #ifdef USE_ESP32 #include "task_log_buffer_esp32.h" -#include "esphome/core/helpers.h" #include "esphome/core/log.h" #ifdef USE_ESPHOME_TASK_LOG_BUFFER namespace esphome::logger { -TaskLogBuffer::TaskLogBuffer(size_t total_buffer_size) { - // Store the buffer size - this->size_ = total_buffer_size; - // Allocate memory for the ring buffer using ESPHome's RAM allocator - RAMAllocator allocator; - this->storage_ = allocator.allocate(this->size_); +TaskLogBuffer::TaskLogBuffer() { // Create a static ring buffer with RINGBUF_TYPE_NOSPLIT for message integrity - this->ring_buffer_ = xRingbufferCreateStatic(this->size_, RINGBUF_TYPE_NOSPLIT, this->storage_, &this->structure_); + // Storage is a member array (embedded in Logger), no heap allocation needed + this->ring_buffer_ = + xRingbufferCreateStatic(sizeof(this->storage_), RINGBUF_TYPE_NOSPLIT, this->storage_, &this->structure_); } TaskLogBuffer::~TaskLogBuffer() { if (this->ring_buffer_ != nullptr) { - // Delete the ring buffer vRingbufferDelete(this->ring_buffer_); this->ring_buffer_ = nullptr; - - // Free the allocated memory - RAMAllocator allocator; - allocator.deallocate(this->storage_, this->size_); - this->storage_ = nullptr; } } diff --git a/esphome/components/logger/task_log_buffer_esp32.h b/esphome/components/logger/task_log_buffer_esp32.h index 88d72eacfc..e819766795 100644 --- a/esphome/components/logger/task_log_buffer_esp32.h +++ b/esphome/components/logger/task_log_buffer_esp32.h @@ -8,7 +8,6 @@ #ifdef USE_ESPHOME_TASK_LOG_BUFFER #include #include -#include #include #include #include @@ -47,8 +46,7 @@ class TaskLogBuffer { inline const char *text_data() const { return reinterpret_cast(this) + sizeof(LogMessage); } }; - // Constructor that takes a total buffer size - explicit TaskLogBuffer(size_t total_buffer_size); + TaskLogBuffer(); ~TaskLogBuffer(); // NOT thread-safe - borrow a message from the ring buffer, only call from main loop @@ -67,13 +65,12 @@ class TaskLogBuffer { } // Get the total buffer size in bytes - inline size_t size() const { return size_; } + static constexpr size_t size() { return ESPHOME_TASK_LOG_BUFFER_SIZE; } private: - RingbufHandle_t ring_buffer_{nullptr}; // FreeRTOS ring buffer handle - StaticRingbuffer_t structure_; // Static structure for the ring buffer - uint8_t *storage_{nullptr}; // Pointer to allocated memory - size_t size_{0}; // Size of allocated memory + RingbufHandle_t ring_buffer_{nullptr}; // FreeRTOS ring buffer handle + StaticRingbuffer_t structure_; // Static structure for the ring buffer + uint8_t storage_[ESPHOME_TASK_LOG_BUFFER_SIZE]; // Embedded in Logger (no separate heap allocation) // Atomic counter for message tracking (only differences matter) std::atomic message_counter_{0}; // Incremented when messages are committed diff --git a/esphome/components/logger/task_log_buffer_host.cpp b/esphome/components/logger/task_log_buffer_host.cpp index c2ab009db4..8ebc946383 100644 --- a/esphome/components/logger/task_log_buffer_host.cpp +++ b/esphome/components/logger/task_log_buffer_host.cpp @@ -10,22 +10,13 @@ namespace esphome::logger { -TaskLogBuffer::TaskLogBuffer(size_t slot_count) : slot_count_(slot_count) { - // Allocate message slots - this->slots_ = std::make_unique(slot_count); -} - -TaskLogBuffer::~TaskLogBuffer() { - // unique_ptr handles cleanup automatically -} - int TaskLogBuffer::acquire_write_slot_() { // Try to reserve a slot using compare-and-swap size_t current_reserve = this->reserve_index_.load(std::memory_order_relaxed); while (true) { // Calculate next index (with wrap-around) - size_t next_reserve = (current_reserve + 1) % this->slot_count_; + size_t next_reserve = (current_reserve + 1) % ESPHOME_TASK_LOG_BUFFER_SIZE; // Check if buffer would be full // Buffer is full when next write position equals read position @@ -50,7 +41,7 @@ void TaskLogBuffer::commit_write_slot_(int slot_index) { // Try to advance the write_index if we're the next expected commit // This ensures messages are read in order size_t expected = slot_index; - size_t next = (slot_index + 1) % this->slot_count_; + size_t next = (slot_index + 1) % ESPHOME_TASK_LOG_BUFFER_SIZE; // We only advance write_index if this slot is the next one expected // This handles out-of-order commits correctly @@ -63,7 +54,7 @@ void TaskLogBuffer::commit_write_slot_(int slot_index) { // Successfully advanced, check if next slot is also ready expected = next; - next = (next + 1) % this->slot_count_; + next = (next + 1) % ESPHOME_TASK_LOG_BUFFER_SIZE; if (!this->slots_[expected].ready.load(std::memory_order_acquire)) { break; } @@ -142,7 +133,7 @@ void TaskLogBuffer::release_message_main_loop() { this->slots_[current_read].ready.store(false, std::memory_order_release); // Advance read index - size_t next_read = (current_read + 1) % this->slot_count_; + size_t next_read = (current_read + 1) % ESPHOME_TASK_LOG_BUFFER_SIZE; this->read_index_.store(next_read, std::memory_order_release); } diff --git a/esphome/components/logger/task_log_buffer_host.h b/esphome/components/logger/task_log_buffer_host.h index 1d4d2b0ec1..25e9c4da58 100644 --- a/esphome/components/logger/task_log_buffer_host.h +++ b/esphome/components/logger/task_log_buffer_host.h @@ -11,7 +11,6 @@ #include #include #include -#include #include namespace esphome::logger { @@ -50,9 +49,6 @@ namespace esphome::logger { */ class TaskLogBuffer { public: - // Default number of message slots - host has plenty of memory - static constexpr size_t DEFAULT_SLOT_COUNT = 64; - // Structure for a log message (fixed size for lock-free operation) struct LogMessage { // Size constants @@ -74,9 +70,7 @@ class TaskLogBuffer { inline char *text_data() { return this->text; } }; - /// Constructor that takes the number of message slots - explicit TaskLogBuffer(size_t slot_count); - ~TaskLogBuffer(); + TaskLogBuffer() = default; // NOT thread-safe - get next message from buffer, only call from main loop // Returns true if a message was retrieved, false if buffer is empty @@ -96,7 +90,7 @@ class TaskLogBuffer { } // Get the buffer size (number of slots) - inline size_t size() const { return slot_count_; } + static constexpr size_t size() { return ESPHOME_TASK_LOG_BUFFER_SIZE; } private: // Acquire a slot for writing (thread-safe) @@ -106,8 +100,7 @@ class TaskLogBuffer { // Commit a slot after writing (thread-safe) void commit_write_slot_(int slot_index); - std::unique_ptr slots_; // Pre-allocated message slots - size_t slot_count_; // Number of slots + LogMessage slots_[ESPHOME_TASK_LOG_BUFFER_SIZE]; // Embedded in Logger (no separate heap allocation) // Lock-free indices using atomics // - reserve_index_: Next slot to reserve (producers CAS this to claim slots) diff --git a/esphome/components/logger/task_log_buffer_libretiny.cpp b/esphome/components/logger/task_log_buffer_libretiny.cpp index 5969f6fb40..b6d6b22ab5 100644 --- a/esphome/components/logger/task_log_buffer_libretiny.cpp +++ b/esphome/components/logger/task_log_buffer_libretiny.cpp @@ -1,19 +1,15 @@ #ifdef USE_LIBRETINY #include "task_log_buffer_libretiny.h" -#include "esphome/core/helpers.h" #include "esphome/core/log.h" #ifdef USE_ESPHOME_TASK_LOG_BUFFER namespace esphome::logger { -TaskLogBuffer::TaskLogBuffer(size_t total_buffer_size) { - this->size_ = total_buffer_size; - // Allocate memory for the circular buffer using ESPHome's RAM allocator - RAMAllocator allocator; - this->storage_ = allocator.allocate(this->size_); +TaskLogBuffer::TaskLogBuffer() { // Create mutex for thread-safe access + // Storage is a member array (embedded in Logger), no heap allocation needed this->mutex_ = xSemaphoreCreateMutex(); } @@ -22,11 +18,6 @@ TaskLogBuffer::~TaskLogBuffer() { vSemaphoreDelete(this->mutex_); this->mutex_ = nullptr; } - if (this->storage_ != nullptr) { - RAMAllocator allocator; - allocator.deallocate(this->storage_, this->size_); - this->storage_ = nullptr; - } } size_t TaskLogBuffer::available_contiguous_space() const { @@ -34,7 +25,7 @@ size_t TaskLogBuffer::available_contiguous_space() const { // head is ahead of or equal to tail // Available space is from head to end, plus from start to tail // But for contiguous, just from head to end (minus 1 to avoid head==tail ambiguity) - size_t space_to_end = this->size_ - this->head_; + size_t space_to_end = ESPHOME_TASK_LOG_BUFFER_SIZE - this->head_; if (this->tail_ == 0) { // Can't use the last byte or head would equal tail return space_to_end > 0 ? space_to_end - 1 : 0; @@ -48,8 +39,8 @@ size_t TaskLogBuffer::available_contiguous_space() const { } bool TaskLogBuffer::borrow_message_main_loop(LogMessage *&message, uint16_t &text_length) { - // Check if buffer was initialized successfully - if (this->mutex_ == nullptr || this->storage_ == nullptr) { + // Check if mutex was initialized successfully + if (this->mutex_ == nullptr) { return false; } @@ -86,7 +77,7 @@ void TaskLogBuffer::release_message_main_loop() { this->tail_ += this->current_message_size_; // Handle wrap-around if we've reached the end - if (this->tail_ >= this->size_) { + if (this->tail_ >= ESPHOME_TASK_LOG_BUFFER_SIZE) { this->tail_ = 0; } @@ -115,9 +106,9 @@ bool TaskLogBuffer::send_message_thread_safe(uint8_t level, const char *tag, uin // Calculate total size needed (header + text length + null terminator) size_t total_size = message_total_size(text_length); - // Check if buffer was initialized successfully - if (this->mutex_ == nullptr || this->storage_ == nullptr) { - return false; // Buffer not initialized, fall back to direct output + // Check if mutex was initialized successfully + if (this->mutex_ == nullptr) { + return false; // Mutex not initialized, fall back to direct output } // Try to acquire mutex without blocking - don't block logging tasks @@ -185,7 +176,7 @@ bool TaskLogBuffer::send_message_thread_safe(uint8_t level, const char *tag, uin this->head_ += total_size; // Handle wrap-around (shouldn't happen due to contiguous space check, but be safe) - if (this->head_ >= this->size_) { + if (this->head_ >= ESPHOME_TASK_LOG_BUFFER_SIZE) { this->head_ = 0; } diff --git a/esphome/components/logger/task_log_buffer_libretiny.h b/esphome/components/logger/task_log_buffer_libretiny.h index c065065fe7..b42894502a 100644 --- a/esphome/components/logger/task_log_buffer_libretiny.h +++ b/esphome/components/logger/task_log_buffer_libretiny.h @@ -59,8 +59,7 @@ class TaskLogBuffer { // Valid log levels are 0-7, so 0xFF cannot be a real message static constexpr uint8_t PADDING_MARKER_LEVEL = 0xFF; - // Constructor that takes a total buffer size - explicit TaskLogBuffer(size_t total_buffer_size); + TaskLogBuffer(); ~TaskLogBuffer(); // NOT thread-safe - borrow a message from the buffer, only call from main loop @@ -78,7 +77,7 @@ class TaskLogBuffer { inline bool HOT has_messages() const { return this->message_count_ != 0; } // Get the total buffer size in bytes - inline size_t size() const { return this->size_; } + static constexpr size_t size() { return ESPHOME_TASK_LOG_BUFFER_SIZE; } private: // Calculate total size needed for a message (header + text + null terminator) @@ -87,10 +86,9 @@ class TaskLogBuffer { // Calculate available contiguous space at write position size_t available_contiguous_space() const; - uint8_t *storage_{nullptr}; // Pointer to allocated memory - size_t size_{0}; // Size of allocated memory - size_t head_{0}; // Write position - size_t tail_{0}; // Read position + uint8_t storage_[ESPHOME_TASK_LOG_BUFFER_SIZE]; // Embedded in Logger (no separate heap allocation) + size_t head_{0}; // Write position + size_t tail_{0}; // Read position SemaphoreHandle_t mutex_{nullptr}; // FreeRTOS mutex for thread safety volatile uint16_t message_count_{0}; // Fast check counter (dirty read OK) diff --git a/esphome/components/logger/task_log_buffer_zephyr.cpp b/esphome/components/logger/task_log_buffer_zephyr.cpp index 44d12d08a3..a994925a54 100644 --- a/esphome/components/logger/task_log_buffer_zephyr.cpp +++ b/esphome/components/logger/task_log_buffer_zephyr.cpp @@ -17,19 +17,16 @@ static inline uint32_t get_wlen(const mpsc_pbuf_generic *item) { return total_size_in_32bit_words(reinterpret_cast(item)->text_length); } -TaskLogBuffer::TaskLogBuffer(size_t total_buffer_size) { - // alignment to 4 bytes - total_buffer_size = (total_buffer_size + 3) / sizeof(uint32_t); - this->mpsc_config_.buf = new uint32_t[total_buffer_size]; - this->mpsc_config_.size = total_buffer_size; +TaskLogBuffer::TaskLogBuffer() { + // Storage is a member array (embedded in Logger), no heap allocation needed + this->mpsc_config_.buf = this->buf_storage_; + this->mpsc_config_.size = BUF_WORD_COUNT; this->mpsc_config_.flags = MPSC_PBUF_MODE_OVERWRITE; - this->mpsc_config_.get_wlen = get_wlen, + this->mpsc_config_.get_wlen = get_wlen; mpsc_pbuf_init(&this->log_buffer_, &this->mpsc_config_); } -TaskLogBuffer::~TaskLogBuffer() { delete[] this->mpsc_config_.buf; } - bool TaskLogBuffer::send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *thread_name, const char *format, va_list args) { // First, calculate the exact length needed using a null buffer (no actual writing) diff --git a/esphome/components/logger/task_log_buffer_zephyr.h b/esphome/components/logger/task_log_buffer_zephyr.h index cc2ed1f687..4f192366ad 100644 --- a/esphome/components/logger/task_log_buffer_zephyr.h +++ b/esphome/components/logger/task_log_buffer_zephyr.h @@ -33,15 +33,14 @@ class TaskLogBuffer { // Methods for accessing message contents inline char *text_data() { return reinterpret_cast(this) + sizeof(LogMessage); } }; - // Constructor that takes a total buffer size - explicit TaskLogBuffer(size_t total_buffer_size); - ~TaskLogBuffer(); + TaskLogBuffer(); + ~TaskLogBuffer() = default; // Check if there are messages ready to be processed using an atomic counter for performance inline bool HOT has_messages() { return mpsc_pbuf_is_pending(&this->log_buffer_); } // Get the total buffer size in bytes - inline size_t size() const { return this->mpsc_config_.size * sizeof(uint32_t); } + static constexpr size_t size() { return BUF_WORD_COUNT * sizeof(uint32_t); } // NOT thread-safe - borrow a message from the ring buffer, only call from main loop bool borrow_message_main_loop(LogMessage *&message, uint16_t &text_length); @@ -54,6 +53,9 @@ class TaskLogBuffer { const char *format, va_list args); protected: + // Round up byte size to 32-bit word count for mpsc_pbuf alignment requirement + static constexpr size_t BUF_WORD_COUNT = (ESPHOME_TASK_LOG_BUFFER_SIZE + 3) / sizeof(uint32_t); + uint32_t buf_storage_[BUF_WORD_COUNT]; // Embedded in Logger (no separate heap allocation) mpsc_pbuf_buffer_config mpsc_config_{}; mpsc_pbuf_buffer log_buffer_{}; const mpsc_pbuf_generic *current_token_{}; diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 676ad3024f..b5612a1d3f 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -200,6 +200,7 @@ #define USE_ESP32_CRASH_HANDLER #define USE_MQTT_IDF_ENQUEUE #define USE_ESPHOME_TASK_LOG_BUFFER +#define ESPHOME_TASK_LOG_BUFFER_SIZE 768 #define USE_OTA_ROLLBACK #define USE_ESP32_MIN_CHIP_REVISION_SET #define USE_ESP32_SRAM1_AS_IRAM @@ -373,18 +374,23 @@ #define USE_WEBSERVER #define USE_WEBSERVER_AUTH #define USE_WEBSERVER_PORT 80 // NOLINT +#define USE_ESPHOME_TASK_LOG_BUFFER +#define ESPHOME_TASK_LOG_BUFFER_SIZE 768 #endif #ifdef USE_HOST #define USE_HTTP_REQUEST_RESPONSE #define USE_SOCKET_IMPL_BSD_SOCKETS #define USE_SOCKET_SELECT_SUPPORT +#define USE_ESPHOME_TASK_LOG_BUFFER +#define ESPHOME_TASK_LOG_BUFFER_SIZE 64 #endif #ifdef USE_NRF52 #define ESPHOME_BLE_NUS_TX_RING_BUFFER_SIZE 512 #define ESPHOME_BLE_NUS_RX_RING_BUFFER_SIZE 512 #define USE_ESPHOME_TASK_LOG_BUFFER +#define ESPHOME_TASK_LOG_BUFFER_SIZE 768 #define USE_LOGGER_EARLY_MESSAGE #define USE_LOGGER_UART_SELECTION_USB_CDC #define USE_LOGGER_USB_CDC diff --git a/tests/benchmarks/components/main.cpp b/tests/benchmarks/components/main.cpp index 901dc44c07..9bc0c31a15 100644 --- a/tests/benchmarks/components/main.cpp +++ b/tests/benchmarks/components/main.cpp @@ -26,7 +26,7 @@ void setup() { // Log functions call global_logger->log_vprintf_() without a null check, // so we must set up a Logger before any test that triggers logging. - static esphome::logger::Logger test_logger(0, 64); + static esphome::logger::Logger test_logger(0); test_logger.set_log_level(ESPHOME_LOG_LEVEL); test_logger.pre_setup(); diff --git a/tests/components/main.cpp b/tests/components/main.cpp index 622b1f107b..373fde7151 100644 --- a/tests/components/main.cpp +++ b/tests/components/main.cpp @@ -22,7 +22,7 @@ void original_setup() { void setup() { // Log functions call global_logger->log_vprintf_() without a null check, // so we must set up a Logger before any test that triggers logging. - static esphome::logger::Logger test_logger(0, 64); + static esphome::logger::Logger test_logger(0); test_logger.set_log_level(ESPHOME_LOG_LEVEL); test_logger.pre_setup(); diff --git a/tests/dummy_main.cpp b/tests/dummy_main.cpp index 329286e2fa..6fa0c08aa3 100644 --- a/tests/dummy_main.cpp +++ b/tests/dummy_main.cpp @@ -15,7 +15,7 @@ void setup() { static char name[] = "livingroom"; static char friendly_name[] = "LivingRoom"; App.pre_setup(name, sizeof(name) - 1, friendly_name, sizeof(friendly_name) - 1); - auto *log = new logger::Logger(115200, 512); // NOLINT + auto *log = new logger::Logger(115200); // NOLINT log->pre_setup(); log->set_uart_selection(logger::UART_SELECTION_UART0); App.register_component_(log); From af5b98c635f3209cfcc940a8341545eaaf74749a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 24 Mar 2026 15:07:28 -1000 Subject: [PATCH 145/166] [time] Remove dummy placeholder values for recalc_timestamp_utc() (#15129) --- esphome/components/bm8563/bm8563.cpp | 1 - esphome/components/ds1307/ds1307.cpp | 3 --- esphome/components/gps/time/gps_time.cpp | 4 ---- esphome/components/pcf85063/pcf85063.cpp | 3 --- esphome/components/pcf8563/pcf8563.cpp | 3 --- esphome/components/rx8130/rx8130.cpp | 3 --- 6 files changed, 17 deletions(-) diff --git a/esphome/components/bm8563/bm8563.cpp b/esphome/components/bm8563/bm8563.cpp index 07831485c1..269acfea44 100644 --- a/esphome/components/bm8563/bm8563.cpp +++ b/esphome/components/bm8563/bm8563.cpp @@ -56,7 +56,6 @@ void BM8563::read_time() { ESPTime rtc_time; this->get_time_(rtc_time); this->get_date_(rtc_time); - rtc_time.day_of_year = 1; // unused by recalc_timestamp_utc, but needs to be valid ESP_LOGD(TAG, "Read time: %i-%i-%i %i, %i:%i:%i", rtc_time.year, rtc_time.month, rtc_time.day_of_month, rtc_time.day_of_week, rtc_time.hour, rtc_time.minute, rtc_time.second); diff --git a/esphome/components/ds1307/ds1307.cpp b/esphome/components/ds1307/ds1307.cpp index 5c0e98290b..8fff4213b4 100644 --- a/esphome/components/ds1307/ds1307.cpp +++ b/esphome/components/ds1307/ds1307.cpp @@ -40,11 +40,8 @@ void DS1307Component::read_time() { .hour = uint8_t(ds1307_.reg.hour + 10u * ds1307_.reg.hour_10), .day_of_week = uint8_t(ds1307_.reg.weekday), .day_of_month = uint8_t(ds1307_.reg.day + 10u * ds1307_.reg.day_10), - .day_of_year = 1, // ignored by recalc_timestamp_utc(false) .month = uint8_t(ds1307_.reg.month + 10u * ds1307_.reg.month_10), .year = uint16_t(ds1307_.reg.year + 10u * ds1307_.reg.year_10 + 2000), - .is_dst = false, // not used - .timestamp = 0 // overwritten by recalc_timestamp_utc(false) }; rtc_time.recalc_timestamp_utc(false); if (!rtc_time.is_valid()) { diff --git a/esphome/components/gps/time/gps_time.cpp b/esphome/components/gps/time/gps_time.cpp index cff8c1fb07..fb662a3d60 100644 --- a/esphome/components/gps/time/gps_time.cpp +++ b/esphome/components/gps/time/gps_time.cpp @@ -16,10 +16,6 @@ void GPSTime::from_tiny_gps_(TinyGPSPlus &tiny_gps) { val.year = tiny_gps.date.year(); val.month = tiny_gps.date.month(); val.day_of_month = tiny_gps.date.day(); - // Set these to valid value for recalc_timestamp_utc - it's not used for calculation - val.day_of_week = 1; - val.day_of_year = 1; - val.hour = tiny_gps.time.hour(); val.minute = tiny_gps.time.minute(); val.second = tiny_gps.time.second(); diff --git a/esphome/components/pcf85063/pcf85063.cpp b/esphome/components/pcf85063/pcf85063.cpp index 03ed78654f..1cf28a4955 100644 --- a/esphome/components/pcf85063/pcf85063.cpp +++ b/esphome/components/pcf85063/pcf85063.cpp @@ -40,11 +40,8 @@ void PCF85063Component::read_time() { .hour = uint8_t(pcf85063_.reg.hour + 10u * pcf85063_.reg.hour_10), .day_of_week = uint8_t(pcf85063_.reg.weekday), .day_of_month = uint8_t(pcf85063_.reg.day + 10u * pcf85063_.reg.day_10), - .day_of_year = 1, // ignored by recalc_timestamp_utc(false) .month = uint8_t(pcf85063_.reg.month + 10u * pcf85063_.reg.month_10), .year = uint16_t(pcf85063_.reg.year + 10u * pcf85063_.reg.year_10 + 2000), - .is_dst = false, // not used - .timestamp = 0, // overwritten by recalc_timestamp_utc(false) }; rtc_time.recalc_timestamp_utc(false); if (!rtc_time.is_valid()) { diff --git a/esphome/components/pcf8563/pcf8563.cpp b/esphome/components/pcf8563/pcf8563.cpp index dc68807aef..b748f0156a 100644 --- a/esphome/components/pcf8563/pcf8563.cpp +++ b/esphome/components/pcf8563/pcf8563.cpp @@ -40,11 +40,8 @@ void PCF8563Component::read_time() { .hour = uint8_t(pcf8563_.reg.hour + 10u * pcf8563_.reg.hour_10), .day_of_week = uint8_t(pcf8563_.reg.weekday), .day_of_month = uint8_t(pcf8563_.reg.day + 10u * pcf8563_.reg.day_10), - .day_of_year = 1, // ignored by recalc_timestamp_utc(false) .month = uint8_t(pcf8563_.reg.month + 10u * pcf8563_.reg.month_10), .year = uint16_t(pcf8563_.reg.year + 10u * pcf8563_.reg.year_10 + 2000), - .is_dst = false, // not used - .timestamp = 0, // overwritten by recalc_timestamp_utc(false) }; rtc_time.recalc_timestamp_utc(false); if (!rtc_time.is_valid()) { diff --git a/esphome/components/rx8130/rx8130.cpp b/esphome/components/rx8130/rx8130.cpp index 07ed7acc56..3b704d2551 100644 --- a/esphome/components/rx8130/rx8130.cpp +++ b/esphome/components/rx8130/rx8130.cpp @@ -77,11 +77,8 @@ void RX8130Component::read_time() { .hour = bcd2dec(date[2] & 0x3f), .day_of_week = static_cast((date[3] & 0x7f) ? __builtin_ctz(date[3] & 0x7f) + 1 : 1), .day_of_month = bcd2dec(date[4] & 0x3f), - .day_of_year = 1, // ignored by recalc_timestamp_utc(false) .month = bcd2dec(date[5] & 0x1f), .year = static_cast(bcd2dec(date[6]) + 2000), - .is_dst = false, // not used - .timestamp = 0 // overwritten by recalc_timestamp_utc(false) }; rtc_time.recalc_timestamp_utc(false); if (!rtc_time.is_valid()) { From 7a407595678d7b855fb79f8b2912fc13d1f1aaad Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 25 Mar 2026 08:55:12 +0000 Subject: [PATCH 146/166] Bump aioesphomeapi from 44.7.0 to 44.8.0 (#15159) 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 2e09e2ed99..9e75e6d039 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.19 esptool==5.2.0 click==8.3.1 esphome-dashboard==20260210.0 -aioesphomeapi==44.7.0 +aioesphomeapi==44.8.0 zeroconf==0.148.0 puremagic==1.30 ruamel.yaml==0.19.1 # dashboard_import From c45c9da771c47dfbdfa966902a60540e778792c8 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Wed, 25 Mar 2026 19:51:23 +1000 Subject: [PATCH 147/166] [lvgl] Various 9.5 fixes (#15157) --- esphome/components/lvgl/lvgl_esphome.cpp | 4 +- esphome/components/lvgl/lvgl_esphome.h | 2 +- esphome/components/lvgl/widgets/__init__.py | 2 +- esphome/components/lvgl/widgets/meter.py | 45 +++++++++++++++------ 4 files changed, 37 insertions(+), 16 deletions(-) diff --git a/esphome/components/lvgl/lvgl_esphome.cpp b/esphome/components/lvgl/lvgl_esphome.cpp index b3cb4d56ad..d26bcdc714 100644 --- a/esphome/components/lvgl/lvgl_esphome.cpp +++ b/esphome/components/lvgl/lvgl_esphome.cpp @@ -673,14 +673,14 @@ void LvglComponent::static_flush_cb(lv_display_t *disp_drv, const lv_area_t *are * @param color_end The color to apply to the last tick * @param width */ -void lv_scale_draw_event_cb(lv_event_t *e, uint16_t range_start, uint16_t range_end, lv_color_t color_start, +void lv_scale_draw_event_cb(lv_event_t *e, int16_t range_start, int16_t range_end, lv_color_t color_start, lv_color_t color_end, int width, bool local) { auto *scale = static_cast(lv_event_get_target(e)); lv_draw_task_t *task = lv_event_get_draw_task(e); if (lv_draw_task_get_type(task) == LV_DRAW_TASK_TYPE_LINE) { auto *line_dsc = static_cast(lv_draw_task_get_draw_dsc(task)); - auto tick = line_dsc->base.id1; + int tick = line_dsc->base.id2; if (tick >= range_start && tick <= range_end) { unsigned range = range_end - range_start; if (local) { diff --git a/esphome/components/lvgl/lvgl_esphome.h b/esphome/components/lvgl/lvgl_esphome.h index 66f823d549..7baeeb233b 100644 --- a/esphome/components/lvgl/lvgl_esphome.h +++ b/esphome/components/lvgl/lvgl_esphome.h @@ -52,7 +52,7 @@ extern std::string lv_event_code_name_for(lv_event_t *event); lv_obj_t *lv_container_create(lv_obj_t *parent); #ifdef USE_LVGL_SCALE -void lv_scale_draw_event_cb(lv_event_t *e, uint16_t range_start, uint16_t range_end, lv_color_t color_start, +void lv_scale_draw_event_cb(lv_event_t *e, int16_t range_start, int16_t range_end, lv_color_t color_start, lv_color_t color_end, int width, bool local); #endif #if LV_COLOR_DEPTH == 16 diff --git a/esphome/components/lvgl/widgets/__init__.py b/esphome/components/lvgl/widgets/__init__.py index a2a8cf2129..b383196963 100644 --- a/esphome/components/lvgl/widgets/__init__.py +++ b/esphome/components/lvgl/widgets/__init__.py @@ -158,7 +158,7 @@ class WidgetType: await self.on_create(var, config) w = Widget.create(wid, var, self, config) - if theme := theme_widget_map.get(self.w_type.name): + if theme := theme_widget_map.get(self.name): for part, states in theme.items(): part = "LV_PART_" + part.upper() for state, style in states.items(): diff --git a/esphome/components/lvgl/widgets/meter.py b/esphome/components/lvgl/widgets/meter.py index 6a7559c42c..d32efd145b 100644 --- a/esphome/components/lvgl/widgets/meter.py +++ b/esphome/components/lvgl/widgets/meter.py @@ -79,6 +79,7 @@ from ..types import ( from . import Widget, WidgetType, get_widgets, widget_to_code from .arc import CONF_ARC from .img import CONF_IMAGE +from .label import CONF_LABEL from .line import CONF_LINE CONF_ANGLE_RANGE = "angle_range" @@ -222,12 +223,31 @@ INDICATOR_SCHEMA = cv.Schema( } ) + +def _scale_validate(config): + if indicators := config.get(CONF_INDICATORS): + style_index = next( + ( + i + for i, indicator in enumerate(indicators) + if CONF_TICK_STYLE in indicator + ), + -1, + ) + if style_index >= 0 and CONF_TICKS not in config: + raise cv.Invalid( + "'tick_style' can't be applied if the enclosing scale has no 'ticks' configured", + path=[CONF_INDICATORS, style_index], + ) + return config + + SCALE_SCHEMA = cv.Schema( { cv.GenerateID(): cv.declare_id(lv_scale_t), cv.Optional(CONF_TICKS): cv.Schema( { - cv.Optional(CONF_COUNT, default=12): cv.positive_int, + cv.Optional(CONF_COUNT, default=12): cv.int_range(min=2), cv.Optional(CONF_WIDTH, default=2): cv.positive_int, cv.Optional(CONF_LENGTH, default=10): size, cv.Optional(CONF_RADIAL_OFFSET, default=0): size, @@ -251,7 +271,7 @@ SCALE_SCHEMA = cv.Schema( cv.Optional(CONF_INDICATORS): cv.ensure_list(INDICATOR_SCHEMA), cv.Optional(CONF_DRAW_TICKS_ON_TOP, default=True): bool, } -) +).add_extra(_scale_validate) METER_SCHEMA = { cv.Optional(CONF_PIVOT): STATE_SCHEMA, @@ -259,17 +279,14 @@ METER_SCHEMA = { cv.Optional(CONF_SCALES): cv.ensure_list(SCALE_SCHEMA), } +# Only handling light style at the moment LIGHT_STYLE = LVStyle( "lv_meter_light", { "bg_opa": 1.0, - "bg_color": 0xEEEEEE, - "line_width": 1, - "line_color": 0xEEEEEE, - "arc_width": 2, - "arc_color": 0xEEEEEE, + "bg_color": 0xFFFFFF, "pad_all": 10, - "border_width": 2, + "border_width": 3, "border_color": 0xEEEEEE, "radius": "LV_RADIUS_CIRCLE", }, @@ -329,7 +346,7 @@ class MeterType(WidgetType): ) def get_uses(self): - return CONF_SCALE, CONF_LINE, CONF_IMAGE + return CONF_SCALE, CONF_LINE, CONF_IMAGE, CONF_LABEL def validate(self, value): return cv.has_at_most_one_key(CONF_INDICATOR, CONF_PIVOT)(value) @@ -478,6 +495,8 @@ class MeterType(WidgetType): await iw.set_property(CONF_SRC, await lv_image.process(src)) await set_indicator_values(iw, v) + # Hide the scale line + lv.obj_set_style_arc_opa(scale_var, LV_OPA.TRANSP, LV_PART.MAIN) if ticks := scale_conf.get(CONF_TICKS): # Set total tick count lv.scale_set_total_tick_count(scale_var, ticks[CONF_COUNT]) @@ -503,8 +522,6 @@ class MeterType(WidgetType): LV_PART.ITEMS, ) - # Hide the scale line - lv.obj_set_style_arc_opa(scale_var, LV_OPA.TRANSP, LV_PART.MAIN) if CONF_MAJOR in ticks: major = ticks[CONF_MAJOR] # Set major tick frequency @@ -547,7 +564,11 @@ class MeterType(WidgetType): else: lv.scale_set_major_tick_every(scale_var, 0) else: - lv.scale_set_total_tick_count(scale_var, 0) + # Must have at least 2 ticks otherwise the scale isn't even drawn + lv.scale_set_total_tick_count(scale_var, 2) + # Hide the ticks by making them 0 width + lv_obj.set_style_line_width(scale_var, 0, LV_PART.ITEMS) + lv.scale_set_major_tick_every(scale_var, 0) # Add a pivot # Get the default style From f5bbff0b05a677e7aa0d28218291a8b868c0fb39 Mon Sep 17 00:00:00 2001 From: Piotr Szulc Date: Wed, 25 Mar 2026 12:40:39 +0100 Subject: [PATCH 148/166] [core] Add CONF_LIBRETINY constant to const.py (#15141) 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/const/__init__.py | 1 + esphome/components/libretiny/const.py | 1 - esphome/components/libretiny/text_sensor.py | 3 ++- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/esphome/components/const/__init__.py b/esphome/components/const/__init__.py index 2a972a2939..1fbf88c276 100644 --- a/esphome/components/const/__init__.py +++ b/esphome/components/const/__init__.py @@ -13,6 +13,7 @@ CONF_DATA_BITS = "data_bits" CONF_DRAW_ROUNDING = "draw_rounding" CONF_ENABLED = "enabled" CONF_IGNORE_NOT_FOUND = "ignore_not_found" +CONF_LIBRETINY = "libretiny" CONF_ON_PACKET = "on_packet" CONF_ON_RECEIVE = "on_receive" CONF_ON_STATE_CHANGE = "on_state_change" diff --git a/esphome/components/libretiny/const.py b/esphome/components/libretiny/const.py index bc4ca99ab4..332be0de1d 100644 --- a/esphome/components/libretiny/const.py +++ b/esphome/components/libretiny/const.py @@ -14,7 +14,6 @@ class LibreTinyComponent: supports_atomics: bool = False # True for Cortex-M4(F) with LDREX/STREX -CONF_LIBRETINY = "libretiny" CONF_LOGLEVEL = "loglevel" CONF_SDK_SILENT = "sdk_silent" CONF_GPIO_RECOVER = "gpio_recover" diff --git a/esphome/components/libretiny/text_sensor.py b/esphome/components/libretiny/text_sensor.py index fa33fb6c02..c1012774c8 100644 --- a/esphome/components/libretiny/text_sensor.py +++ b/esphome/components/libretiny/text_sensor.py @@ -1,5 +1,6 @@ import esphome.codegen as cg from esphome.components import text_sensor +from esphome.components.const import CONF_LIBRETINY import esphome.config_validation as cv from esphome.const import ( CONF_VERSION, @@ -7,7 +8,7 @@ from esphome.const import ( ICON_CELLPHONE_ARROW_DOWN, ) -from .const import CONF_LIBRETINY, LTComponent +from .const import LTComponent DEPENDENCIES = ["libretiny"] From 2355fcb44e0804b68d2892fbe49f574baf9a7a6a Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Wed, 25 Mar 2026 23:51:51 +1000 Subject: [PATCH 149/166] [lvgl] Update function and type names (#15109) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/lvgl/gradient.py | 2 +- esphome/components/lvgl/hello_world.yaml | 6 ++-- esphome/components/lvgl/lvgl_esphome.cpp | 10 +++---- esphome/components/lvgl/lvgl_esphome.h | 8 +++--- esphome/components/lvgl/schemas.py | 12 ++++++-- esphome/components/lvgl/trigger.py | 2 +- esphome/components/lvgl/types.py | 3 +- esphome/components/lvgl/widgets/canvas.py | 4 ++- esphome/components/lvgl/widgets/img.py | 4 +-- esphome/components/lvgl/widgets/meter.py | 4 +-- esphome/components/lvgl/widgets/tileview.py | 8 +++--- tests/components/lvgl/lvgl-package.yaml | 31 ++++++++------------- 12 files changed, 47 insertions(+), 47 deletions(-) diff --git a/esphome/components/lvgl/gradient.py b/esphome/components/lvgl/gradient.py index f3ded6a518..c4a3c8f2cb 100644 --- a/esphome/components/lvgl/gradient.py +++ b/esphome/components/lvgl/gradient.py @@ -31,7 +31,7 @@ GRADIENT_SCHEMA = cv.ensure_list( cv.Required(CONF_DIRECTION): cv.one_of( "HOR", "HORIZONTAL", "VER", "VERTICAL", upper=True ), - cv.Optional(CONF_DITHER, default="NONE"): LV_DITHER.one_of, + cv.Optional(CONF_DITHER): LV_DITHER.one_of, cv.Required(CONF_STOPS): cv.All( [ cv.Schema( diff --git a/esphome/components/lvgl/hello_world.yaml b/esphome/components/lvgl/hello_world.yaml index 359e73cd52..4af179a589 100644 --- a/esphome/components/lvgl/hello_world.yaml +++ b/esphome/components/lvgl/hello_world.yaml @@ -43,14 +43,14 @@ on_boot: lvgl.widget.refresh: hello_world_title_ hidden: !lambda |- - return lv_obj_get_width(lv_scr_act()) < 400; + return lv_obj_get_width(lv_screen_active()) < 400; - checkbox: text: Checkbox id: hello_world_checkbox_ on_boot: lvgl.widget.refresh: hello_world_checkbox_ hidden: !lambda |- - return lv_obj_get_width(lv_scr_act()) < 240; + return lv_obj_get_width(lv_screen_active()) < 240; on_click: lvgl.label.update: id: hello_world_label_ @@ -94,7 +94,7 @@ outline_width: 0 border_width: 0 hidden: !lambda |- - return lv_obj_get_width(lv_scr_act()) < 300 && lv_obj_get_height(lv_scr_act()) < 400; + return lv_obj_get_width(lv_screen_active()) < 300 && lv_obj_get_height(lv_screen_active()) < 400; widgets: - label: text_font: montserrat_14 diff --git a/esphome/components/lvgl/lvgl_esphome.cpp b/esphome/components/lvgl/lvgl_esphome.cpp index d26bcdc714..bf86a4e9ee 100644 --- a/esphome/components/lvgl/lvgl_esphome.cpp +++ b/esphome/components/lvgl/lvgl_esphome.cpp @@ -172,18 +172,18 @@ void LvglComponent::add_page(LvPageType *page) { page->setup(this->pages_.size() - 1); } -void LvglComponent::show_page(size_t index, lv_scr_load_anim_t anim, uint32_t time) { +void LvglComponent::show_page(size_t index, lv_screen_load_anim_t anim, uint32_t time) { if (index >= this->pages_.size()) return; this->current_page_ = index; if (anim == LV_SCREEN_LOAD_ANIM_NONE) { - lv_scr_load(this->pages_[this->current_page_]->obj); + lv_screen_load(this->pages_[this->current_page_]->obj); } else { - lv_scr_load_anim(this->pages_[this->current_page_]->obj, anim, time, 0, false); + lv_screen_load_anim(this->pages_[this->current_page_]->obj, anim, time, 0, false); } } -void LvglComponent::show_next_page(lv_scr_load_anim_t anim, uint32_t time) { +void LvglComponent::show_next_page(lv_screen_load_anim_t anim, uint32_t time) { if (this->pages_.empty() || (this->current_page_ == this->pages_.size() - 1 && !this->page_wrap_)) return; size_t start = this->current_page_; @@ -195,7 +195,7 @@ void LvglComponent::show_next_page(lv_scr_load_anim_t anim, uint32_t time) { this->show_page(this->current_page_, anim, time); } -void LvglComponent::show_prev_page(lv_scr_load_anim_t anim, uint32_t time) { +void LvglComponent::show_prev_page(lv_screen_load_anim_t anim, uint32_t time) { if (this->pages_.empty() || (this->current_page_ == 0 && !this->page_wrap_)) return; size_t start = this->current_page_; diff --git a/esphome/components/lvgl/lvgl_esphome.h b/esphome/components/lvgl/lvgl_esphome.h index 7baeeb233b..8de82d50c0 100644 --- a/esphome/components/lvgl/lvgl_esphome.h +++ b/esphome/components/lvgl/lvgl_esphome.h @@ -163,7 +163,7 @@ class LvglComponent : public PollingComponent { static void render_end_cb(lv_event_t *event); static void render_start_cb(lv_event_t *event); void dump_config() override; - lv_disp_t *get_disp() { return this->disp_; } + lv_display_t *get_disp() { return this->disp_; } lv_obj_t *get_screen_active() { return lv_display_get_screen_active(this->disp_); } // Pause or resume the display. // @param paused If true, pause the display. If false, resume the display. @@ -189,9 +189,9 @@ class LvglComponent : public PollingComponent { lv_event_code_t event3); void add_page(LvPageType *page); - void show_page(size_t index, lv_scr_load_anim_t anim, uint32_t time); - void show_next_page(lv_scr_load_anim_t anim, uint32_t time); - void show_prev_page(lv_scr_load_anim_t anim, uint32_t time); + void show_page(size_t index, lv_screen_load_anim_t anim, uint32_t time); + void show_next_page(lv_screen_load_anim_t anim, uint32_t time); + void show_prev_page(lv_screen_load_anim_t anim, uint32_t time); void set_page_wrap(bool wrap) { this->page_wrap_ = wrap; } void set_big_endian(bool big_endian) { this->big_endian_ = big_endian; } size_t get_current_page() const; diff --git a/esphome/components/lvgl/schemas.py b/esphome/components/lvgl/schemas.py index 4e2bfeae85..bcbb193ce3 100644 --- a/esphome/components/lvgl/schemas.py +++ b/esphome/components/lvgl/schemas.py @@ -250,9 +250,17 @@ STYLE_REMAP = { } -def remap_property(prop): +def remap_property(prop, record=True): + """ + Remap an old style property to new style property. + Optionally record the use of the deprecated property. + :param prop: Name of the style property to remap. + :param record: Whether to record the use of the deprecated property. + :return: The remapped property name, or ``prop`` if no remapping exists. + """ if prop in STYLE_REMAP: - get_remapped_uses().add(prop) + if record: + get_remapped_uses().add(prop) return STYLE_REMAP[prop] return prop diff --git a/esphome/components/lvgl/trigger.py b/esphome/components/lvgl/trigger.py index c5ad4d402e..077ff06bb7 100644 --- a/esphome/components/lvgl/trigger.py +++ b/esphome/components/lvgl/trigger.py @@ -72,7 +72,7 @@ async def generate_triggers(): dir = DIRECTIONS.mapper(dir) w.clear_flag("LV_OBJ_FLAG_SCROLLABLE") selected = literal( - f"lv_indev_get_gesture_dir(lv_indev_get_act()) == {dir}" + f"lv_indev_get_gesture_dir(lv_indev_active()) == {dir}" ) await add_trigger( conf, w, literal("LV_EVENT_GESTURE"), is_selected=selected diff --git a/esphome/components/lvgl/types.py b/esphome/components/lvgl/types.py index 03739f3ff1..8343a542a9 100644 --- a/esphome/components/lvgl/types.py +++ b/esphome/components/lvgl/types.py @@ -59,7 +59,6 @@ lv_style_t = cg.global_ns.struct("lv_style_t") lv_pseudo_button_t = lvgl_ns.class_("LvPseudoButton") lv_obj_base_t = cg.global_ns.class_("lv_obj_t", lv_pseudo_button_t) lv_obj_t_ptr = lv_obj_base_t.operator("ptr") -lv_disp_t = cg.global_ns.struct("lv_disp_t") lv_color_t = cg.global_ns.struct("lv_color_t") lv_opa_t = cg.global_ns.struct("lv_opa_t") lv_group_t = cg.global_ns.struct("lv_group_t") @@ -67,7 +66,7 @@ LVTouchListener = lvgl_ns.class_("LVTouchListener") LVEncoderListener = lvgl_ns.class_("LVEncoderListener") lv_obj_t = LvType("lv_obj_t") lv_page_t = LvType("LvPageType", parents=(LvCompound,)) -lv_img_t = LvType("lv_img_t") +lv_image_t = LvType("lv_image_t") lv_gradient_t = LvType("lv_grad_dsc_t") lv_event_t = LvType("lv_event_t") diff --git a/esphome/components/lvgl/widgets/canvas.py b/esphome/components/lvgl/widgets/canvas.py index c670e3732c..0e40d0dfbe 100644 --- a/esphome/components/lvgl/widgets/canvas.py +++ b/esphome/components/lvgl/widgets/canvas.py @@ -369,7 +369,9 @@ def _scale_map(config): def _get_prop_validator(prop): - return STYLE_PROPS.get(f"transform_{remap_property(prop)}") or STYLE_PROPS.get(prop) + return STYLE_PROPS.get( + f"transform_{remap_property(prop, False)}" + ) or STYLE_PROPS.get(prop) def _prop_validator(prop): diff --git a/esphome/components/lvgl/widgets/img.py b/esphome/components/lvgl/widgets/img.py index ed6fd30c09..8a046fea33 100644 --- a/esphome/components/lvgl/widgets/img.py +++ b/esphome/components/lvgl/widgets/img.py @@ -17,7 +17,7 @@ from ..defines import ( CONF_ZOOM, ) from ..lv_validation import lv_angle, lv_bool, lv_image, scale, size -from ..types import lv_img_t +from ..types import lv_image_t from . import Widget, WidgetType from .label import CONF_LABEL @@ -55,7 +55,7 @@ class ImgType(WidgetType): def __init__(self): super().__init__( CONF_IMAGE, - lv_img_t, + lv_image_t, (CONF_MAIN,), IMG_SCHEMA, IMG_MODIFY_SCHEMA, diff --git a/esphome/components/lvgl/widgets/meter.py b/esphome/components/lvgl/widgets/meter.py index d32efd145b..494f811a8e 100644 --- a/esphome/components/lvgl/widgets/meter.py +++ b/esphome/components/lvgl/widgets/meter.py @@ -73,7 +73,7 @@ from ..types import ( LvType, ObjUpdateAction, lv_event_t, - lv_img_t, + lv_image_t, lv_obj_t, ) from . import Widget, WidgetType, get_widgets, widget_to_code @@ -205,7 +205,7 @@ INDICATOR_SCHEMA = cv.Schema( INDICATOR_IMG_SCHEMA.extend( { cv.GenerateID(): cv.declare_id(lv_meter_indicator_image_t), - cv.GenerateID(CONF_IMAGE_ID): cv.declare_id(lv_img_t), + cv.GenerateID(CONF_IMAGE_ID): cv.declare_id(lv_image_t), } ), requires_component("image"), diff --git a/esphome/components/lvgl/widgets/tileview.py b/esphome/components/lvgl/widgets/tileview.py index dadaef7d07..8e9d95f349 100644 --- a/esphome/components/lvgl/widgets/tileview.py +++ b/esphome/components/lvgl/widgets/tileview.py @@ -29,7 +29,7 @@ lv_tile_t = LvType("lv_tileview_tile_t") lv_tileview_t = LvType( "lv_tileview_t", largs=[(lv_obj_t_ptr, "tile")], - lvalue=lambda w: w.get_property("tile_act"), + lvalue=lambda w: w.get_property("tile_active"), has_on_value=True, ) @@ -85,7 +85,7 @@ class TileviewType(WidgetType): await add_widgets(tile, tile_conf) if tiles: # Set the first tile as active - lv_obj.set_tile_id( + lv.tileview_set_tile_by_index( w.obj, tiles[0][CONF_COLUMN], tiles[0][CONF_ROW], literal("LV_ANIM_OFF") ) @@ -122,11 +122,11 @@ async def tileview_select(config, action_id, template_arg, args): async def do_select(w: Widget): if tile := config.get(CONF_TILE_ID): tile = await cg.get_variable(tile) - lv_obj.set_tile(w.obj, tile, literal(config[CONF_ANIMATED])) + lv.tileview_set_tile(w.obj, tile, literal(config[CONF_ANIMATED])) else: row = await lv_int.process(config[CONF_ROW]) column = await lv_int.process(config[CONF_COLUMN]) - lv_obj.set_tile_id( + lv.tileview_set_tile_by_index( widgets[0].obj, column, row, literal(config[CONF_ANIMATED]) ) lv.event_send(w.obj, LV_EVENT.VALUE_CHANGED, cg.nullptr) diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml index 606f57d6a1..b168578a98 100644 --- a/tests/components/lvgl/lvgl-package.yaml +++ b/tests/components/lvgl/lvgl-package.yaml @@ -43,9 +43,6 @@ lvgl: start_value: 0 end_value: 180 bg_color: light_blue - disp_bg_color: color_id - disp_bg_image: cat_image - disp_bg_opa: cover bottom_layer: widgets: - obj: @@ -58,7 +55,6 @@ lvgl: gradients: - id: color_bar direction: hor - # dither: err_diff stops: - color: 0xFF0000 position: 0 @@ -143,12 +139,11 @@ lvgl: body: text: This is a sample messagebox bg_color: 0x808080 - button_style: - bg_color: 0xff00 - border_width: 4 buttons: - id: msgbox_button text: Button + bg_color: 0x00ff00 + border_width: 4 - id: msgbox_apply text: "Close" on_click: @@ -160,8 +155,8 @@ lvgl: bg_opa: !lambda return 0.5; - lvgl.image.update: id: lv_image - zoom: !lambda return 512; - angle: !lambda return 100; + scale: !lambda return 512; + rotation: !lambda return 100; pivot_x: !lambda return 20; pivot_y: !lambda return 20; offset_x: !lambda return 20; @@ -287,8 +282,8 @@ lvgl: then: - lvgl.animimg.stop: anim_img - lvgl.update: - disp_bg_color: 0xffff00 - disp_bg_image: none + bottom_layer: + bg_color: 0xffff00 - lvgl.widget.show: message_box - label: text: "Hello shiny day" @@ -361,8 +356,6 @@ lvgl: pad_right: 10px pad_top: 10px shadow_color: light_blue - shadow_ofs_x: 5 - shadow_ofs_y: 5 shadow_opa: cover shadow_spread: 5 shadow_width: 10 @@ -373,12 +366,10 @@ lvgl: text_letter_space: 4 text_line_space: 4 text_opa: cover - transform_angle: 180 transform_rotation: 90 transform_height: 100 transform_pivot_x: 50% transform_pivot_y: 50% - transform_zoom: 0.5 transform_scale: 2.0 transform_scale_x: 1.5 transform_scale_y: 0.8 @@ -470,11 +461,11 @@ lvgl: id: button_button width: 20% height: 10% - transform_angle: !lambda return(180*100); + transform_rotation: !lambda return(180*100); arc_width: !lambda return 4; border_width: !lambda return 6; - shadow_ofs_x: !lambda return 6; - shadow_ofs_y: !lambda return 6; + shadow_offset_x: !lambda return 6; + shadow_offset_y: !lambda return 6; shadow_spread: !lambda return 6; shadow_width: !lambda return 6; pressed: @@ -646,8 +637,8 @@ lvgl: border_opa: 80% shadow_color: black shadow_width: 10 - shadow_ofs_x: 5 - shadow_ofs_y: 5 + shadow_offset_x: 5 + shadow_offset_y: 5 shadow_spread: 4 shadow_opa: cover outline_color: red From 6c981e83db9186381742618e5c5bf17f9da689e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Brandon=20der=20Bl=C3=A4tter?= Date: Wed, 25 Mar 2026 06:52:50 -0700 Subject: [PATCH 150/166] [hub75] Add SCAN_1_8_32PX_FULL wiring option (#15130) --- esphome/components/hub75/display.py | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/components/hub75/display.py b/esphome/components/hub75/display.py index ede5078c33..0d1b87941d 100644 --- a/esphome/components/hub75/display.py +++ b/esphome/components/hub75/display.py @@ -128,6 +128,7 @@ SCAN_WIRINGS = { "STANDARD_TWO_SCAN": Hub75ScanWiring.STANDARD_TWO_SCAN, "SCAN_1_4_16PX_HIGH": Hub75ScanWiring.SCAN_1_4_16PX_HIGH, "SCAN_1_8_32PX_HIGH": Hub75ScanWiring.SCAN_1_8_32PX_HIGH, + "SCAN_1_8_32PX_FULL": Hub75ScanWiring.SCAN_1_8_32PX_FULL, "SCAN_1_8_40PX_HIGH": Hub75ScanWiring.SCAN_1_8_40PX_HIGH, "SCAN_1_8_64PX_HIGH": Hub75ScanWiring.SCAN_1_8_64PX_HIGH, } From b66ff374a2be7a6ebf4761f8124546c9b14684f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Metrich?= <45318189+FredM67@users.noreply.github.com> Date: Wed, 25 Mar 2026 15:26:33 +0100 Subject: [PATCH 151/166] [esp32] Fix GPIO strapping pins and add USB-JTAG warnings (#15105) Co-authored-by: Claude Opus 4.6 Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- esphome/components/esp32/gpio_esp32_c3.py | 8 ++++++++ esphome/components/esp32/gpio_esp32_c6.py | 10 +++++++++- esphome/components/esp32/gpio_esp32_h2.py | 6 +++--- esphome/components/esp32/gpio_esp32_p4.py | 4 ++-- esphome/components/esp32/gpio_esp32_s3.py | 22 +++++++++++++++------- 5 files changed, 37 insertions(+), 13 deletions(-) diff --git a/esphome/components/esp32/gpio_esp32_c3.py b/esphome/components/esp32/gpio_esp32_c3.py index 93e0b97093..6eb002f3f0 100644 --- a/esphome/components/esp32/gpio_esp32_c3.py +++ b/esphome/components/esp32/gpio_esp32_c3.py @@ -14,6 +14,8 @@ _ESP32C3_SPI_PSRAM_PINS = { 17: "SPIQ", } +_ESP32C3_USB_JTAG_PINS = {18, 19} + _ESP32C3_STRAPPING_PINS = {2, 8, 9} _LOGGER = logging.getLogger(__name__) @@ -26,6 +28,12 @@ def esp32_c3_validate_gpio_pin(value: int) -> int: raise cv.Invalid( f"This pin cannot be used on ESP32-C3s and is already used by the SPI/PSRAM interface (function: {_ESP32C3_SPI_PSRAM_PINS[value]})" ) + if value in _ESP32C3_USB_JTAG_PINS: + _LOGGER.warning( + "GPIO%d is used by the USB-Serial-JTAG interface." + " Using this pin as GPIO will conflict with USB-Serial-JTAG.", + value, + ) return value diff --git a/esphome/components/esp32/gpio_esp32_c6.py b/esphome/components/esp32/gpio_esp32_c6.py index cfd3bca833..993606d9de 100644 --- a/esphome/components/esp32/gpio_esp32_c6.py +++ b/esphome/components/esp32/gpio_esp32_c6.py @@ -18,7 +18,9 @@ _ESP32C6_SPI_PSRAM_PINS = { 30: "SPID", } -_ESP32C6_STRAPPING_PINS = {8, 9, 15} +_ESP32C6_USB_JTAG_PINS = {12, 13} + +_ESP32C6_STRAPPING_PINS = {4, 5, 8, 9, 15} _LOGGER = logging.getLogger(__name__) @@ -30,6 +32,12 @@ def esp32_c6_validate_gpio_pin(value: int) -> int: raise cv.Invalid( f"This pin cannot be used on ESP32-C6s and is already used by the SPI/PSRAM interface (function: {_ESP32C6_SPI_PSRAM_PINS[value]})" ) + if value in _ESP32C6_USB_JTAG_PINS: + _LOGGER.warning( + "GPIO%d is used by the USB-Serial-JTAG interface." + " Using this pin as GPIO will conflict with USB-Serial-JTAG.", + value, + ) return value diff --git a/esphome/components/esp32/gpio_esp32_h2.py b/esphome/components/esp32/gpio_esp32_h2.py index 5e7a6158f9..9dd6537694 100644 --- a/esphome/components/esp32/gpio_esp32_h2.py +++ b/esphome/components/esp32/gpio_esp32_h2.py @@ -9,7 +9,7 @@ _ESP32H2_SPI_FLASH_PINS = {6, 7, 15, 16, 17, 18, 19, 20, 21} _ESP32H2_USB_JTAG_PINS = {26, 27} -_ESP32H2_STRAPPING_PINS = {2, 3, 8, 9, 25} +_ESP32H2_STRAPPING_PINS = {8, 9, 25} _LOGGER = logging.getLogger(__name__) @@ -26,8 +26,8 @@ def esp32_h2_validate_gpio_pin(value: int) -> int: ) if value in _ESP32H2_USB_JTAG_PINS: _LOGGER.warning( - "GPIO%d is reserved for the USB-Serial-JTAG interface.\n" - "To use this pin as GPIO, USB-Serial-JTAG will be disabled.", + "GPIO%d is used by the USB-Serial-JTAG interface." + " Using this pin as GPIO will conflict with USB-Serial-JTAG.", value, ) diff --git a/esphome/components/esp32/gpio_esp32_p4.py b/esphome/components/esp32/gpio_esp32_p4.py index 865db92652..6e9227c501 100644 --- a/esphome/components/esp32/gpio_esp32_p4.py +++ b/esphome/components/esp32/gpio_esp32_p4.py @@ -20,8 +20,8 @@ def esp32_p4_validate_gpio_pin(value: int) -> int: raise cv.Invalid(f"Invalid pin number: {value} (must be 0-54)") if value in _ESP32P4_USB_JTAG_PINS: _LOGGER.warning( - "GPIO%d is reserved for the USB-Serial-JTAG interface.\n" - "To use this pin as GPIO, USB-Serial-JTAG will be disabled.", + "GPIO%d is used by the USB-Serial-JTAG interface." + " Using this pin as GPIO will conflict with USB-Serial-JTAG.", value, ) diff --git a/esphome/components/esp32/gpio_esp32_s3.py b/esphome/components/esp32/gpio_esp32_s3.py index cb0eb8178c..f528de4ccd 100644 --- a/esphome/components/esp32/gpio_esp32_s3.py +++ b/esphome/components/esp32/gpio_esp32_s3.py @@ -5,7 +5,7 @@ import esphome.config_validation as cv from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER from esphome.pins import check_strapping_pin -_ESP_32S3_SPI_PSRAM_PINS = { +_ESP32S3_SPI_PSRAM_PINS = { 26: "SPICS1", 27: "SPIHD", 28: "SPIWP", @@ -15,7 +15,7 @@ _ESP_32S3_SPI_PSRAM_PINS = { 32: "SPID", } -_ESP_32_ESP32_S3R8_PSRAM_PINS = { +_ESP32S3R8_PSRAM_PINS = { 33: "SPIIO4", 34: "SPIIO5", 35: "SPIIO6", @@ -23,7 +23,9 @@ _ESP_32_ESP32_S3R8_PSRAM_PINS = { 37: "SPIDQS", } -_ESP_32S3_STRAPPING_PINS = {0, 3, 45, 46} +_ESP32S3_USB_JTAG_PINS = {19, 20} + +_ESP32S3_STRAPPING_PINS = {0, 3, 45, 46} _LOGGER = logging.getLogger(__name__) @@ -32,11 +34,11 @@ def esp32_s3_validate_gpio_pin(value: int) -> int: if value < 0 or value > 48: raise cv.Invalid(f"Invalid pin number: {value} (must be 0-48)") - if value in _ESP_32S3_SPI_PSRAM_PINS: + if value in _ESP32S3_SPI_PSRAM_PINS: raise cv.Invalid( - f"This pin cannot be used on ESP32-S3s and is already used by the SPI/PSRAM interface(function: {_ESP_32S3_SPI_PSRAM_PINS[value]})" + f"This pin cannot be used on ESP32-S3s and is already used by the SPI/PSRAM interface(function: {_ESP32S3_SPI_PSRAM_PINS[value]})" ) - if value in _ESP_32_ESP32_S3R8_PSRAM_PINS: + if value in _ESP32S3R8_PSRAM_PINS: _LOGGER.warning( "GPIO%d is used by the PSRAM interface on ESP32-S3R8 / ESP32-S3R8V and should be avoided on these models", value, @@ -46,6 +48,12 @@ def esp32_s3_validate_gpio_pin(value: int) -> int: # These pins are not exposed in GPIO mux (reason unknown) # but they're missing from IO_MUX list in datasheet raise cv.Invalid(f"The pin GPIO{value} is not usable on ESP32-S3s.") + if value in _ESP32S3_USB_JTAG_PINS: + _LOGGER.warning( + "GPIO%d is used by the USB-Serial-JTAG interface." + " Using this pin as GPIO will conflict with USB-Serial-JTAG.", + value, + ) return value @@ -61,5 +69,5 @@ def esp32_s3_validate_supports(value: dict[str, Any]) -> dict[str, Any]: # All ESP32 pins support input mode pass - check_strapping_pin(value, _ESP_32S3_STRAPPING_PINS, _LOGGER) + check_strapping_pin(value, _ESP32S3_STRAPPING_PINS, _LOGGER) return value From e0d8000007beb22f66fb093399379de8f592075c Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Thu, 26 Mar 2026 00:34:34 +1000 Subject: [PATCH 152/166] [ai] Add instructions regarding constructor parameters (#15091) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .ai/instructions.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/.ai/instructions.md b/.ai/instructions.md index 240a47a52f..a7e08f9c4d 100644 --- a/.ai/instructions.md +++ b/.ai/instructions.md @@ -124,6 +124,28 @@ This document provides essential context for AI models interacting with this pro * **Indentation:** Use spaces (two per indentation level), not tabs * **Type aliases:** Prefer `using type_t = int;` over `typedef int type_t;` * **Line length:** Wrap lines at no more than 120 characters + * **Constructor parameters vs setters:** Component properties that are both **required** and **invariant** + (never change after construction) should be constructor parameters rather than set via setter methods. + This makes the dependency explicit and prevents use of the object in an incompletely-initialized state. + In code generation, when calling `cg.new_Pvariable()` or the relevant helper function to create the component, pass these as arguments. + ```cpp + // Good - required invariant dependency as constructor parameter + class SourceTextSensor : public text_sensor::TextSensor, public Component { + public: + explicit SourceTextSensor(text::Text *source) : source_(source) {} + protected: + text::Text *source_; + }; + ``` + ```cpp + // Bad - required invariant dependency as setter + class SourceTextSensor : public text_sensor::TextSensor, public Component { + public: + void set_source(text::Text *source) { this->source_ = source; } + protected: + text::Text *source_{nullptr}; + }; + ``` * **Component Structure:** * **Standard Files:** From 5d67868ac6d72159b40f080f556bc8534a1831e9 Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Wed, 25 Mar 2026 15:39:46 +0100 Subject: [PATCH 153/166] [nextion] Fix inline doc parameter types for page and touch callbacks (#14972) --- esphome/components/nextion/nextion.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/nextion/nextion.h b/esphome/components/nextion/nextion.h index 2842e57ce8..bb5998cf5d 100644 --- a/esphome/components/nextion/nextion.h +++ b/esphome/components/nextion/nextion.h @@ -1160,13 +1160,13 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe /** Add a callback to be notified when the nextion changes pages. * - * @param callback The void(std::string) callback. + * @param callback The void(uint8_t) callback. */ template void add_new_page_callback(F &&callback) { this->page_callback_.add(std::forward(callback)); } /** Add a callback to be notified when Nextion has a touch event. * - * @param callback The void() callback. + * @param callback The void(uint8_t, uint8_t, bool) callback. */ template void add_touch_event_callback(F &&callback) { this->touch_callback_.add(std::forward(callback)); From a15389318f41373a4c4466c69056cff9f6466e4b Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 25 Mar 2026 11:57:33 -0400 Subject: [PATCH 154/166] [audio] Bump esp-audio-libs to 2.0.4 (#15164) --- esphome/components/audio/__init__.py | 2 +- esphome/idf_component.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/audio/__init__.py b/esphome/components/audio/__init__.py index 9cc80b9b33..acc3b5d351 100644 --- a/esphome/components/audio/__init__.py +++ b/esphome/components/audio/__init__.py @@ -204,7 +204,7 @@ async def to_code(config): add_idf_component( name="esphome/esp-audio-libs", - ref="2.0.3", + ref="2.0.4", ) data = _get_data() diff --git a/esphome/idf_component.yml b/esphome/idf_component.yml index 4148147a3b..c44853969e 100644 --- a/esphome/idf_component.yml +++ b/esphome/idf_component.yml @@ -2,7 +2,7 @@ dependencies: bblanchon/arduinojson: version: "7.4.2" esphome/esp-audio-libs: - version: 2.0.3 + version: 2.0.4 esphome/micro-opus: version: 0.3.6 espressif/esp-dsp: From 010516aef2f1a155f569fe51a8437e6a48a2f876 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 25 Mar 2026 07:33:17 -1000 Subject: [PATCH 155/166] [benchmark] Add sensor publish_state benchmarks (#15034) --- .../sensor/bench_sensor_publish.cpp | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 tests/benchmarks/components/sensor/bench_sensor_publish.cpp diff --git a/tests/benchmarks/components/sensor/bench_sensor_publish.cpp b/tests/benchmarks/components/sensor/bench_sensor_publish.cpp new file mode 100644 index 0000000000..9639191a4d --- /dev/null +++ b/tests/benchmarks/components/sensor/bench_sensor_publish.cpp @@ -0,0 +1,79 @@ +#include + +#include "esphome/components/sensor/sensor.h" + +namespace esphome::benchmarks { + +// Inner iteration count to amortize CodSpeed instrumentation overhead. +// Without this, the ~60ns per-iteration valgrind start/stop cost dominates +// sub-microsecond benchmarks. +static constexpr int kInnerIterations = 2000; + +// Test subclass to access protected configure_entity_() for benchmark setup. +class TestSensor : public sensor::Sensor { + public: + void configure(const char *name) { this->configure_entity_(name, 0x12345678, 0); } +}; + +// --- Sensor::publish_state() with no callbacks registered --- +// Measures baseline publish overhead: state assignment, logging, +// internal_send_state_to_frontend, ControllerRegistry notification. + +static void SensorPublish_NoCallbacks(benchmark::State &state) { + TestSensor sensor; + sensor.configure("test_sensor"); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + sensor.publish_state(static_cast(i)); + } + benchmark::DoNotOptimize(sensor.state); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(SensorPublish_NoCallbacks); + +// --- Sensor::publish_state() with one state callback --- +// Measures callback dispatch overhead through LazyCallbackManager. + +static void SensorPublish_WithCallback(benchmark::State &state) { + TestSensor sensor; + sensor.configure("test_sensor"); + + float callback_value = 0.0f; + sensor.add_on_state_callback([&callback_value](float value) { callback_value = value; }); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + sensor.publish_state(static_cast(i)); + } + benchmark::DoNotOptimize(callback_value); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(SensorPublish_WithCallback); + +// --- Sensor::publish_state() with the same value every time --- +// Steady-state pattern: sensor reports an unchanged reading. +// Sensor doesn't dedup today, so this exercises the same code path +// as changing values, but tracks the common real-world pattern +// separately for regression detection. + +static void SensorPublish_SameValue(benchmark::State &state) { + TestSensor sensor; + sensor.configure("test_sensor"); + + // Warm up so has_state is already set + sensor.publish_state(23.5f); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + sensor.publish_state(23.5f); + } + benchmark::DoNotOptimize(sensor.state); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(SensorPublish_SameValue); + +} // namespace esphome::benchmarks From a22d47c71924c2e6355d12cc014a134619e0e536 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 25 Mar 2026 07:36:53 -1000 Subject: [PATCH 156/166] [api] Add --no-states flag to esphome logs command (#15160) --- esphome/__main__.py | 11 +++++++- esphome/components/api/client.py | 18 +++++++++--- tests/unit_tests/test_main.py | 47 ++++++++++++++++++++++++++++---- 3 files changed, 65 insertions(+), 11 deletions(-) diff --git a/esphome/__main__.py b/esphome/__main__.py index 4b0fc2cec7..87abd7f796 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -1046,7 +1046,11 @@ def show_logs(config: ConfigType, args: ArgsProtocol, devices: list[str]) -> int ): from esphome.components.api.client import run_logs - return run_logs(config, network_devices) + return run_logs( + config, + network_devices, + subscribe_states=not getattr(args, "no_states", False), + ) if port_type in (PortType.NETWORK, PortType.MQTT) and has_mqtt_logging(): from esphome import mqtt @@ -1664,6 +1668,11 @@ def parse_args(argv): help="Reset the device before starting serial logs.", default=os.getenv("ESPHOME_SERIAL_LOGGING_RESET"), ) + parser_logs.add_argument( + "--no-states", + action="store_true", + help="Do not show entity state changes in log output.", + ) parser_discover = subparsers.add_parser( "discover", diff --git a/esphome/components/api/client.py b/esphome/components/api/client.py index 0e71ad8fcb..0c6c569c7d 100644 --- a/esphome/components/api/client.py +++ b/esphome/components/api/client.py @@ -32,7 +32,11 @@ if TYPE_CHECKING: _LOGGER = logging.getLogger(__name__) -async def async_run_logs(config: dict[str, Any], addresses: list[str]) -> None: +async def async_run_logs( + config: dict[str, Any], + addresses: list[str], + subscribe_states: bool = True, +) -> None: """Run the logs command in the event loop.""" conf = config["api"] name = config["esphome"]["name"] @@ -89,14 +93,20 @@ async def async_run_logs(config: dict[str, Any], addresses: list[str]) -> None: config, raw_line, backtrace_state=backtrace_state ) - stop = await async_run(cli, on_log, name=name) + stop = await async_run(cli, on_log, name=name, subscribe_states=subscribe_states) try: await asyncio.Event().wait() finally: await stop() -def run_logs(config: dict[str, Any], addresses: list[str]) -> None: +def run_logs( + config: dict[str, Any], + addresses: list[str], + subscribe_states: bool = True, +) -> None: """Run the logs command.""" with contextlib.suppress(KeyboardInterrupt): - asyncio.run(async_run_logs(config, addresses)) + asyncio.run( + async_run_logs(config, addresses, subscribe_states=subscribe_states) + ) diff --git a/tests/unit_tests/test_main.py b/tests/unit_tests/test_main.py index 5e36c06bb3..115ce38c93 100644 --- a/tests/unit_tests/test_main.py +++ b/tests/unit_tests/test_main.py @@ -1762,7 +1762,34 @@ def test_show_logs_api( assert result == 0 mock_run_logs.assert_called_once_with( - CORE.config, ["192.168.1.100", "192.168.1.101"] + CORE.config, ["192.168.1.100", "192.168.1.101"], subscribe_states=True + ) + + +@patch("esphome.components.api.client.run_logs") +def test_show_logs_api_no_states( + mock_run_logs: Mock, +) -> None: + """Test show_logs with --no-states flag.""" + setup_core( + config={ + "logger": {}, + CONF_API: {}, + CONF_MDNS: {CONF_DISABLED: False}, + }, + platform=PLATFORM_ESP32, + ) + mock_run_logs.return_value = 0 + + args = MockArgs() + args.no_states = True + devices = ["192.168.1.100"] + + result = show_logs(CORE.config, args, devices) + + assert result == 0 + mock_run_logs.assert_called_once_with( + CORE.config, ["192.168.1.100"], subscribe_states=False ) @@ -1788,7 +1815,9 @@ def test_show_logs_api_with_fqdn_mdns_disabled( assert result == 0 # Should use the FQDN directly, not try MQTT lookup - mock_run_logs.assert_called_once_with(CORE.config, ["device.example.com"]) + mock_run_logs.assert_called_once_with( + CORE.config, ["device.example.com"], subscribe_states=True + ) @patch("esphome.components.api.client.run_logs") @@ -1816,7 +1845,9 @@ def test_show_logs_api_with_mqtt_fallback( assert result == 0 mock_mqtt_get_ip.assert_called_once_with(CORE.config, "user", "pass", "client") - mock_run_logs.assert_called_once_with(CORE.config, ["192.168.1.200"]) + mock_run_logs.assert_called_once_with( + CORE.config, ["192.168.1.200"], subscribe_states=True + ) @patch("esphome.mqtt.show_logs") @@ -2746,7 +2777,7 @@ def test_show_logs_api_static_ip_with_mqttip( # Verify run_logs was called with both IPs mock_run_logs.assert_called_once_with( - CORE.config, ["192.168.1.100", "192.168.2.50"] + CORE.config, ["192.168.1.100", "192.168.2.50"], subscribe_states=True ) @@ -2782,7 +2813,9 @@ def test_show_logs_api_multiple_mqttip_resolves_once( # Note: "MQTT" is a different magic string from "MQTTIP", but both trigger MQTT resolution # The _resolve_network_devices helper filters out both after first resolution mock_run_logs.assert_called_once_with( - CORE.config, ["192.168.2.50", "192.168.2.51", "192.168.1.100"] + CORE.config, + ["192.168.2.50", "192.168.2.51", "192.168.1.100"], + subscribe_states=True, ) @@ -2862,7 +2895,9 @@ def test_show_logs_api_mqtt_timeout_fallback( mock_mqtt_get_ip.assert_called_once_with(CORE.config, "user", "pass", "client") # Verify run_logs was called with only the static IP (MQTT failed) - mock_run_logs.assert_called_once_with(CORE.config, ["192.168.1.100"]) + mock_run_logs.assert_called_once_with( + CORE.config, ["192.168.1.100"], subscribe_states=True + ) def test_detect_external_components_no_external( From 65d0a91fcc0ad85de3d7a1792ad95a0e8c8977ec Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Wed, 25 Mar 2026 19:01:52 +0100 Subject: [PATCH 157/166] [nextion] Add defined keys to `defines.h` (#14971) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/nextion/nextion.cpp | 8 ++--- esphome/core/defines.h | 7 ++++ tests/components/nextion/common.yaml | 47 ++++++++++++++++---------- 3 files changed, 41 insertions(+), 21 deletions(-) diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp index 85da6af48a..ac17e14312 100644 --- a/esphome/components/nextion/nextion.cpp +++ b/esphome/components/nextion/nextion.cpp @@ -50,10 +50,10 @@ bool Nextion::check_connect_() { return true; #ifdef USE_NEXTION_CONFIG_SKIP_CONNECTION_HANDSHAKE - ESP_LOGW(TAG, "Connected (no handshake)"); // Log the connection status without handshake - this->is_connected_ = true; // Set the connection status to true - return true; // Return true indicating the connection is set -#else // USE_NEXTION_CONFIG_SKIP_CONNECTION_HANDSHAKE + ESP_LOGW(TAG, "Connected (no handshake)"); // Log the connection status without handshake + this->connection_state_.is_connected_ = true; // Set the connection status to true + return true; // Return true indicating the connection is set +#else // USE_NEXTION_CONFIG_SKIP_CONNECTION_HANDSHAKE if (this->comok_sent_ == 0) { this->reset_(false); diff --git a/esphome/core/defines.h b/esphome/core/defines.h index b5612a1d3f..8cf331c4d6 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -115,6 +115,13 @@ #define SNTP_SERVER_COUNT 3 #define USE_MEDIA_PLAYER #define USE_MEDIA_SOURCE +#define USE_NEXTION_COMMAND_SPACING +#define USE_NEXTION_CONF_START_UP_PAGE +#define USE_NEXTION_CONFIG_DUMP_DEVICE_INFO +#define USE_NEXTION_CONFIG_EXIT_REPARSE_ON_START +#define USE_NEXTION_CONFIG_SKIP_CONNECTION_HANDSHAKE +#define USE_NEXTION_MAX_COMMANDS_PER_LOOP +#define USE_NEXTION_MAX_QUEUE_SIZE #define USE_NEXTION_TFT_UPLOAD #define USE_NUMBER #define USE_OUTPUT diff --git a/tests/components/nextion/common.yaml b/tests/components/nextion/common.yaml index 4373fe5462..d9493db50c 100644 --- a/tests/components/nextion/common.yaml +++ b/tests/components/nextion/common.yaml @@ -273,26 +273,39 @@ text_sensor: display: - platform: nextion id: main_lcd + auto_wake_on_touch: true + brightness: 80% + command_spacing: 5ms + dump_device_info: true + exit_reparse_on_start: true + lambda: |- + ESP_LOGD("display","Display is being tested!"); max_commands_per_loop: 20 + max_queue_age: 5000ms # Remove queue items after 5s max_queue_size: 50 - update_interval: 5s - on_sleep: - then: - lambda: 'ESP_LOGD("display","Display went to sleep");' - on_wake: - then: - lambda: 'ESP_LOGD("display","Display woke up");' - on_setup: - then: - lambda: 'ESP_LOGD("display","Display setup completed");' - on_page: - then: - lambda: 'ESP_LOGD("display","Display shows new page %u", x);' on_buffer_overflow: then: logger.log: "Nextion reported a buffer overflow!" - - command_spacing: 5ms - dump_device_info: true - max_queue_age: 5000ms # Remove queue items after 5s + on_page: + then: + lambda: 'ESP_LOGD("display","Display shows new page %u", x);' + on_setup: + then: + lambda: 'ESP_LOGD("display","Display setup completed");' + on_sleep: + then: + lambda: 'ESP_LOGD("display","Display went to sleep");' + on_touch: + then: + lambda: |- + ESP_LOGD("display", + "Display was touched at page %u, component %u, touch event: %s", + page_id, component_id, touch_event ? "press" : "release"); + on_wake: + then: + lambda: 'ESP_LOGD("display","Display woke up");' + update_interval: 5s + start_up_page: 1 startup_override_ms: 10000ms # Wait 10s for display ready + touch_sleep_timeout: 3 + wake_up_page: 2 From c42c6745b960b989e3373a7bedc663b6360d57f5 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 25 Mar 2026 14:06:48 -0400 Subject: [PATCH 158/166] [mcp9600] Fix setup success check using OR instead of AND (#15165) --- esphome/components/mcp9600/mcp9600.cpp | 28 +++++++++++++------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/esphome/components/mcp9600/mcp9600.cpp b/esphome/components/mcp9600/mcp9600.cpp index ff411bef7a..0c5362b4ba 100644 --- a/esphome/components/mcp9600/mcp9600.cpp +++ b/esphome/components/mcp9600/mcp9600.cpp @@ -40,20 +40,20 @@ void MCP9600Component::setup() { } bool success = this->write_byte(MCP9600_REGISTER_STATUS, 0x00); - success |= this->write_byte(MCP9600_REGISTER_SENSOR_CONFIG, uint8_t(0x00 | thermocouple_type_ << 4)); - success |= this->write_byte(MCP9600_REGISTER_CONFIG, 0x00); - success |= this->write_byte(MCP9600_REGISTER_ALERT1_CONFIG, 0x00); - success |= this->write_byte(MCP9600_REGISTER_ALERT2_CONFIG, 0x00); - success |= this->write_byte(MCP9600_REGISTER_ALERT3_CONFIG, 0x00); - success |= this->write_byte(MCP9600_REGISTER_ALERT4_CONFIG, 0x00); - success |= this->write_byte(MCP9600_REGISTER_ALERT1_HYSTERESIS, 0x00); - success |= this->write_byte(MCP9600_REGISTER_ALERT2_HYSTERESIS, 0x00); - success |= this->write_byte(MCP9600_REGISTER_ALERT3_HYSTERESIS, 0x00); - success |= this->write_byte(MCP9600_REGISTER_ALERT4_HYSTERESIS, 0x00); - success |= this->write_byte_16(MCP9600_REGISTER_ALERT1_LIMIT, 0x0000); - success |= this->write_byte_16(MCP9600_REGISTER_ALERT2_LIMIT, 0x0000); - success |= this->write_byte_16(MCP9600_REGISTER_ALERT3_LIMIT, 0x0000); - success |= this->write_byte_16(MCP9600_REGISTER_ALERT4_LIMIT, 0x0000); + success &= this->write_byte(MCP9600_REGISTER_SENSOR_CONFIG, uint8_t(0x00 | thermocouple_type_ << 4)); + success &= this->write_byte(MCP9600_REGISTER_CONFIG, 0x00); + success &= this->write_byte(MCP9600_REGISTER_ALERT1_CONFIG, 0x00); + success &= this->write_byte(MCP9600_REGISTER_ALERT2_CONFIG, 0x00); + success &= this->write_byte(MCP9600_REGISTER_ALERT3_CONFIG, 0x00); + success &= this->write_byte(MCP9600_REGISTER_ALERT4_CONFIG, 0x00); + success &= this->write_byte(MCP9600_REGISTER_ALERT1_HYSTERESIS, 0x00); + success &= this->write_byte(MCP9600_REGISTER_ALERT2_HYSTERESIS, 0x00); + success &= this->write_byte(MCP9600_REGISTER_ALERT3_HYSTERESIS, 0x00); + success &= this->write_byte(MCP9600_REGISTER_ALERT4_HYSTERESIS, 0x00); + success &= this->write_byte_16(MCP9600_REGISTER_ALERT1_LIMIT, 0x0000); + success &= this->write_byte_16(MCP9600_REGISTER_ALERT2_LIMIT, 0x0000); + success &= this->write_byte_16(MCP9600_REGISTER_ALERT3_LIMIT, 0x0000); + success &= this->write_byte_16(MCP9600_REGISTER_ALERT4_LIMIT, 0x0000); if (!success) { this->error_code_ = FAILED_TO_UPDATE_CONFIGURATION; From 19615f2eaeeb37c5245a7de66abe0965b0e740f8 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 25 Mar 2026 14:10:04 -0400 Subject: [PATCH 159/166] [bme68x_bsec2] Fix uninitialized bme68x_conf in measurement duration calculation (#15168) --- esphome/components/bme68x_bsec2/bme68x_bsec2.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/bme68x_bsec2/bme68x_bsec2.cpp b/esphome/components/bme68x_bsec2/bme68x_bsec2.cpp index ed2ec80896..cf516f6ca6 100644 --- a/esphome/components/bme68x_bsec2/bme68x_bsec2.cpp +++ b/esphome/components/bme68x_bsec2/bme68x_bsec2.cpp @@ -276,8 +276,8 @@ void BME68xBSEC2Component::run_() { } if (this->bsec_settings_.trigger_measurement && this->bsec_settings_.op_mode != BME68X_SLEEP_MODE) { - uint32_t meas_dur = 0; - meas_dur = bme68x_get_meas_dur(this->op_mode_, &bme68x_conf, &this->bme68x_); + bme68x_get_conf(&bme68x_conf, &this->bme68x_); + uint32_t meas_dur = bme68x_get_meas_dur(this->op_mode_, &bme68x_conf, &this->bme68x_); ESP_LOGV(TAG, "Queueing read in %uus", meas_dur); this->trigger_time_ns_ = curr_time_ns; this->set_timeout("read", meas_dur / 1000, [this]() { this->read_(this->trigger_time_ns_); }); From f6c5767a8347ecccb446e4ff60627f18bd354d18 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 25 Mar 2026 14:10:28 -0400 Subject: [PATCH 160/166] [inkplate] Use atomic GPIO write to prevent ISR race (#15166) --- esphome/components/inkplate/inkplate.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/inkplate/inkplate.cpp b/esphome/components/inkplate/inkplate.cpp index 326bdff774..3b4b1a63d5 100644 --- a/esphome/components/inkplate/inkplate.cpp +++ b/esphome/components/inkplate/inkplate.cpp @@ -229,7 +229,7 @@ void Inkplate::eink_off_() { this->oe_pin_->digital_write(false); this->gmod_pin_->digital_write(false); - GPIO.out &= ~(this->get_data_pin_mask_() | (1UL << this->cl_pin_->get_pin()) | (1UL << this->le_pin_->get_pin())); + GPIO.out_w1tc = this->get_data_pin_mask_() | (1UL << this->cl_pin_->get_pin()) | (1UL << this->le_pin_->get_pin()); this->ckv_pin_->digital_write(false); this->sph_pin_->digital_write(false); this->spv_pin_->digital_write(false); From d8fbce365aff406360a8ea64e980f25ff510f503 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 25 Mar 2026 09:38:20 -1000 Subject: [PATCH 161/166] Bump requests from 2.32.5 to 2.33.0 (#15170) 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 9e75e6d039..ce735f398a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -24,7 +24,7 @@ freetype-py==2.5.1 jinja2==3.1.6 bleak==2.1.1 smpclient==6.0.0 -requests==2.32.5 +requests==2.33.0 # esp-idf >= 5.0 requires this pyparsing >= 3.0 From ec60da893f6613edceccf6003bc23a3001ed50fe Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 25 Mar 2026 09:45:06 -1000 Subject: [PATCH 162/166] [core] Move state logging to client-side formatting, console to VERBOSE (#15155) --- .../alarm_control_panel.cpp | 2 +- .../binary_sensor/binary_sensor.cpp | 2 +- esphome/components/climate/climate.cpp | 48 +++++++++---------- esphome/components/cover/cover.cpp | 26 +++++----- esphome/components/datetime/date_entity.cpp | 10 ++-- .../components/datetime/datetime_entity.cpp | 16 +++---- esphome/components/datetime/time_entity.cpp | 10 ++-- esphome/components/event/event.cpp | 2 +- esphome/components/fan/fan.cpp | 22 ++++----- esphome/components/light/light_call.cpp | 24 +++++----- esphome/components/lock/lock.cpp | 6 +-- .../components/media_player/media_player.cpp | 10 ++-- esphome/components/number/number.cpp | 2 +- esphome/components/number/number_call.cpp | 8 ++-- esphome/components/select/select.cpp | 2 +- esphome/components/select/select_call.cpp | 4 +- esphome/components/sensor/sensor.cpp | 2 +- esphome/components/switch/switch.cpp | 2 +- esphome/components/text/text.cpp | 4 +- esphome/components/text/text_call.cpp | 4 +- .../components/text_sensor/text_sensor.cpp | 2 +- esphome/components/update/update_entity.cpp | 14 +++--- esphome/components/valve/valve.cpp | 22 ++++----- .../components/water_heater/water_heater.cpp | 26 +++++----- 24 files changed, 135 insertions(+), 135 deletions(-) diff --git a/esphome/components/alarm_control_panel/alarm_control_panel.cpp b/esphome/components/alarm_control_panel/alarm_control_panel.cpp index fb61776532..623241851a 100644 --- a/esphome/components/alarm_control_panel/alarm_control_panel.cpp +++ b/esphome/components/alarm_control_panel/alarm_control_panel.cpp @@ -31,7 +31,7 @@ void AlarmControlPanel::publish_state(AlarmControlPanelState state) { this->last_update_ = millis(); if (state != this->current_state_) { auto prev_state = this->current_state_; - ESP_LOGD(TAG, "'%s' >> %s (was %s)", this->get_name().c_str(), + ESP_LOGV(TAG, "'%s' >> %s (was %s)", this->get_name().c_str(), LOG_STR_ARG(alarm_control_panel_state_to_string(state)), LOG_STR_ARG(alarm_control_panel_state_to_string(prev_state))); this->current_state_ = state; diff --git a/esphome/components/binary_sensor/binary_sensor.cpp b/esphome/components/binary_sensor/binary_sensor.cpp index c4d3a29a1e..8ace7eafd1 100644 --- a/esphome/components/binary_sensor/binary_sensor.cpp +++ b/esphome/components/binary_sensor/binary_sensor.cpp @@ -45,7 +45,7 @@ bool BinarySensor::set_new_state(const optional &new_state) { #if defined(USE_BINARY_SENSOR) && defined(USE_CONTROLLER_REGISTRY) ControllerRegistry::notify_binary_sensor_update(this); #endif - ESP_LOGD(TAG, "'%s' >> %s", this->get_name().c_str(), ONOFFMAYBE(new_state)); + ESP_LOGV(TAG, "'%s' >> %s", this->get_name().c_str(), ONOFFMAYBE(new_state)); return true; } return false; diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index 3f44b986dc..5cbe9a5daf 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -46,45 +46,45 @@ constexpr StringToUint8 CLIMATE_SWING_MODES_BY_STR[] = { void ClimateCall::perform() { this->parent_->control_callback_.call(*this); - ESP_LOGD(TAG, "'%s' - Setting", this->parent_->get_name().c_str()); + ESP_LOGV(TAG, "'%s' - Setting", this->parent_->get_name().c_str()); this->validate_(); if (this->mode_.has_value()) { const LogString *mode_s = climate_mode_to_string(*this->mode_); - ESP_LOGD(TAG, " Mode: %s", LOG_STR_ARG(mode_s)); + ESP_LOGV(TAG, " Mode: %s", LOG_STR_ARG(mode_s)); } if (this->custom_fan_mode_ != nullptr) { this->fan_mode_.reset(); - ESP_LOGD(TAG, " Custom Fan: %s", this->custom_fan_mode_); + ESP_LOGV(TAG, " Custom Fan: %s", this->custom_fan_mode_); } if (this->fan_mode_.has_value()) { this->custom_fan_mode_ = nullptr; const LogString *fan_mode_s = climate_fan_mode_to_string(*this->fan_mode_); - ESP_LOGD(TAG, " Fan: %s", LOG_STR_ARG(fan_mode_s)); + ESP_LOGV(TAG, " Fan: %s", LOG_STR_ARG(fan_mode_s)); } if (this->custom_preset_ != nullptr) { this->preset_.reset(); - ESP_LOGD(TAG, " Custom Preset: %s", this->custom_preset_); + ESP_LOGV(TAG, " Custom Preset: %s", this->custom_preset_); } if (this->preset_.has_value()) { this->custom_preset_ = nullptr; const LogString *preset_s = climate_preset_to_string(*this->preset_); - ESP_LOGD(TAG, " Preset: %s", LOG_STR_ARG(preset_s)); + ESP_LOGV(TAG, " Preset: %s", LOG_STR_ARG(preset_s)); } if (this->swing_mode_.has_value()) { const LogString *swing_mode_s = climate_swing_mode_to_string(*this->swing_mode_); - ESP_LOGD(TAG, " Swing: %s", LOG_STR_ARG(swing_mode_s)); + ESP_LOGV(TAG, " Swing: %s", LOG_STR_ARG(swing_mode_s)); } if (this->target_temperature_.has_value()) { - ESP_LOGD(TAG, " Target Temperature: %.2f", *this->target_temperature_); + ESP_LOGV(TAG, " Target Temperature: %.2f", *this->target_temperature_); } if (this->target_temperature_low_.has_value()) { - ESP_LOGD(TAG, " Target Temperature Low: %.2f", *this->target_temperature_low_); + ESP_LOGV(TAG, " Target Temperature Low: %.2f", *this->target_temperature_low_); } if (this->target_temperature_high_.has_value()) { - ESP_LOGD(TAG, " Target Temperature High: %.2f", *this->target_temperature_high_); + ESP_LOGV(TAG, " Target Temperature High: %.2f", *this->target_temperature_high_); } if (this->target_humidity_.has_value()) { - ESP_LOGD(TAG, " Target Humidity: %.0f", *this->target_humidity_); + ESP_LOGV(TAG, " Target Humidity: %.0f", *this->target_humidity_); } this->parent_->control(*this); } @@ -435,43 +435,43 @@ void Climate::save_state_() { } void Climate::publish_state() { - ESP_LOGD(TAG, "'%s' >>", this->name_.c_str()); + ESP_LOGV(TAG, "'%s' >>", this->name_.c_str()); auto traits = this->get_traits(); - ESP_LOGD(TAG, " Mode: %s", LOG_STR_ARG(climate_mode_to_string(this->mode))); + ESP_LOGV(TAG, " Mode: %s", LOG_STR_ARG(climate_mode_to_string(this->mode))); if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_ACTION)) { - ESP_LOGD(TAG, " Action: %s", LOG_STR_ARG(climate_action_to_string(this->action))); + ESP_LOGV(TAG, " Action: %s", LOG_STR_ARG(climate_action_to_string(this->action))); } if (traits.get_supports_fan_modes() && this->fan_mode.has_value()) { - ESP_LOGD(TAG, " Fan Mode: %s", LOG_STR_ARG(climate_fan_mode_to_string(this->fan_mode.value()))); + ESP_LOGV(TAG, " Fan Mode: %s", LOG_STR_ARG(climate_fan_mode_to_string(this->fan_mode.value()))); } if (!traits.get_supported_custom_fan_modes().empty() && this->has_custom_fan_mode()) { - ESP_LOGD(TAG, " Custom Fan Mode: %s", this->custom_fan_mode_); + ESP_LOGV(TAG, " Custom Fan Mode: %s", this->custom_fan_mode_); } if (traits.get_supports_presets() && this->preset.has_value()) { - ESP_LOGD(TAG, " Preset: %s", LOG_STR_ARG(climate_preset_to_string(this->preset.value()))); + ESP_LOGV(TAG, " Preset: %s", LOG_STR_ARG(climate_preset_to_string(this->preset.value()))); } if (!traits.get_supported_custom_presets().empty() && this->has_custom_preset()) { - ESP_LOGD(TAG, " Custom Preset: %s", this->custom_preset_); + ESP_LOGV(TAG, " Custom Preset: %s", this->custom_preset_); } if (traits.get_supports_swing_modes()) { - ESP_LOGD(TAG, " Swing Mode: %s", LOG_STR_ARG(climate_swing_mode_to_string(this->swing_mode))); + ESP_LOGV(TAG, " Swing Mode: %s", LOG_STR_ARG(climate_swing_mode_to_string(this->swing_mode))); } if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE)) { - ESP_LOGD(TAG, " Current Temperature: %.2f°C", this->current_temperature); + ESP_LOGV(TAG, " Current Temperature: %.2f°C", this->current_temperature); } if (traits.has_feature_flags(CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE | CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) { - ESP_LOGD(TAG, " Target Temperature: Low: %.2f°C High: %.2f°C", this->target_temperature_low, + ESP_LOGV(TAG, " Target Temperature: Low: %.2f°C High: %.2f°C", this->target_temperature_low, this->target_temperature_high); } else { - ESP_LOGD(TAG, " Target Temperature: %.2f°C", this->target_temperature); + ESP_LOGV(TAG, " Target Temperature: %.2f°C", this->target_temperature); } if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY)) { - ESP_LOGD(TAG, " Current Humidity: %.0f%%", this->current_humidity); + ESP_LOGV(TAG, " Current Humidity: %.0f%%", this->current_humidity); } if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY)) { - ESP_LOGD(TAG, " Target Humidity: %.0f%%", this->target_humidity); + ESP_LOGV(TAG, " Target Humidity: %.0f%%", this->target_humidity); } // Send state to frontend diff --git a/esphome/components/cover/cover.cpp b/esphome/components/cover/cover.cpp index bb5965d861..e98a555fe5 100644 --- a/esphome/components/cover/cover.cpp +++ b/esphome/components/cover/cover.cpp @@ -68,24 +68,24 @@ CoverCall &CoverCall::set_tilt(float tilt) { return *this; } void CoverCall::perform() { - ESP_LOGD(TAG, "'%s' - Setting", this->parent_->get_name().c_str()); + ESP_LOGV(TAG, "'%s' - Setting", this->parent_->get_name().c_str()); auto traits = this->parent_->get_traits(); this->validate_(); if (this->stop_) { - ESP_LOGD(TAG, " Command: STOP"); + ESP_LOGV(TAG, " Command: STOP"); } if (this->position_.has_value()) { if (traits.get_supports_position()) { - ESP_LOGD(TAG, " Position: %.0f%%", *this->position_ * 100.0f); + ESP_LOGV(TAG, " Position: %.0f%%", *this->position_ * 100.0f); } else { - ESP_LOGD(TAG, " Command: %s", LOG_STR_ARG(cover_command_to_str(*this->position_))); + ESP_LOGV(TAG, " Command: %s", LOG_STR_ARG(cover_command_to_str(*this->position_))); } } if (this->tilt_.has_value()) { - ESP_LOGD(TAG, " Tilt: %.0f%%", *this->tilt_ * 100.0f); + ESP_LOGV(TAG, " Tilt: %.0f%%", *this->tilt_ * 100.0f); } if (this->toggle_.has_value()) { - ESP_LOGD(TAG, " Command: TOGGLE"); + ESP_LOGV(TAG, " Command: TOGGLE"); } this->parent_->control(*this); } @@ -143,23 +143,23 @@ void Cover::publish_state(bool save) { this->position = clamp(this->position, 0.0f, 1.0f); this->tilt = clamp(this->tilt, 0.0f, 1.0f); - ESP_LOGD(TAG, "'%s' >>", this->name_.c_str()); + ESP_LOGV(TAG, "'%s' >>", this->name_.c_str()); auto traits = this->get_traits(); if (traits.get_supports_position()) { - ESP_LOGD(TAG, " Position: %.0f%%", this->position * 100.0f); + ESP_LOGV(TAG, " Position: %.0f%%", this->position * 100.0f); } else { if (this->position == COVER_OPEN) { - ESP_LOGD(TAG, " State: OPEN"); + ESP_LOGV(TAG, " State: OPEN"); } else if (this->position == COVER_CLOSED) { - ESP_LOGD(TAG, " State: CLOSED"); + ESP_LOGV(TAG, " State: CLOSED"); } else { - ESP_LOGD(TAG, " State: UNKNOWN"); + ESP_LOGV(TAG, " State: UNKNOWN"); } } if (traits.get_supports_tilt()) { - ESP_LOGD(TAG, " Tilt: %.0f%%", this->tilt * 100.0f); + ESP_LOGV(TAG, " Tilt: %.0f%%", this->tilt * 100.0f); } - ESP_LOGD(TAG, " Current Operation: %s", LOG_STR_ARG(cover_operation_to_str(this->current_operation))); + ESP_LOGV(TAG, " Current Operation: %s", LOG_STR_ARG(cover_operation_to_str(this->current_operation))); this->state_callback_.call(); #if defined(USE_COVER) && defined(USE_CONTROLLER_REGISTRY) diff --git a/esphome/components/datetime/date_entity.cpp b/esphome/components/datetime/date_entity.cpp index 3ba488c0aa..997aec3f69 100644 --- a/esphome/components/datetime/date_entity.cpp +++ b/esphome/components/datetime/date_entity.cpp @@ -30,7 +30,7 @@ void DateEntity::publish_state() { return; } this->set_has_state(true); - ESP_LOGD(TAG, "'%s' >> %d-%d-%d", this->get_name().c_str(), this->year_, this->month_, this->day_); + ESP_LOGV(TAG, "'%s' >> %d-%d-%d", this->get_name().c_str(), this->year_, this->month_, this->day_); this->state_callback_.call(); #if defined(USE_DATETIME_DATE) && defined(USE_CONTROLLER_REGISTRY) ControllerRegistry::notify_date_update(this); @@ -83,16 +83,16 @@ void DateCall::validate_() { void DateCall::perform() { this->validate_(); - ESP_LOGD(TAG, "'%s' - Setting", this->parent_->get_name().c_str()); + ESP_LOGV(TAG, "'%s' - Setting", this->parent_->get_name().c_str()); if (this->year_.has_value()) { - ESP_LOGD(TAG, " Year: %d", *this->year_); + ESP_LOGV(TAG, " Year: %d", *this->year_); } if (this->month_.has_value()) { - ESP_LOGD(TAG, " Month: %d", *this->month_); + ESP_LOGV(TAG, " Month: %d", *this->month_); } if (this->day_.has_value()) { - ESP_LOGD(TAG, " Day: %d", *this->day_); + ESP_LOGV(TAG, " Day: %d", *this->day_); } this->parent_->control(*this); } diff --git a/esphome/components/datetime/datetime_entity.cpp b/esphome/components/datetime/datetime_entity.cpp index fa50271f04..a8e00d6eb3 100644 --- a/esphome/components/datetime/datetime_entity.cpp +++ b/esphome/components/datetime/datetime_entity.cpp @@ -45,7 +45,7 @@ void DateTimeEntity::publish_state() { return; } this->set_has_state(true); - ESP_LOGD(TAG, "'%s' >> %04u-%02u-%02u %02d:%02d:%02d", this->get_name().c_str(), this->year_, this->month_, + ESP_LOGV(TAG, "'%s' >> %04u-%02u-%02u %02d:%02d:%02d", this->get_name().c_str(), this->year_, this->month_, this->day_, this->hour_, this->minute_, this->second_); this->state_callback_.call(); #if defined(USE_DATETIME_DATETIME) && defined(USE_CONTROLLER_REGISTRY) @@ -127,25 +127,25 @@ void DateTimeCall::validate_() { void DateTimeCall::perform() { this->validate_(); - ESP_LOGD(TAG, "'%s' - Setting", this->parent_->get_name().c_str()); + ESP_LOGV(TAG, "'%s' - Setting", this->parent_->get_name().c_str()); if (this->year_.has_value()) { - ESP_LOGD(TAG, " Year: %d", *this->year_); + ESP_LOGV(TAG, " Year: %d", *this->year_); } if (this->month_.has_value()) { - ESP_LOGD(TAG, " Month: %d", *this->month_); + ESP_LOGV(TAG, " Month: %d", *this->month_); } if (this->day_.has_value()) { - ESP_LOGD(TAG, " Day: %d", *this->day_); + ESP_LOGV(TAG, " Day: %d", *this->day_); } if (this->hour_.has_value()) { - ESP_LOGD(TAG, " Hour: %d", *this->hour_); + ESP_LOGV(TAG, " Hour: %d", *this->hour_); } if (this->minute_.has_value()) { - ESP_LOGD(TAG, " Minute: %d", *this->minute_); + ESP_LOGV(TAG, " Minute: %d", *this->minute_); } if (this->second_.has_value()) { - ESP_LOGD(TAG, " Second: %d", *this->second_); + ESP_LOGV(TAG, " Second: %d", *this->second_); } this->parent_->control(*this); } diff --git a/esphome/components/datetime/time_entity.cpp b/esphome/components/datetime/time_entity.cpp index 74e43fbbe7..1cc9eaf2fb 100644 --- a/esphome/components/datetime/time_entity.cpp +++ b/esphome/components/datetime/time_entity.cpp @@ -26,7 +26,7 @@ void TimeEntity::publish_state() { return; } this->set_has_state(true); - ESP_LOGD(TAG, "'%s' >> %02d:%02d:%02d", this->get_name().c_str(), this->hour_, this->minute_, this->second_); + ESP_LOGV(TAG, "'%s' >> %02d:%02d:%02d", this->get_name().c_str(), this->hour_, this->minute_, this->second_); this->state_callback_.call(); #if defined(USE_DATETIME_TIME) && defined(USE_CONTROLLER_REGISTRY) ControllerRegistry::notify_time_update(this); @@ -52,15 +52,15 @@ void TimeCall::validate_() { void TimeCall::perform() { this->validate_(); - ESP_LOGD(TAG, "'%s' - Setting", this->parent_->get_name().c_str()); + ESP_LOGV(TAG, "'%s' - Setting", this->parent_->get_name().c_str()); if (this->hour_.has_value()) { - ESP_LOGD(TAG, " Hour: %d", *this->hour_); + ESP_LOGV(TAG, " Hour: %d", *this->hour_); } if (this->minute_.has_value()) { - ESP_LOGD(TAG, " Minute: %d", *this->minute_); + ESP_LOGV(TAG, " Minute: %d", *this->minute_); } if (this->second_.has_value()) { - ESP_LOGD(TAG, " Second: %d", *this->second_); + ESP_LOGV(TAG, " Second: %d", *this->second_); } this->parent_->control(*this); } diff --git a/esphome/components/event/event.cpp b/esphome/components/event/event.cpp index ec63fd9c3e..a5d64a2748 100644 --- a/esphome/components/event/event.cpp +++ b/esphome/components/event/event.cpp @@ -22,7 +22,7 @@ void Event::trigger(const std::string &event_type) { return; } this->last_event_type_ = found; - ESP_LOGD(TAG, "'%s' >> '%s'", this->get_name().c_str(), this->last_event_type_); + ESP_LOGV(TAG, "'%s' >> '%s'", this->get_name().c_str(), this->last_event_type_); this->event_callback_.call(StringRef(found)); #if defined(USE_EVENT) && defined(USE_CONTROLLER_REGISTRY) ControllerRegistry::notify_event(this); diff --git a/esphome/components/fan/fan.cpp b/esphome/components/fan/fan.cpp index 97336e17b5..dc7a75018c 100644 --- a/esphome/components/fan/fan.cpp +++ b/esphome/components/fan/fan.cpp @@ -44,22 +44,22 @@ FanCall &FanCall::set_preset_mode(const char *preset_mode, size_t len) { } void FanCall::perform() { - ESP_LOGD(TAG, "'%s' - Setting:", this->parent_.get_name().c_str()); + ESP_LOGV(TAG, "'%s' - Setting:", this->parent_.get_name().c_str()); this->validate_(); if (this->binary_state_.has_value()) { - ESP_LOGD(TAG, " State: %s", ONOFF(*this->binary_state_)); + ESP_LOGV(TAG, " State: %s", ONOFF(*this->binary_state_)); } if (this->oscillating_.has_value()) { - ESP_LOGD(TAG, " Oscillating: %s", YESNO(*this->oscillating_)); + ESP_LOGV(TAG, " Oscillating: %s", YESNO(*this->oscillating_)); } if (this->speed_.has_value()) { - ESP_LOGD(TAG, " Speed: %d", *this->speed_); + ESP_LOGV(TAG, " Speed: %d", *this->speed_); } if (this->direction_.has_value()) { - ESP_LOGD(TAG, " Direction: %s", LOG_STR_ARG(fan_direction_to_string(*this->direction_))); + ESP_LOGV(TAG, " Direction: %s", LOG_STR_ARG(fan_direction_to_string(*this->direction_))); } if (this->preset_mode_ != nullptr) { - ESP_LOGD(TAG, " Preset Mode: %s", this->preset_mode_); + ESP_LOGV(TAG, " Preset Mode: %s", this->preset_mode_); } this->parent_.control(*this); } @@ -196,21 +196,21 @@ void Fan::apply_preset_mode_(const FanCall &call) { void Fan::publish_state() { auto traits = this->get_traits(); - ESP_LOGD(TAG, + ESP_LOGV(TAG, "'%s' >>\n" " State: %s", this->name_.c_str(), ONOFF(this->state)); if (traits.supports_speed()) { - ESP_LOGD(TAG, " Speed: %d", this->speed); + ESP_LOGV(TAG, " Speed: %d", this->speed); } if (traits.supports_oscillation()) { - ESP_LOGD(TAG, " Oscillating: %s", YESNO(this->oscillating)); + ESP_LOGV(TAG, " Oscillating: %s", YESNO(this->oscillating)); } if (traits.supports_direction()) { - ESP_LOGD(TAG, " Direction: %s", LOG_STR_ARG(fan_direction_to_string(this->direction))); + ESP_LOGV(TAG, " Direction: %s", LOG_STR_ARG(fan_direction_to_string(this->direction))); } if (this->preset_mode_ != nullptr) { - ESP_LOGD(TAG, " Preset Mode: %s", this->preset_mode_); + ESP_LOGV(TAG, " Preset Mode: %s", this->preset_mode_); } this->state_callback_.call(); #if defined(USE_FAN) && defined(USE_CONTROLLER_REGISTRY) diff --git a/esphome/components/light/light_call.cpp b/esphome/components/light/light_call.cpp index 0b2d391fd6..41bd98de7b 100644 --- a/esphome/components/light/light_call.cpp +++ b/esphome/components/light/light_call.cpp @@ -62,9 +62,9 @@ static const LogString *color_mode_to_human(ColorMode color_mode) { } // Helper to log percentage values -#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE static void log_percent(const LogString *param, float value) { - ESP_LOGD(TAG, " %s: %.0f%%", LOG_STR_ARG(param), value * 100.0f); + ESP_LOGV(TAG, " %s: %.0f%%", LOG_STR_ARG(param), value * 100.0f); } #else #define log_percent(param, value) @@ -76,20 +76,20 @@ void LightCall::perform() { const bool publish = this->get_publish_(); if (publish) { - ESP_LOGD(TAG, "'%s' Setting:", name); + ESP_LOGV(TAG, "'%s' Setting:", name); // Only print color mode when it's being changed ColorMode current_color_mode = this->parent_->remote_values.get_color_mode(); ColorMode target_color_mode = this->has_color_mode() ? this->color_mode_ : current_color_mode; if (target_color_mode != current_color_mode) { - ESP_LOGD(TAG, " Color mode: %s", LOG_STR_ARG(color_mode_to_human(v.get_color_mode()))); + ESP_LOGV(TAG, " Color mode: %s", LOG_STR_ARG(color_mode_to_human(v.get_color_mode()))); } // Only print state when it's being changed bool current_state = this->parent_->remote_values.is_on(); bool target_state = this->has_state() ? this->state_ : current_state; if (target_state != current_state) { - ESP_LOGD(TAG, " State: %s", ONOFF(v.is_on())); + ESP_LOGV(TAG, " State: %s", ONOFF(v.is_on())); } if (this->has_brightness()) { @@ -100,7 +100,7 @@ void LightCall::perform() { 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, + ESP_LOGV(TAG, " Red: %.0f%%, Green: %.0f%%, Blue: %.0f%%", v.get_red() * 100.0f, v.get_green() * 100.0f, v.get_blue() * 100.0f); } @@ -108,11 +108,11 @@ void LightCall::perform() { log_percent(LOG_STR("White"), v.get_white()); } if (this->has_color_temperature()) { - ESP_LOGD(TAG, " Color temperature: %.1f mireds", v.get_color_temperature()); + ESP_LOGV(TAG, " Color temperature: %.1f mireds", v.get_color_temperature()); } if (this->has_cold_white() || this->has_warm_white()) { - ESP_LOGD(TAG, " Cold white: %.0f%%, warm white: %.0f%%", v.get_cold_white() * 100.0f, + ESP_LOGV(TAG, " Cold white: %.0f%%, warm white: %.0f%%", v.get_cold_white() * 100.0f, v.get_warm_white() * 100.0f); } } @@ -120,20 +120,20 @@ void LightCall::perform() { if (this->has_flash_()) { // FLASH if (publish) { - ESP_LOGD(TAG, " Flash length: %.1fs", this->flash_length_ / 1e3f); + ESP_LOGV(TAG, " Flash length: %.1fs", this->flash_length_ / 1e3f); } this->parent_->start_flash_(v, this->flash_length_, publish); } else if (this->has_transition_()) { // TRANSITION if (publish) { - ESP_LOGD(TAG, " Transition length: %.1fs", this->transition_length_ / 1e3f); + ESP_LOGV(TAG, " Transition length: %.1fs", this->transition_length_ / 1e3f); } // Special case: Transition and effect can be set when turning off if (this->has_effect_()) { if (publish) { - ESP_LOGD(TAG, " Effect: 'None'"); + ESP_LOGV(TAG, " Effect: 'None'"); } this->parent_->stop_effect_(); } @@ -150,7 +150,7 @@ void LightCall::perform() { } if (publish) { - ESP_LOGD(TAG, " Effect: '%.*s'", (int) effect_s.size(), effect_s.c_str()); + ESP_LOGV(TAG, " Effect: '%.*s'", (int) effect_s.size(), effect_s.c_str()); } this->parent_->start_effect_(this->effect_); diff --git a/esphome/components/lock/lock.cpp b/esphome/components/lock/lock.cpp index 4aa636e998..90937485b9 100644 --- a/esphome/components/lock/lock.cpp +++ b/esphome/components/lock/lock.cpp @@ -41,7 +41,7 @@ void Lock::publish_state(LockState state) { this->state = state; this->rtc_.save(&this->state); - ESP_LOGD(TAG, "'%s' >> %s", this->name_.c_str(), LOG_STR_ARG(lock_state_to_string(state))); + ESP_LOGV(TAG, "'%s' >> %s", this->name_.c_str(), LOG_STR_ARG(lock_state_to_string(state))); this->state_callback_.call(); #if defined(USE_LOCK) && defined(USE_CONTROLLER_REGISTRY) ControllerRegistry::notify_lock_update(this); @@ -49,10 +49,10 @@ void Lock::publish_state(LockState state) { } void LockCall::perform() { - ESP_LOGD(TAG, "'%s' - Setting", this->parent_->get_name().c_str()); + ESP_LOGV(TAG, "'%s' - Setting", this->parent_->get_name().c_str()); this->validate_(); if (this->state_.has_value()) { - ESP_LOGD(TAG, " State: %s", LOG_STR_ARG(lock_state_to_string(*this->state_))); + ESP_LOGV(TAG, " State: %s", LOG_STR_ARG(lock_state_to_string(*this->state_))); } this->parent_->control(*this); } diff --git a/esphome/components/media_player/media_player.cpp b/esphome/components/media_player/media_player.cpp index 70086089ff..a0eb7b5500 100644 --- a/esphome/components/media_player/media_player.cpp +++ b/esphome/components/media_player/media_player.cpp @@ -110,20 +110,20 @@ void MediaPlayerCall::validate_() { } void MediaPlayerCall::perform() { - ESP_LOGD(TAG, "'%s' - Setting", this->parent_->get_name().c_str()); + ESP_LOGV(TAG, "'%s' - Setting", this->parent_->get_name().c_str()); this->validate_(); if (this->command_.has_value()) { const char *command_s = media_player_command_to_string(this->command_.value()); - ESP_LOGD(TAG, " Command: %s", command_s); + ESP_LOGV(TAG, " Command: %s", command_s); } if (this->media_url_.has_value()) { - ESP_LOGD(TAG, " Media URL: %s", this->media_url_.value().c_str()); + ESP_LOGV(TAG, " Media URL: %s", this->media_url_.value().c_str()); } if (this->volume_.has_value()) { - ESP_LOGD(TAG, " Volume: %.2f", this->volume_.value()); + ESP_LOGV(TAG, " Volume: %.2f", this->volume_.value()); } if (this->announcement_.has_value()) { - ESP_LOGD(TAG, " Announcement: %s", this->announcement_.value() ? "yes" : "no"); + ESP_LOGV(TAG, " Announcement: %s", this->announcement_.value() ? "yes" : "no"); } this->parent_->control(*this); } diff --git a/esphome/components/number/number.cpp b/esphome/components/number/number.cpp index fb5d6e9f28..ca5aab6469 100644 --- a/esphome/components/number/number.cpp +++ b/esphome/components/number/number.cpp @@ -22,7 +22,7 @@ void log_number(const char *tag, const char *prefix, const char *type, Number *o void Number::publish_state(float state) { this->set_has_state(true); this->state = state; - ESP_LOGD(TAG, "'%s' >> %.2f", this->get_name().c_str(), state); + ESP_LOGV(TAG, "'%s' >> %.2f", this->get_name().c_str(), state); this->state_callback_.call(state); #if defined(USE_NUMBER) && defined(USE_CONTROLLER_REGISTRY) ControllerRegistry::notify_number_update(this); diff --git a/esphome/components/number/number_call.cpp b/esphome/components/number/number_call.cpp index aac9b2a23d..e300ca72de 100644 --- a/esphome/components/number/number_call.cpp +++ b/esphome/components/number/number_call.cpp @@ -61,7 +61,7 @@ void NumberCall::perform() { float max_value = traits.get_max_value(); if (this->operation_ == NUMBER_OP_SET) { - ESP_LOGD(TAG, "'%s': Setting value", name); + ESP_LOGV(TAG, "'%s': Setting value", name); if (!this->value_.has_value() || std::isnan(*this->value_)) { this->log_perform_warning_(LOG_STR("No value")); return; @@ -80,7 +80,7 @@ void NumberCall::perform() { target_value = max_value; } } else if (this->operation_ == NUMBER_OP_INCREMENT) { - ESP_LOGD(TAG, "'%s': Increment with%s cycling", name, this->cycle_ ? LOG_STR_LITERAL("") : LOG_STR_LITERAL("out")); + ESP_LOGV(TAG, "'%s': Increment with%s cycling", name, this->cycle_ ? LOG_STR_LITERAL("") : LOG_STR_LITERAL("out")); if (!parent->has_state()) { this->log_perform_warning_(LOG_STR("Can't increment, no state")); return; @@ -90,7 +90,7 @@ void NumberCall::perform() { if (target_value > max_value) target_value = this->cycle_or_clamp_(max_value, min_value); } else if (this->operation_ == NUMBER_OP_DECREMENT) { - ESP_LOGD(TAG, "'%s': Decrement with%s cycling", name, this->cycle_ ? LOG_STR_LITERAL("") : LOG_STR_LITERAL("out")); + ESP_LOGV(TAG, "'%s': Decrement with%s cycling", name, this->cycle_ ? LOG_STR_LITERAL("") : LOG_STR_LITERAL("out")); if (!parent->has_state()) { this->log_perform_warning_(LOG_STR("Can't decrement, no state")); return; @@ -110,7 +110,7 @@ void NumberCall::perform() { return; } - ESP_LOGD(TAG, " New value: %f", target_value); + ESP_LOGV(TAG, " New value: %f", target_value); this->parent_->control(target_value); } diff --git a/esphome/components/select/select.cpp b/esphome/components/select/select.cpp index df90c657e2..7c3dab15ad 100644 --- a/esphome/components/select/select.cpp +++ b/esphome/components/select/select.cpp @@ -31,7 +31,7 @@ void Select::publish_state(size_t index) { #pragma GCC diagnostic ignored "-Wdeprecated-declarations" this->state = option; // Update deprecated member for backward compatibility #pragma GCC diagnostic pop - ESP_LOGD(TAG, "'%s' >> %s (%zu)", this->get_name().c_str(), option, index); + ESP_LOGV(TAG, "'%s' >> %s (%zu)", this->get_name().c_str(), option, index); this->state_callback_.call(index); #if defined(USE_SELECT) && defined(USE_CONTROLLER_REGISTRY) ControllerRegistry::notify_select_update(this); diff --git a/esphome/components/select/select_call.cpp b/esphome/components/select/select_call.cpp index 83f5052fc8..0e14371d00 100644 --- a/esphome/components/select/select_call.cpp +++ b/esphome/components/select/select_call.cpp @@ -64,7 +64,7 @@ optional SelectCall::calculate_target_index_(const char *name) { } if (this->operation_ == SELECT_OP_SET) { - ESP_LOGD(TAG, "'%s' - Setting", name); + ESP_LOGV(TAG, "'%s' - Setting", name); if (!this->index_.has_value()) { ESP_LOGW(TAG, "'%s' - No option set", name); return nullopt; @@ -73,7 +73,7 @@ optional SelectCall::calculate_target_index_(const char *name) { } // SELECT_OP_NEXT or SELECT_OP_PREVIOUS - ESP_LOGD(TAG, "'%s' - Selecting %s, with%s cycling", name, + ESP_LOGV(TAG, "'%s' - Selecting %s, with%s cycling", name, this->operation_ == SELECT_OP_NEXT ? LOG_STR_LITERAL("next") : LOG_STR_LITERAL("previous"), this->cycle_ ? LOG_STR_LITERAL("") : LOG_STR_LITERAL("out")); diff --git a/esphome/components/sensor/sensor.cpp b/esphome/components/sensor/sensor.cpp index aad7f86dcf..59e011932b 100644 --- a/esphome/components/sensor/sensor.cpp +++ b/esphome/components/sensor/sensor.cpp @@ -122,7 +122,7 @@ void Sensor::clear_filters() { void Sensor::internal_send_state_to_frontend(float state) { this->set_has_state(true); this->state = state; - ESP_LOGD(TAG, "'%s' >> %.*f %s", this->get_name().c_str(), std::max(0, (int) this->get_accuracy_decimals()), state, + ESP_LOGV(TAG, "'%s' >> %.*f %s", this->get_name().c_str(), std::max(0, (int) this->get_accuracy_decimals()), state, this->get_unit_of_measurement_ref().c_str()); this->callback_.call(state); #if defined(USE_SENSOR) && defined(USE_CONTROLLER_REGISTRY) diff --git a/esphome/components/switch/switch.cpp b/esphome/components/switch/switch.cpp index df762addbb..11840db3a3 100644 --- a/esphome/components/switch/switch.cpp +++ b/esphome/components/switch/switch.cpp @@ -61,7 +61,7 @@ void Switch::publish_state(bool state) { if (restore_mode & RESTORE_MODE_PERSISTENT_MASK) this->rtc_.save(&this->state); - ESP_LOGD(TAG, "'%s' >> %s", this->name_.c_str(), ONOFF(this->state)); + ESP_LOGV(TAG, "'%s' >> %s", this->name_.c_str(), ONOFF(this->state)); this->state_callback_.call(this->state); #if defined(USE_SWITCH) && defined(USE_CONTROLLER_REGISTRY) ControllerRegistry::notify_switch_update(this); diff --git a/esphome/components/text/text.cpp b/esphome/components/text/text.cpp index 12abc5d939..032ea468e6 100644 --- a/esphome/components/text/text.cpp +++ b/esphome/components/text/text.cpp @@ -19,9 +19,9 @@ void Text::publish_state(const char *state, size_t len) { this->state.assign(state, len); } if (this->traits.get_mode() == TEXT_MODE_PASSWORD) { - ESP_LOGD(TAG, "'%s' >> " LOG_SECRET("'%s'"), this->get_name().c_str(), this->state.c_str()); + ESP_LOGV(TAG, "'%s' >> " LOG_SECRET("'%s'"), this->get_name().c_str(), this->state.c_str()); } else { - ESP_LOGD(TAG, "'%s' >> '%s'", this->get_name().c_str(), this->state.c_str()); + ESP_LOGV(TAG, "'%s' >> '%s'", this->get_name().c_str(), this->state.c_str()); } this->state_callback_.call(this->state); #if defined(USE_TEXT) && defined(USE_CONTROLLER_REGISTRY) diff --git a/esphome/components/text/text_call.cpp b/esphome/components/text/text_call.cpp index b7aed098c7..b7692658af 100644 --- a/esphome/components/text/text_call.cpp +++ b/esphome/components/text/text_call.cpp @@ -48,10 +48,10 @@ void TextCall::perform() { std::string target_value = this->value_.value(); if (this->parent_->traits.get_mode() == TEXT_MODE_PASSWORD) { - ESP_LOGD(TAG, "'%s' - Setting password value: " LOG_SECRET("'%s'"), this->parent_->get_name().c_str(), + ESP_LOGV(TAG, "'%s' - Setting password value: " LOG_SECRET("'%s'"), this->parent_->get_name().c_str(), target_value.c_str()); } else { - ESP_LOGD(TAG, "'%s' - Setting text value: %s", this->parent_->get_name().c_str(), target_value.c_str()); + ESP_LOGV(TAG, "'%s' - Setting text value: %s", this->parent_->get_name().c_str(), target_value.c_str()); } this->parent_->control(target_value); } diff --git a/esphome/components/text_sensor/text_sensor.cpp b/esphome/components/text_sensor/text_sensor.cpp index 0dc29f9a94..31543117b8 100644 --- a/esphome/components/text_sensor/text_sensor.cpp +++ b/esphome/components/text_sensor/text_sensor.cpp @@ -112,7 +112,7 @@ void TextSensor::internal_send_state_to_frontend(const char *state, size_t len) void TextSensor::notify_frontend_() { this->set_has_state(true); - ESP_LOGD(TAG, "'%s' >> '%s'", this->name_.c_str(), this->state.c_str()); + ESP_LOGV(TAG, "'%s' >> '%s'", this->name_.c_str(), this->state.c_str()); this->callback_.call(this->state); #if defined(USE_TEXT_SENSOR) && defined(USE_CONTROLLER_REGISTRY) ControllerRegistry::notify_text_sensor_update(this); diff --git a/esphome/components/update/update_entity.cpp b/esphome/components/update/update_entity.cpp index 7edea2fe22..1a5a55577f 100644 --- a/esphome/components/update/update_entity.cpp +++ b/esphome/components/update/update_entity.cpp @@ -18,28 +18,28 @@ const LogString *update_state_to_string(UpdateState state) { } void UpdateEntity::publish_state() { - ESP_LOGD(TAG, + ESP_LOGV(TAG, "'%s' >>\n" " Current Version: %s", this->name_.c_str(), this->update_info_.current_version.c_str()); if (!this->update_info_.md5.empty()) { - ESP_LOGD(TAG, " Latest Version: %s", this->update_info_.latest_version.c_str()); + ESP_LOGV(TAG, " Latest Version: %s", this->update_info_.latest_version.c_str()); } if (!this->update_info_.firmware_url.empty()) { - ESP_LOGD(TAG, " Firmware URL: %s", this->update_info_.firmware_url.c_str()); + ESP_LOGV(TAG, " Firmware URL: %s", this->update_info_.firmware_url.c_str()); } - ESP_LOGD(TAG, " Title: %s", this->update_info_.title.c_str()); + ESP_LOGV(TAG, " Title: %s", this->update_info_.title.c_str()); if (!this->update_info_.summary.empty()) { - ESP_LOGD(TAG, " Summary: %s", this->update_info_.summary.c_str()); + ESP_LOGV(TAG, " Summary: %s", this->update_info_.summary.c_str()); } if (!this->update_info_.release_url.empty()) { - ESP_LOGD(TAG, " Release URL: %s", this->update_info_.release_url.c_str()); + ESP_LOGV(TAG, " Release URL: %s", this->update_info_.release_url.c_str()); } if (this->update_info_.has_progress) { - ESP_LOGD(TAG, " Progress: %.0f%%", this->update_info_.progress); + ESP_LOGV(TAG, " Progress: %.0f%%", this->update_info_.progress); } this->set_has_state(true); diff --git a/esphome/components/valve/valve.cpp b/esphome/components/valve/valve.cpp index 636da1f3c3..9e1ef9da50 100644 --- a/esphome/components/valve/valve.cpp +++ b/esphome/components/valve/valve.cpp @@ -68,21 +68,21 @@ ValveCall &ValveCall::set_position(float position) { return *this; } void ValveCall::perform() { - ESP_LOGD(TAG, "'%s' - Setting", this->parent_->get_name().c_str()); + ESP_LOGV(TAG, "'%s' - Setting", this->parent_->get_name().c_str()); auto traits = this->parent_->get_traits(); this->validate_(); if (this->stop_) { - ESP_LOGD(TAG, " Command: STOP"); + ESP_LOGV(TAG, " Command: STOP"); } if (this->position_.has_value()) { if (traits.get_supports_position()) { - ESP_LOGD(TAG, " Position: %.0f%%", *this->position_ * 100.0f); + ESP_LOGV(TAG, " Position: %.0f%%", *this->position_ * 100.0f); } else { - ESP_LOGD(TAG, " Command: %s", LOG_STR_ARG(valve_command_to_str(*this->position_))); + ESP_LOGV(TAG, " Command: %s", LOG_STR_ARG(valve_command_to_str(*this->position_))); } } if (this->toggle_.has_value()) { - ESP_LOGD(TAG, " Command: TOGGLE"); + ESP_LOGV(TAG, " Command: TOGGLE"); } this->parent_->control(*this); } @@ -128,20 +128,20 @@ ValveCall Valve::make_call() { return {this}; } void Valve::publish_state(bool save) { this->position = clamp(this->position, 0.0f, 1.0f); - ESP_LOGD(TAG, "'%s' >>", this->name_.c_str()); + ESP_LOGV(TAG, "'%s' >>", this->name_.c_str()); auto traits = this->get_traits(); if (traits.get_supports_position()) { - ESP_LOGD(TAG, " Position: %.0f%%", this->position * 100.0f); + ESP_LOGV(TAG, " Position: %.0f%%", this->position * 100.0f); } else { if (this->position == VALVE_OPEN) { - ESP_LOGD(TAG, " State: OPEN"); + ESP_LOGV(TAG, " State: OPEN"); } else if (this->position == VALVE_CLOSED) { - ESP_LOGD(TAG, " State: CLOSED"); + ESP_LOGV(TAG, " State: CLOSED"); } else { - ESP_LOGD(TAG, " State: UNKNOWN"); + ESP_LOGV(TAG, " State: UNKNOWN"); } } - ESP_LOGD(TAG, " Current Operation: %s", LOG_STR_ARG(valve_operation_to_str(this->current_operation))); + ESP_LOGV(TAG, " Current Operation: %s", LOG_STR_ARG(valve_operation_to_str(this->current_operation))); this->state_callback_.call(); #if defined(USE_VALVE) && defined(USE_CONTROLLER_REGISTRY) diff --git a/esphome/components/water_heater/water_heater.cpp b/esphome/components/water_heater/water_heater.cpp index 3989230d2d..9a74877f0a 100644 --- a/esphome/components/water_heater/water_heater.cpp +++ b/esphome/components/water_heater/water_heater.cpp @@ -83,25 +83,25 @@ WaterHeaterCall &WaterHeaterCall::set_on(bool on) { } void WaterHeaterCall::perform() { - ESP_LOGD(TAG, "'%s' - Setting", this->parent_->get_name().c_str()); + ESP_LOGV(TAG, "'%s' - Setting", this->parent_->get_name().c_str()); this->validate_(); if (this->mode_.has_value()) { - ESP_LOGD(TAG, " Mode: %s", LOG_STR_ARG(water_heater_mode_to_string(*this->mode_))); + ESP_LOGV(TAG, " Mode: %s", LOG_STR_ARG(water_heater_mode_to_string(*this->mode_))); } if (!std::isnan(this->target_temperature_)) { - ESP_LOGD(TAG, " Target Temperature: %.2f", this->target_temperature_); + ESP_LOGV(TAG, " Target Temperature: %.2f", this->target_temperature_); } if (!std::isnan(this->target_temperature_low_)) { - ESP_LOGD(TAG, " Target Temperature Low: %.2f", this->target_temperature_low_); + ESP_LOGV(TAG, " Target Temperature Low: %.2f", this->target_temperature_low_); } if (!std::isnan(this->target_temperature_high_)) { - ESP_LOGD(TAG, " Target Temperature High: %.2f", this->target_temperature_high_); + ESP_LOGV(TAG, " Target Temperature High: %.2f", this->target_temperature_high_); } if (this->state_mask_ & WATER_HEATER_STATE_AWAY) { - ESP_LOGD(TAG, " Away: %s", (this->state_ & WATER_HEATER_STATE_AWAY) ? "YES" : "NO"); + ESP_LOGV(TAG, " Away: %s", (this->state_ & WATER_HEATER_STATE_AWAY) ? "YES" : "NO"); } if (this->state_mask_ & WATER_HEATER_STATE_ON) { - ESP_LOGD(TAG, " On: %s", (this->state_ & WATER_HEATER_STATE_ON) ? "YES" : "NO"); + ESP_LOGV(TAG, " On: %s", (this->state_ & WATER_HEATER_STATE_ON) ? "YES" : "NO"); } this->parent_->control(*this); } @@ -158,24 +158,24 @@ void WaterHeaterCall::validate_() { void WaterHeater::publish_state() { auto traits = this->get_traits(); - ESP_LOGD(TAG, + ESP_LOGV(TAG, "'%s' >>\n" " Mode: %s", this->name_.c_str(), LOG_STR_ARG(water_heater_mode_to_string(this->mode_))); if (!std::isnan(this->current_temperature_)) { - ESP_LOGD(TAG, " Current Temperature: %.2f°C", this->current_temperature_); + ESP_LOGV(TAG, " Current Temperature: %.2f°C", this->current_temperature_); } if (traits.get_supports_two_point_target_temperature()) { - ESP_LOGD(TAG, " Target Temperature: Low: %.2f°C High: %.2f°C", this->target_temperature_low_, + ESP_LOGV(TAG, " Target Temperature: Low: %.2f°C High: %.2f°C", this->target_temperature_low_, this->target_temperature_high_); } else if (!std::isnan(this->target_temperature_)) { - ESP_LOGD(TAG, " Target Temperature: %.2f°C", this->target_temperature_); + ESP_LOGV(TAG, " Target Temperature: %.2f°C", this->target_temperature_); } if (this->state_ & WATER_HEATER_STATE_AWAY) { - ESP_LOGD(TAG, " Away: YES"); + ESP_LOGV(TAG, " Away: YES"); } if (traits.has_feature_flags(WATER_HEATER_SUPPORTS_ON_OFF)) { - ESP_LOGD(TAG, " On: %s", (this->state_ & WATER_HEATER_STATE_ON) ? "YES" : "NO"); + ESP_LOGV(TAG, " On: %s", (this->state_ & WATER_HEATER_STATE_ON) ? "YES" : "NO"); } #if defined(USE_WATER_HEATER) && defined(USE_CONTROLLER_REGISTRY) From a075f63b59c68dd6e627c505397c1cdf5a052b45 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 25 Mar 2026 16:50:37 -0400 Subject: [PATCH 163/166] [uart] Fix debug callback missing peeked byte and reading past end (#15169) --- esphome/components/uart/uart_component_esp_idf.cpp | 6 ++++-- esphome/components/uart/uart_component_host.cpp | 6 ++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/esphome/components/uart/uart_component_esp_idf.cpp b/esphome/components/uart/uart_component_esp_idf.cpp index bd2f915d3a..cd77cd1189 100644 --- a/esphome/components/uart/uart_component_esp_idf.cpp +++ b/esphome/components/uart/uart_component_esp_idf.cpp @@ -324,6 +324,9 @@ bool IDFUARTComponent::peek_byte(uint8_t *data) { } bool IDFUARTComponent::read_array(uint8_t *data, size_t len) { + if (len == 0) { + return false; + } size_t length_to_read = len; int32_t read_len = 0; if (!this->check_read_timeout_(len)) @@ -331,11 +334,10 @@ bool IDFUARTComponent::read_array(uint8_t *data, size_t len) { if (this->has_peek_) { length_to_read--; *data = this->peek_byte_; - data++; this->has_peek_ = false; } if (length_to_read > 0) - read_len = uart_read_bytes(this->uart_num_, data, length_to_read, 20 / portTICK_PERIOD_MS); + read_len = uart_read_bytes(this->uart_num_, data + (len - length_to_read), length_to_read, 20 / portTICK_PERIOD_MS); #ifdef USE_UART_DEBUGGER for (size_t i = 0; i < len; i++) { this->debug_callback_.call(UART_DIRECTION_RX, data[i]); diff --git a/esphome/components/uart/uart_component_host.cpp b/esphome/components/uart/uart_component_host.cpp index 0042ffae23..085610a983 100644 --- a/esphome/components/uart/uart_component_host.cpp +++ b/esphome/components/uart/uart_component_host.cpp @@ -235,16 +235,14 @@ bool HostUartComponent::read_array(uint8_t *data, size_t len) { } if (!this->check_read_timeout_(len)) return false; - uint8_t *data_ptr = data; size_t length_to_read = len; if (this->has_peek_) { length_to_read--; - *data_ptr = this->peek_byte_; - data_ptr++; + *data = this->peek_byte_; this->has_peek_ = false; } if (length_to_read > 0) { - int sz = ::read(this->file_descriptor_, data_ptr, length_to_read); + int sz = ::read(this->file_descriptor_, data + (len - length_to_read), length_to_read); if (sz == -1) { this->update_error_(strerror(errno)); return false; From 29e263ad7d42fc90d54e809e41709f03d2e7f006 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 25 Mar 2026 13:43:01 -1000 Subject: [PATCH 164/166] [esp32] Wrap vfprintf to fix printf stub on picolibc (IDF 6) (#15172) --- esphome/components/esp32/__init__.py | 2 +- esphome/components/esp32/printf_stubs.cpp | 17 +++++++++++------ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 0e216485ac..91eb913e3d 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -1587,7 +1587,7 @@ async def to_code(config): if conf[CONF_ADVANCED][CONF_ENABLE_FULL_PRINTF]: cg.add_define("USE_FULL_PRINTF") else: - for symbol in ("vprintf", "printf", "fprintf"): + for symbol in ("vprintf", "printf", "fprintf", "vfprintf"): cg.add_build_flag(f"-Wl,--wrap={symbol}") else: cg.add_build_flag("-DUSE_ARDUINO") diff --git a/esphome/components/esp32/printf_stubs.cpp b/esphome/components/esp32/printf_stubs.cpp index c6f03bc363..386fbbd79d 100644 --- a/esphome/components/esp32/printf_stubs.cpp +++ b/esphome/components/esp32/printf_stubs.cpp @@ -2,10 +2,11 @@ * Linker wrap stubs for FILE*-based printf functions. * * ESP-IDF SDK components (gpio driver, ringbuf, log_write) reference - * fprintf(), printf(), and vprintf() which pull in newlib's _vfprintf_r - * (~11 KB). This is a separate implementation from _svfprintf_r (used by - * snprintf/vsnprintf) that handles FILE* stream I/O with buffering and - * locking. + * fprintf(), printf(), vprintf(), and vfprintf() which pull in the full + * printf implementation (~11 KB on newlib's _vfprintf_r, ~2.8 KB on + * picolibc's vfprintf). This is a separate implementation from the one + * used by snprintf/vsnprintf that handles FILE* stream I/O with buffering + * and locking. * * ESPHome replaces the ESP-IDF log handler via esp_log_set_vprintf_(), * so the SDK's vprintf() path is dead code at runtime. The fprintf() @@ -70,11 +71,15 @@ int __wrap_printf(const char *fmt, ...) { return len; } +int __wrap_vfprintf(FILE *stream, const char *fmt, va_list ap) { + char buf[PRINTF_BUFFER_SIZE]; + return write_printf_buffer(stream, buf, vsnprintf(buf, sizeof(buf), fmt, ap)); +} + int __wrap_fprintf(FILE *stream, const char *fmt, ...) { va_list ap; va_start(ap, fmt); - char buf[PRINTF_BUFFER_SIZE]; - int len = write_printf_buffer(stream, buf, vsnprintf(buf, sizeof(buf), fmt, ap)); + int len = __wrap_vfprintf(stream, fmt, ap); va_end(ap); return len; } From 676ac9d8b876044b0278f48fbd70100cf8594141 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Wed, 25 Mar 2026 21:30:46 -0500 Subject: [PATCH 165/166] [infrared][ir_rf_proxy] Add `receiver_frequency` config for IR receiver demodulation frequency (#15156) Co-authored-by: J. Nick Koston --- esphome/components/api/api.proto | 1 + esphome/components/api/api_connection.cpp | 1 + esphome/components/api/api_pb2.cpp | 2 ++ esphome/components/api/api_pb2.h | 3 ++- esphome/components/api/api_pb2_dump.cpp | 1 + esphome/components/const/__init__.py | 1 + esphome/components/infrared/infrared.h | 4 ++++ esphome/components/ir_rf_proxy/infrared.py | 15 ++++++++++++++- esphome/components/ir_rf_proxy/ir_rf_proxy.h | 3 +++ tests/components/ir_rf_proxy/common-rx.yaml | 1 + 10 files changed, 30 insertions(+), 2 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 86daa9a2bf..96ee2fb920 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -2512,6 +2512,7 @@ message ListEntitiesInfraredResponse { EntityCategory entity_category = 6; uint32 device_id = 7 [(field_ifdef) = "USE_DEVICES"]; uint32 capabilities = 8; // Bitfield of InfraredCapabilityFlags + uint32 receiver_frequency = 9; // Demodulation frequency of the IR receiver in Hz (0 = unspecified) } // Command to transmit infrared/RF data using raw timings diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index d023cd21a8..0a99adcacf 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1549,6 +1549,7 @@ uint16_t APIConnection::try_send_infrared_info(EntityBase *entity, APIConnection auto *infrared = static_cast(entity); ListEntitiesInfraredResponse msg; msg.capabilities = infrared->get_capability_flags(); + msg.receiver_frequency = infrared->get_traits().get_receiver_frequency_hz(); return fill_and_encode_entity_info(infrared, msg, conn, remaining_size); } #endif diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index f77f4df545..ae2cd2bae8 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -3657,6 +3657,7 @@ void ListEntitiesInfraredResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(7, this->device_id); #endif buffer.encode_uint32(8, this->capabilities); + buffer.encode_uint32(9, this->receiver_frequency); } uint32_t ListEntitiesInfraredResponse::calculate_size() const { uint32_t size = 0; @@ -3672,6 +3673,7 @@ uint32_t ListEntitiesInfraredResponse::calculate_size() const { size += ProtoSize::calc_uint32(1, this->device_id); #endif size += ProtoSize::calc_uint32(1, this->capabilities); + size += ProtoSize::calc_uint32(1, this->receiver_frequency); return size; } #endif diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 16586e6e9a..14f6c704ae 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -3041,11 +3041,12 @@ class ZWaveProxyRequest final : public ProtoDecodableMessage { class ListEntitiesInfraredResponse final : public InfoResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 135; - static constexpr uint8_t ESTIMATED_SIZE = 44; + static constexpr uint8_t ESTIMATED_SIZE = 48; #ifdef HAS_PROTO_MESSAGE_DUMP const LogString *message_name() const override { return LOG_STR("list_entities_infrared_response"); } #endif uint32_t capabilities{0}; + uint32_t receiver_frequency{0}; void encode(ProtoWriteBuffer &buffer) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP diff --git a/esphome/components/api/api_pb2_dump.cpp b/esphome/components/api/api_pb2_dump.cpp index a11f3b231e..640c347371 100644 --- a/esphome/components/api/api_pb2_dump.cpp +++ b/esphome/components/api/api_pb2_dump.cpp @@ -2572,6 +2572,7 @@ const char *ListEntitiesInfraredResponse::dump_to(DumpBuffer &out) const { dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif dump_field(out, ESPHOME_PSTR("capabilities"), this->capabilities); + dump_field(out, ESPHOME_PSTR("receiver_frequency"), this->receiver_frequency); return out.c_str(); } #endif diff --git a/esphome/components/const/__init__.py b/esphome/components/const/__init__.py index 1fbf88c276..0eb37e3029 100644 --- a/esphome/components/const/__init__.py +++ b/esphome/components/const/__init__.py @@ -18,6 +18,7 @@ CONF_ON_PACKET = "on_packet" CONF_ON_RECEIVE = "on_receive" CONF_ON_STATE_CHANGE = "on_state_change" CONF_PARITY = "parity" +CONF_RECEIVER_FREQUENCY = "receiver_frequency" CONF_REQUEST_HEADERS = "request_headers" CONF_ROWS = "rows" CONF_STOP_BITS = "stop_bits" diff --git a/esphome/components/infrared/infrared.h b/esphome/components/infrared/infrared.h index 59535f499a..6d91c97cce 100644 --- a/esphome/components/infrared/infrared.h +++ b/esphome/components/infrared/infrared.h @@ -101,9 +101,13 @@ class InfraredTraits { bool get_supports_receiver() const { return this->supports_receiver_; } void set_supports_receiver(bool supports) { this->supports_receiver_ = supports; } + uint32_t get_receiver_frequency_hz() const { return this->receiver_frequency_hz_; } + void set_receiver_frequency_hz(uint32_t freq) { this->receiver_frequency_hz_ = freq; } + protected: bool supports_transmitter_{false}; bool supports_receiver_{false}; + uint32_t receiver_frequency_hz_{0}; // Demodulation frequency of the IR receiver in Hz (0 = unspecified) }; /// Infrared - Base class for infrared remote control implementations diff --git a/esphome/components/ir_rf_proxy/infrared.py b/esphome/components/ir_rf_proxy/infrared.py index 4a4d9fa860..3218889721 100644 --- a/esphome/components/ir_rf_proxy/infrared.py +++ b/esphome/components/ir_rf_proxy/infrared.py @@ -4,6 +4,7 @@ from typing import Any import esphome.codegen as cg from esphome.components import infrared, remote_receiver, remote_transmitter +from esphome.components.const import CONF_RECEIVER_FREQUENCY import esphome.config_validation as cv from esphome.const import CONF_CARRIER_DUTY_PERCENT, CONF_FREQUENCY import esphome.final_validate as fv @@ -19,6 +20,7 @@ CONFIG_SCHEMA = cv.All( infrared.infrared_schema(IrRfProxy).extend( { cv.Optional(CONF_FREQUENCY, default=0): cv.frequency, + cv.Optional(CONF_RECEIVER_FREQUENCY): cv.frequency, cv.Optional(CONF_REMOTE_RECEIVER_ID): cv.use_id( remote_receiver.RemoteReceiverComponent ), @@ -33,7 +35,14 @@ CONFIG_SCHEMA = cv.All( def _final_validate(config: dict[str, Any]) -> None: """Validate that transmitters have a proper carrier duty cycle.""" - # Only validate if this is an infrared (not RF) configuration with a transmitter + # receiver_frequency is only meaningful for receiver configurations + if CONF_RECEIVER_FREQUENCY in config and CONF_REMOTE_RECEIVER_ID not in config: + raise cv.Invalid( + f"'{CONF_RECEIVER_FREQUENCY}' can only be used with '{CONF_REMOTE_RECEIVER_ID}', " + "not with a transmitter" + ) + + # Only validate duty cycle if this is an infrared (not RF) configuration with a transmitter if config.get(CONF_FREQUENCY, 0) != 0 or CONF_REMOTE_TRANSMITTER_ID not in config: return @@ -75,3 +84,7 @@ async def to_code(config: dict[str, Any]) -> None: if CONF_REMOTE_RECEIVER_ID in config: receiver = await cg.get_variable(config[CONF_REMOTE_RECEIVER_ID]) cg.add(var.set_receiver(receiver)) + + # Set receiver demodulation frequency if specified (metadata only, no hardware effect) + if CONF_RECEIVER_FREQUENCY in config: + cg.add(var.set_receiver_frequency(config[CONF_RECEIVER_FREQUENCY])) diff --git a/esphome/components/ir_rf_proxy/ir_rf_proxy.h b/esphome/components/ir_rf_proxy/ir_rf_proxy.h index f067a6e17a..05b988f287 100644 --- a/esphome/components/ir_rf_proxy/ir_rf_proxy.h +++ b/esphome/components/ir_rf_proxy/ir_rf_proxy.h @@ -22,6 +22,9 @@ class IrRfProxy : public infrared::Infrared { /// Check if this is RF mode (non-zero frequency) bool is_rf() const { return this->frequency_khz_ > 0; } + /// Set the receiver's hardware demodulation frequency in Hz (metadata only, does not affect hardware) + void set_receiver_frequency(uint32_t frequency_hz) { this->get_traits().set_receiver_frequency_hz(frequency_hz); } + protected: // RF frequency in kHz (Hz / 1000); 0 = infrared, non-zero = RF uint32_t frequency_khz_{0}; diff --git a/tests/components/ir_rf_proxy/common-rx.yaml b/tests/components/ir_rf_proxy/common-rx.yaml index 0f758f832d..37033a128e 100644 --- a/tests/components/ir_rf_proxy/common-rx.yaml +++ b/tests/components/ir_rf_proxy/common-rx.yaml @@ -8,6 +8,7 @@ infrared: - platform: ir_rf_proxy id: ir_rx name: "IR Receiver" + receiver_frequency: 38kHz remote_receiver_id: ir_receiver # RF 900MHz receiver From 8a6b009173a8373df76b3a4d07040c8852162b8e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 25 Mar 2026 16:53:33 -1000 Subject: [PATCH 166/166] [light] Move normal state logging to VERBOSE (#15177) --- esphome/components/light/light_call.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/light/light_call.cpp b/esphome/components/light/light_call.cpp index 41bd98de7b..7c936b51b7 100644 --- a/esphome/components/light/light_call.cpp +++ b/esphome/components/light/light_call.cpp @@ -385,7 +385,7 @@ void LightCall::transform_parameters_() { !(this->color_mode_ & ColorCapability::WHITE) && // !(this->color_mode_ & ColorCapability::COLOR_TEMPERATURE) && // min_mireds > 0.0f && max_mireds > 0.0f) { - ESP_LOGD(TAG, "'%s': setting cold/warm white channels using white/color temperature values", + ESP_LOGV(TAG, "'%s': setting cold/warm white channels using white/color temperature values", this->parent_->get_name().c_str()); // Only compute cold_white/warm_white from color_temperature if they're not already explicitly set. // This is important for state restoration, where both color_temperature and cold_white/warm_white @@ -432,7 +432,7 @@ ColorMode LightCall::compute_color_mode_() { // Don't change if the current mode is in the intersection (suitable AND supported) if (ColorModeMask::mask_contains(intersection, current_mode)) { - ESP_LOGI(TAG, "'%s': color mode not specified; retaining %s", this->parent_->get_name().c_str(), + ESP_LOGV(TAG, "'%s': color mode not specified; retaining %s", this->parent_->get_name().c_str(), LOG_STR_ARG(color_mode_to_human(current_mode))); return current_mode; } @@ -440,7 +440,7 @@ ColorMode LightCall::compute_color_mode_() { // Use the preferred suitable mode. if (intersection != 0) { ColorMode mode = ColorModeMask::first_value_from_mask(intersection); - ESP_LOGI(TAG, "'%s': color mode not specified; using %s", this->parent_->get_name().c_str(), + ESP_LOGV(TAG, "'%s': color mode not specified; using %s", this->parent_->get_name().c_str(), LOG_STR_ARG(color_mode_to_human(mode))); return mode; }