From b4b98505baed1fea37c1e2e5da11c2c8ea7d2e26 Mon Sep 17 00:00:00 2001 From: James <23900@qq.com> Date: Mon, 24 Nov 2025 23:05:02 +1300 Subject: [PATCH 01/28] [mipi_dsi] add guition JC4880P443 display (#12068) --- esphome/components/mipi_dsi/models/guition.py | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/esphome/components/mipi_dsi/models/guition.py b/esphome/components/mipi_dsi/models/guition.py index 5f7db4ebdac..cd566633f9d 100644 --- a/esphome/components/mipi_dsi/models/guition.py +++ b/esphome/components/mipi_dsi/models/guition.py @@ -35,3 +35,70 @@ DriverChip( (0x10, 0x0C), (0x11, 0x0C), (0x12, 0x0C), (0x13, 0x0C), (0x30, 0x00), ], ) + + +# JC4880P443 Driver Configuration (ST7701) +# Using parameters from esp_lcd_st7701.h and the working full init sequence +# ---------------------------------------------------------------------------------------------------------------------- +# * Resolution: 480x800 +# * PCLK Frequency: 34 MHz +# * DSI Lane Bit Rate: 500 Mbps (using 2-Lane DSI configuration) +# * Horizontal Timing (hsync_pulse_width=12, hsync_back_porch=42, hsync_front_porch=42) +# * Vertical Timing (vsync_pulse_width=2, vsync_back_porch=8, vsync_front_porch=166) +# ---------------------------------------------------------------------------------------------------------------------- +DriverChip( + "JC4880P443", + width=480, + height=800, + hsync_back_porch=42, + hsync_pulse_width=12, + hsync_front_porch=42, + vsync_back_porch=8, + vsync_pulse_width=2, + vsync_front_porch=166, + pclk_frequency="34MHz", + lane_bit_rate="500Mbps", + swap_xy=cv.UNDEFINED, + color_order="RGB", + reset_pin=5, + initsequence=[ + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x13), + (0xEF, 0x08), + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x10), + (0xC0, 0x63, 0x00), + (0xC1, 0x0D, 0x02), + (0xC2, 0x10, 0x08), + (0xCC, 0x10), + (0xB0, 0x80, 0x09, 0x53, 0x0C, 0xD0, 0x07, 0x0C, 0x09, 0x09, 0x28, 0x06, 0xD4, 0x13, 0x69, 0x2B, 0x71), + (0xB1, 0x80, 0x94, 0x5A, 0x10, 0xD3, 0x06, 0x0A, 0x08, 0x08, 0x25, 0x03, 0xD3, 0x12, 0x66, 0x6A, 0x0D), + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x11), + (0xB0, 0x5D), + (0xB1, 0x58), + (0xB2, 0x87), + (0xB3, 0x80), + (0xB5, 0x4E), + (0xB7, 0x85), + (0xB8, 0x21), + (0xB9, 0x10, 0x1F), + (0xBB, 0x03), + (0xBC, 0x00), + (0xC1, 0x78), + (0xC2, 0x78), + (0xD0, 0x88), + (0xE0, 0x00, 0x3A, 0x02), + (0xE1, 0x04, 0xA0, 0x00, 0xA0, 0x05, 0xA0, 0x00, 0xA0, 0x00, 0x40, 0x40), + (0xE2, 0x30, 0x00, 0x40, 0x40, 0x32, 0xA0, 0x00, 0xA0, 0x00, 0xA0, 0x00, 0xA0, 0x00), + (0xE3, 0x00, 0x00, 0x33, 0x33), + (0xE4, 0x44, 0x44), + (0xE5, 0x09, 0x2E, 0xA0, 0xA0, 0x0B, 0x30, 0xA0, 0xA0, 0x05, 0x2A, 0xA0, 0xA0, 0x07, 0x2C, 0xA0, 0xA0), + (0xE6, 0x00, 0x00, 0x33, 0x33), + (0xE7, 0x44, 0x44), + (0xE8, 0x08, 0x2D, 0xA0, 0xA0, 0x0A, 0x2F, 0xA0, 0xA0, 0x04, 0x29, 0xA0, 0xA0, 0x06, 0x2B, 0xA0, 0xA0), + (0xEB, 0x00, 0x00, 0x4E, 0x4E, 0x00, 0x00, 0x00), + (0xEC, 0x08, 0x01), + (0xED, 0xB0, 0x2B, 0x98, 0xA4, 0x56, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xF7, 0x65, 0x4A, 0x89, 0xB2, 0x0B), + (0xEF, 0x08, 0x08, 0x08, 0x45, 0x3F, 0x54), + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x00), + ] +) +# fmt: on From 8607a0881d4f3d3b6fe064287710f6af3f3a16b6 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 24 Nov 2025 10:10:24 -0500 Subject: [PATCH 02/28] [core] Add support for passing yaml files to clean-all (#12039) --- esphome/__main__.py | 2 +- esphome/writer.py | 8 +++++++- tests/unit_tests/test_writer.py | 31 +++++++++++++++++++++++++++++++ 3 files changed, 39 insertions(+), 2 deletions(-) diff --git a/esphome/__main__.py b/esphome/__main__.py index b0c081a34f2..f8fb678cb27 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -1319,7 +1319,7 @@ def parse_args(argv): "clean-all", help="Clean all build and platform files." ) parser_clean_all.add_argument( - "configuration", help="Your YAML configuration directory.", nargs="*" + "configuration", help="Your YAML file or configuration directory.", nargs="*" ) parser_dashboard = subparsers.add_parser( diff --git a/esphome/writer.py b/esphome/writer.py index b866a804b32..3124e9e12ce 100644 --- a/esphome/writer.py +++ b/esphome/writer.py @@ -343,7 +343,13 @@ def clean_build(clear_pio_cache: bool = True): def clean_all(configuration: list[str]): import shutil - data_dirs = [Path(dir) / ".esphome" for dir in configuration] + 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") + else: + data_dirs.append(item / ".esphome") if is_ha_addon(): data_dirs.append(Path("/data")) if "ESPHOME_DATA_DIR" in os.environ: diff --git a/tests/unit_tests/test_writer.py b/tests/unit_tests/test_writer.py index a4490fbbc05..a2a358f4d35 100644 --- a/tests/unit_tests/test_writer.py +++ b/tests/unit_tests/test_writer.py @@ -737,6 +737,37 @@ def test_write_cpp_with_duplicate_markers( write_cpp("// New code") +@patch("esphome.writer.CORE") +def test_clean_all_with_yaml_file( + mock_core: MagicMock, + tmp_path: Path, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test clean_all with a .yaml file uses parent directory.""" + # Create config directory with yaml file + config_dir = tmp_path / "config" + config_dir.mkdir() + yaml_file = config_dir / "test.yaml" + yaml_file.write_text("esphome:\n name: test\n") + + 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)]) + + # Verify .esphome directory still exists but contents cleaned + assert build_dir.exists() + assert not (build_dir / "dummy.txt").exists() + + # Verify logging mentions the build dir + assert "Cleaning" in caplog.text + assert str(build_dir) in caplog.text + + @patch("esphome.writer.CORE") def test_clean_all( mock_core: MagicMock, From 1f0a5e1eeab2d86031934a3f9c9e182458d60a5e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 24 Nov 2025 10:21:32 -0600 Subject: [PATCH 03/28] [logger] Reduce UART overhead on ESP32/ESP8266 and fix buffer truncation (#11927) --- esphome/components/logger/__init__.py | 2 + esphome/components/logger/logger.cpp | 21 ++++---- esphome/components/logger/logger.h | 53 +++++++++++++++++-- esphome/components/logger/logger_esp32.cpp | 32 ++++++----- esphome/components/logger/logger_esp8266.cpp | 5 +- esphome/components/logger/logger_host.cpp | 2 +- .../components/logger/logger_libretiny.cpp | 2 +- esphome/components/logger/logger_rp2040.cpp | 2 +- esphome/components/logger/logger_zephyr.cpp | 2 +- 9 files changed, 84 insertions(+), 37 deletions(-) diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index cf78e6ae631..39877030e9e 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -365,8 +365,10 @@ async def to_code(config): if CORE.is_esp32: if config[CONF_HARDWARE_UART] == USB_CDC: add_idf_sdkconfig_option("CONFIG_ESP_CONSOLE_USB_CDC", True) + cg.add_define("USE_LOGGER_UART_SELECTION_USB_CDC") elif config[CONF_HARDWARE_UART] == USB_SERIAL_JTAG: add_idf_sdkconfig_option("CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG", True) + cg.add_define("USE_LOGGER_UART_SELECTION_USB_SERIAL_JTAG") try: uart_selection(USB_SERIAL_JTAG) cg.add_define("USE_LOGGER_USB_SERIAL_JTAG") diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index 9a9bf89fe33..9803bf528c7 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -65,7 +65,9 @@ void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const ch uint16_t buffer_at = 0; // Initialize buffer position this->format_log_to_buffer_with_terminator_(level, tag, line, format, args, console_buffer, &buffer_at, MAX_CONSOLE_LOG_MSG_SIZE); - this->write_msg_(console_buffer); + // Add newline if platform needs it (ESP32 doesn't add via write_msg_) + this->add_newline_to_buffer_if_needed_(console_buffer, &buffer_at, MAX_CONSOLE_LOG_MSG_SIZE); + this->write_msg_(console_buffer, buffer_at); } // Reset the recursion guard for this task @@ -131,18 +133,19 @@ void Logger::log_vprintf_(uint8_t level, const char *tag, int line, const __Flas // Save the offset before calling format_log_to_buffer_with_terminator_ // since it will increment tx_buffer_at_ to the end of the formatted string - uint32_t msg_start = this->tx_buffer_at_; + uint16_t msg_start = this->tx_buffer_at_; this->format_log_to_buffer_with_terminator_(level, tag, line, this->tx_buffer_, args, this->tx_buffer_, &this->tx_buffer_at_, this->tx_buffer_size_); - // Write to console and send callback starting at the msg_start - if (this->baud_rate_ > 0) { - this->write_msg_(this->tx_buffer_ + msg_start); - } - size_t msg_length = + uint16_t msg_length = this->tx_buffer_at_ - msg_start; // Don't subtract 1 - tx_buffer_at_ is already at the null terminator position + + // Callbacks get message first (before console write) this->log_callback_.call(level, tag, this->tx_buffer_ + msg_start, msg_length); + // Write to console starting at the msg_start + this->write_tx_buffer_to_console_(msg_start, &msg_length); + global_recursion_guard_ = false; } #endif // USE_STORE_LOG_STR_IN_FLASH @@ -209,9 +212,7 @@ void Logger::process_messages_() { // This ensures all log messages appear on the console in a clean, serialized manner // Note: Messages may appear slightly out of order due to async processing, but // this is preferred over corrupted/interleaved console output - if (this->baud_rate_ > 0) { - this->write_msg_(this->tx_buffer_); - } + this->write_tx_buffer_to_console_(); } } else { // No messages to process, disable loop if appropriate diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index dc8e06e0c98..8ba3dacacb7 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -71,6 +71,17 @@ static constexpr uint16_t MAX_HEADER_SIZE = 128; // "0x" + 2 hex digits per byte + '\0' static constexpr size_t MAX_POINTER_REPRESENTATION = 2 + sizeof(void *) * 2 + 1; +// Platform-specific: does write_msg_ add its own newline? +// false: Caller must add newline to buffer before calling write_msg_ (ESP32, ESP8266) +// Allows single write call with newline included for efficiency +// true: write_msg_ adds newline itself via puts()/println() (other platforms) +// Newline should NOT be added to buffer +#if defined(USE_ESP32) || defined(USE_ESP8266) +static constexpr bool WRITE_MSG_ADDS_NEWLINE = false; +#else +static constexpr bool WRITE_MSG_ADDS_NEWLINE = true; +#endif + #if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) /** Enum for logging UART selection * @@ -173,7 +184,7 @@ class Logger : public Component { protected: void process_messages_(); - void write_msg_(const char *msg); + void write_msg_(const char *msg, size_t len); // Format a log message with printf-style arguments and write it to a buffer with header, footer, and null terminator // It's the caller's responsibility to initialize buffer_at (typically to 0) @@ -200,6 +211,35 @@ class Logger : public Component { } } + // Helper to add newline to buffer for platforms that need it + // Modifies buffer_at to include the newline + inline void HOT add_newline_to_buffer_if_needed_(char *buffer, uint16_t *buffer_at, uint16_t buffer_size) { + if constexpr (!WRITE_MSG_ADDS_NEWLINE) { + // Add newline - don't need to maintain null termination + // write_msg_ now always receives explicit length, so we can safely overwrite the null terminator + // This is safe because: + // 1. Callbacks already received the message (before we add newline) + // 2. write_msg_ receives the length explicitly (doesn't need null terminator) + if (*buffer_at < buffer_size) { + buffer[(*buffer_at)++] = '\n'; + } else if (buffer_size > 0) { + // Buffer was full - replace last char with newline to ensure it's visible + buffer[buffer_size - 1] = '\n'; + *buffer_at = buffer_size; + } + } + } + + // Helper to write tx_buffer_ to console if logging is enabled + // INTERNAL USE ONLY - offset > 0 requires length parameter to be non-null + inline void HOT write_tx_buffer_to_console_(uint16_t offset = 0, uint16_t *length = nullptr) { + if (this->baud_rate_ > 0) { + uint16_t *len_ptr = length ? length : &this->tx_buffer_at_; + this->add_newline_to_buffer_if_needed_(this->tx_buffer_ + offset, len_ptr, this->tx_buffer_size_ - offset); + this->write_msg_(this->tx_buffer_ + offset, *len_ptr); + } + } + // Helper to format and send a log message to both console and callbacks inline void HOT log_message_to_buffer_and_send_(uint8_t level, const char *tag, int line, const char *format, va_list args) { @@ -208,10 +248,11 @@ class Logger : public Component { this->format_log_to_buffer_with_terminator_(level, tag, line, format, args, this->tx_buffer_, &this->tx_buffer_at_, this->tx_buffer_size_); - if (this->baud_rate_ > 0) { - this->write_msg_(this->tx_buffer_); // If logging is enabled, write to console - } + // Callbacks get message WITHOUT newline (for API/MQTT/syslog) this->log_callback_.call(level, tag, this->tx_buffer_, this->tx_buffer_at_); + + // Console gets message WITH newline (if platform needs it) + this->write_tx_buffer_to_console_(); } // Write the body of the log message to the buffer @@ -425,7 +466,9 @@ class Logger : public Component { } // Update buffer_at with the formatted length (handle truncation) - uint16_t formatted_len = (ret >= remaining) ? remaining : ret; + // When vsnprintf truncates (ret >= remaining), it writes (remaining - 1) chars + null terminator + // When it doesn't truncate (ret < remaining), it writes ret chars + null terminator + uint16_t formatted_len = (ret >= remaining) ? (remaining - 1) : ret; *buffer_at += formatted_len; // Remove all trailing newlines right after formatting diff --git a/esphome/components/logger/logger_esp32.cpp b/esphome/components/logger/logger_esp32.cpp index 7fc79e6f541..32ef7524624 100644 --- a/esphome/components/logger/logger_esp32.cpp +++ b/esphome/components/logger/logger_esp32.cpp @@ -121,25 +121,23 @@ void Logger::pre_setup() { ESP_LOGI(TAG, "Log initialized"); } -void HOT Logger::write_msg_(const char *msg) { - if ( -#if defined(USE_LOGGER_USB_CDC) && !defined(USE_LOGGER_USB_SERIAL_JTAG) - this->uart_ == UART_SELECTION_USB_CDC -#elif defined(USE_LOGGER_USB_SERIAL_JTAG) && !defined(USE_LOGGER_USB_CDC) - this->uart_ == UART_SELECTION_USB_SERIAL_JTAG -#elif defined(USE_LOGGER_USB_CDC) && defined(USE_LOGGER_USB_SERIAL_JTAG) - this->uart_ == UART_SELECTION_USB_CDC || this->uart_ == UART_SELECTION_USB_SERIAL_JTAG +void HOT Logger::write_msg_(const char *msg, size_t len) { + // Length is now always passed explicitly - no strlen() fallback needed + +#if defined(USE_LOGGER_UART_SELECTION_USB_CDC) || defined(USE_LOGGER_UART_SELECTION_USB_SERIAL_JTAG) + // USB CDC/JTAG - single write including newline (already in buffer) + // Use fwrite to stdout which goes through VFS to USB console + // + // Note: These defines indicate the user's YAML configuration choice (hardware_uart: USB_CDC/USB_SERIAL_JTAG). + // They are ONLY defined when the user explicitly selects USB as the logger output in their config. + // This is compile-time selection, not runtime detection - if USB is configured, it's always used. + // There is no fallback to regular UART if "USB isn't connected" - that's the user's responsibility + // to configure correctly for their hardware. This approach eliminates runtime overhead. + fwrite(msg, 1, len, stdout); #else - /* DISABLES CODE */ (false) // NOLINT + // Regular UART - single write including newline (already in buffer) + uart_write_bytes(this->uart_num_, msg, len); #endif - ) { - puts(msg); - } else { - // Use tx_buffer_at_ if msg points to tx_buffer_, otherwise fall back to strlen - size_t len = (msg == this->tx_buffer_) ? this->tx_buffer_at_ : strlen(msg); - uart_write_bytes(this->uart_num_, msg, len); - uart_write_bytes(this->uart_num_, "\n", 1); - } } const LogString *Logger::get_uart_selection_() { diff --git a/esphome/components/logger/logger_esp8266.cpp b/esphome/components/logger/logger_esp8266.cpp index 5063d88b927..0fc73b747a0 100644 --- a/esphome/components/logger/logger_esp8266.cpp +++ b/esphome/components/logger/logger_esp8266.cpp @@ -33,7 +33,10 @@ void Logger::pre_setup() { ESP_LOGI(TAG, "Log initialized"); } -void HOT Logger::write_msg_(const char *msg) { this->hw_serial_->println(msg); } +void HOT Logger::write_msg_(const char *msg, size_t len) { + // Single write with newline already in buffer (added by caller) + this->hw_serial_->write(msg, len); +} const LogString *Logger::get_uart_selection_() { switch (this->uart_) { diff --git a/esphome/components/logger/logger_host.cpp b/esphome/components/logger/logger_host.cpp index 4abe92286a4..c5e1e6f8650 100644 --- a/esphome/components/logger/logger_host.cpp +++ b/esphome/components/logger/logger_host.cpp @@ -3,7 +3,7 @@ namespace esphome::logger { -void HOT Logger::write_msg_(const char *msg) { +void HOT Logger::write_msg_(const char *msg, size_t) { time_t rawtime; struct tm *timeinfo; char buffer[80]; diff --git a/esphome/components/logger/logger_libretiny.cpp b/esphome/components/logger/logger_libretiny.cpp index 3edfa744800..b8017b841dc 100644 --- a/esphome/components/logger/logger_libretiny.cpp +++ b/esphome/components/logger/logger_libretiny.cpp @@ -49,7 +49,7 @@ void Logger::pre_setup() { ESP_LOGI(TAG, "Log initialized"); } -void HOT Logger::write_msg_(const char *msg) { this->hw_serial_->println(msg); } +void HOT Logger::write_msg_(const char *msg, size_t) { this->hw_serial_->println(msg); } const LogString *Logger::get_uart_selection_() { switch (this->uart_) { diff --git a/esphome/components/logger/logger_rp2040.cpp b/esphome/components/logger/logger_rp2040.cpp index 63727c2cda9..4a8535c8e40 100644 --- a/esphome/components/logger/logger_rp2040.cpp +++ b/esphome/components/logger/logger_rp2040.cpp @@ -27,7 +27,7 @@ void Logger::pre_setup() { ESP_LOGI(TAG, "Log initialized"); } -void HOT Logger::write_msg_(const char *msg) { this->hw_serial_->println(msg); } +void HOT Logger::write_msg_(const char *msg, size_t) { this->hw_serial_->println(msg); } const LogString *Logger::get_uart_selection_() { switch (this->uart_) { diff --git a/esphome/components/logger/logger_zephyr.cpp b/esphome/components/logger/logger_zephyr.cpp index fb0c7dcca37..ec2ff3013c0 100644 --- a/esphome/components/logger/logger_zephyr.cpp +++ b/esphome/components/logger/logger_zephyr.cpp @@ -62,7 +62,7 @@ void Logger::pre_setup() { ESP_LOGI(TAG, "Log initialized"); } -void HOT Logger::write_msg_(const char *msg) { +void HOT Logger::write_msg_(const char *msg, size_t) { #ifdef CONFIG_PRINTK printk("%s\n", msg); #endif From 056b4375ebe238250675081613dcb00389e3254e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 24 Nov 2025 10:21:47 -0600 Subject: [PATCH 04/28] [api] Reduce heap allocations in DeviceInfoResponse (#11952) --- esphome/components/api/api_connection.cpp | 12 ++++++++---- esphome/components/bluetooth_proxy/bluetooth_proxy.h | 6 ++---- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index c60680ae431..04221a237b3 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1451,8 +1451,11 @@ bool APIConnection::send_device_info_response(const DeviceInfoRequest &msg) { #ifdef USE_AREAS resp.set_suggested_area(StringRef(App.get_area())); #endif - // mac_address must store temporary string - will be valid during send_message call - std::string mac_address = get_mac_address_pretty(); + // Stack buffer for MAC address (XX:XX:XX:XX:XX:XX\0 = 18 bytes) + char mac_address[18]; + uint8_t mac[6]; + get_mac_address_raw(mac); + format_mac_addr_upper(mac, mac_address); resp.set_mac_address(StringRef(mac_address)); resp.set_esphome_version(ESPHOME_VERSION_REF); @@ -1493,8 +1496,9 @@ bool APIConnection::send_device_info_response(const DeviceInfoRequest &msg) { #endif #ifdef USE_BLUETOOTH_PROXY resp.bluetooth_proxy_feature_flags = bluetooth_proxy::global_bluetooth_proxy->get_feature_flags(); - // bt_mac must store temporary string - will be valid during send_message call - std::string bluetooth_mac = bluetooth_proxy::global_bluetooth_proxy->get_bluetooth_mac_address_pretty(); + // Stack buffer for Bluetooth MAC address (XX:XX:XX:XX:XX:XX\0 = 18 bytes) + char bluetooth_mac[18]; + bluetooth_proxy::global_bluetooth_proxy->get_bluetooth_mac_address_pretty(bluetooth_mac); resp.set_bluetooth_mac_address(StringRef(bluetooth_mac)); #endif #ifdef USE_VOICE_ASSISTANT diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.h b/esphome/components/bluetooth_proxy/bluetooth_proxy.h index a5f0fbe32f8..4de541fac2f 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.h +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.h @@ -130,11 +130,9 @@ class BluetoothProxy final : public esp32_ble_tracker::ESPBTDeviceListener, publ return flags; } - std::string get_bluetooth_mac_address_pretty() { + void get_bluetooth_mac_address_pretty(std::span output) { const uint8_t *mac = esp_bt_dev_get_address(); - char buf[18]; - format_mac_addr_upper(mac, buf); - return std::string(buf); + format_mac_addr_upper(mac, output.data()); } protected: From 426734beef724112b37037641c7ea7c20f044082 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 24 Nov 2025 10:22:01 -0600 Subject: [PATCH 05/28] [web_server_base] Replace shared_ptr with unique_ptr for AsyncWebServer (#11984) --- esphome/components/web_server_base/web_server_base.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/web_server_base/web_server_base.h b/esphome/components/web_server_base/web_server_base.h index 039a452d646..fbf0d00c061 100644 --- a/esphome/components/web_server_base/web_server_base.h +++ b/esphome/components/web_server_base/web_server_base.h @@ -111,7 +111,7 @@ class WebServerBase : public Component { this->initialized_++; return; } - this->server_ = std::make_shared(this->port_); + this->server_ = std::make_unique(this->port_); // All content is controlled and created by user - so allowing all origins is fine here. DefaultHeaders::Instance().addHeader("Access-Control-Allow-Origin", "*"); this->server_->begin(); @@ -127,7 +127,7 @@ class WebServerBase : public Component { this->server_ = nullptr; } } - std::shared_ptr get_server() const { return server_; } + AsyncWebServer *get_server() const { return this->server_.get(); } float get_setup_priority() const override; #ifdef USE_WEBSERVER_AUTH @@ -143,7 +143,7 @@ class WebServerBase : public Component { protected: int initialized_{0}; uint16_t port_{80}; - std::shared_ptr server_{nullptr}; + std::unique_ptr server_{nullptr}; std::vector handlers_; #ifdef USE_WEBSERVER_AUTH internal::Credentials credentials_; From 3c48e13c9f5cf391e775174748c7da77928e9b8d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 24 Nov 2025 10:22:13 -0600 Subject: [PATCH 06/28] [ethernet] Conditionally compile manual_ip to save 24 bytes RAM (#11832) --- esphome/components/ethernet/__init__.py | 1 + esphome/components/ethernet/ethernet_component.cpp | 12 ++++++++++-- esphome/components/ethernet/ethernet_component.h | 4 ++++ esphome/core/defines.h | 1 + 4 files changed, 16 insertions(+), 2 deletions(-) diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py index 2f02d227d71..b4d67635c18 100644 --- a/esphome/components/ethernet/__init__.py +++ b/esphome/components/ethernet/__init__.py @@ -383,6 +383,7 @@ async def to_code(config): cg.add(var.set_use_address(config[CONF_USE_ADDRESS])) if CONF_MANUAL_IP in config: + cg.add_define("USE_ETHERNET_MANUAL_IP") cg.add(var.set_manual_ip(manual_ip(config[CONF_MANUAL_IP]))) # Add compile-time define for PHY types with specific code diff --git a/esphome/components/ethernet/ethernet_component.cpp b/esphome/components/ethernet/ethernet_component.cpp index cad963b2999..9a46aa2687c 100644 --- a/esphome/components/ethernet/ethernet_component.cpp +++ b/esphome/components/ethernet/ethernet_component.cpp @@ -553,11 +553,14 @@ void EthernetComponent::start_connect_() { } esp_netif_ip_info_t info; +#ifdef USE_ETHERNET_MANUAL_IP if (this->manual_ip_.has_value()) { info.ip = this->manual_ip_->static_ip; info.gw = this->manual_ip_->gateway; info.netmask = this->manual_ip_->subnet; - } else { + } else +#endif + { info.ip.addr = 0; info.gw.addr = 0; info.netmask.addr = 0; @@ -578,6 +581,7 @@ void EthernetComponent::start_connect_() { err = esp_netif_set_ip_info(this->eth_netif_, &info); ESPHL_ERROR_CHECK(err, "DHCPC set IP info error"); +#ifdef USE_ETHERNET_MANUAL_IP if (this->manual_ip_.has_value()) { LwIPLock lock; if (this->manual_ip_->dns1.is_set()) { @@ -590,7 +594,9 @@ void EthernetComponent::start_connect_() { d = this->manual_ip_->dns2; dns_setserver(1, &d); } - } else { + } else +#endif + { err = esp_netif_dhcpc_start(this->eth_netif_); if (err != ESP_ERR_ESP_NETIF_DHCP_ALREADY_STARTED) { ESPHL_ERROR_CHECK(err, "DHCPC start error"); @@ -688,7 +694,9 @@ void EthernetComponent::set_clk_mode(emac_rmii_clock_mode_t clk_mode) { this->cl void EthernetComponent::add_phy_register(PHYRegister register_value) { this->phy_registers_.push_back(register_value); } #endif void EthernetComponent::set_type(EthernetType type) { this->type_ = type; } +#ifdef USE_ETHERNET_MANUAL_IP 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. diff --git a/esphome/components/ethernet/ethernet_component.h b/esphome/components/ethernet/ethernet_component.h index f1f0ac9cb8f..bffed4dc4a3 100644 --- a/esphome/components/ethernet/ethernet_component.h +++ b/esphome/components/ethernet/ethernet_component.h @@ -82,7 +82,9 @@ class EthernetComponent : public Component { void add_phy_register(PHYRegister register_value); #endif void set_type(EthernetType type); +#ifdef USE_ETHERNET_MANUAL_IP void set_manual_ip(const ManualIP &manual_ip); +#endif void set_fixed_mac(const std::array &mac) { this->fixed_mac_ = mac; } network::IPAddresses get_ip_addresses(); @@ -137,7 +139,9 @@ class EthernetComponent : public Component { uint8_t mdc_pin_{23}; uint8_t mdio_pin_{18}; #endif +#ifdef USE_ETHERNET_MANUAL_IP optional manual_ip_{}; +#endif uint32_t connect_begin_; // Group all uint8_t types together (enums and bools) diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 03362ce07a1..5e7f51e04ca 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -216,6 +216,7 @@ #define USE_ARDUINO_VERSION_CODE VERSION_CODE(3, 3, 2) #define USE_ETHERNET #define USE_ETHERNET_KSZ8081 +#define USE_ETHERNET_MANUAL_IP #endif #ifdef USE_ESP_IDF From 737f23a0bdb8a1c09bb0dee9709d09cb4c5403c4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 24 Nov 2025 10:23:11 -0600 Subject: [PATCH 07/28] [light] Dynamically disable loop when idle to reduce CPU overhead (#11881) --- esphome/components/light/light_state.cpp | 24 ++++++++++++++++++++++++ esphome/components/light/light_state.h | 3 +++ 2 files changed, 27 insertions(+) diff --git a/esphome/components/light/light_state.cpp b/esphome/components/light/light_state.cpp index 36b2af03a5c..9cde9077da6 100644 --- a/esphome/components/light/light_state.cpp +++ b/esphome/components/light/light_state.cpp @@ -23,6 +23,9 @@ void LightState::setup() { effect->init_internal(this); } + // Start with loop disabled if idle - respects any effects/transitions set up during initialization + this->disable_loop_if_idle_(); + // When supported color temperature range is known, initialize color temperature setting within bounds. auto traits = this->get_traits(); float min_mireds = traits.get_min_mireds(); @@ -125,6 +128,9 @@ void LightState::loop() { this->is_transformer_active_ = false; this->transformer_ = nullptr; this->target_state_reached_callback_.call(); + + // Disable loop if idle (no transformer and no effect) + this->disable_loop_if_idle_(); } } @@ -132,6 +138,8 @@ void LightState::loop() { if (this->next_write_) { this->next_write_ = false; this->output_->write_state(this); + // Disable loop if idle (no transformer and no effect) + this->disable_loop_if_idle_(); } } @@ -227,6 +235,8 @@ void LightState::start_effect_(uint32_t effect_index) { this->active_effect_index_ = effect_index; auto *effect = this->get_active_effect_(); effect->start_internal(); + // Enable loop while effect is active + this->enable_loop(); } LightEffect *LightState::get_active_effect_() { if (this->active_effect_index_ == 0) { @@ -241,6 +251,8 @@ void LightState::stop_effect_() { effect->stop(); } this->active_effect_index_ = 0; + // Disable loop if idle (no effect and no transformer) + this->disable_loop_if_idle_(); } void LightState::start_transition_(const LightColorValues &target, uint32_t length, bool set_remote_values) { @@ -250,6 +262,8 @@ void LightState::start_transition_(const LightColorValues &target, uint32_t leng if (set_remote_values) { this->remote_values = target; } + // Enable loop while transition is active + this->enable_loop(); } void LightState::start_flash_(const LightColorValues &target, uint32_t length, bool set_remote_values) { @@ -265,6 +279,8 @@ void LightState::start_flash_(const LightColorValues &target, uint32_t length, b if (set_remote_values) { this->remote_values = target; }; + // Enable loop while flash is active + this->enable_loop(); } void LightState::set_immediately_(const LightColorValues &target, bool set_remote_values) { @@ -276,6 +292,14 @@ void LightState::set_immediately_(const LightColorValues &target, bool set_remot } this->output_->update_state(this); this->next_write_ = true; + this->enable_loop(); +} + +void LightState::disable_loop_if_idle_() { + // Only disable loop if both transformer and effect are inactive, and no pending writes + if (this->transformer_ == nullptr && this->get_active_effect_() == nullptr && !this->next_write_) { + this->disable_loop(); + } } void LightState::save_remote_values_() { diff --git a/esphome/components/light/light_state.h b/esphome/components/light/light_state.h index 06519cdc14f..ad8922b46fc 100644 --- a/esphome/components/light/light_state.h +++ b/esphome/components/light/light_state.h @@ -255,6 +255,9 @@ class LightState : public EntityBase, public Component { /// Internal method to save the current remote_values to the preferences void save_remote_values_(); + /// Disable loop if neither transformer nor effect is active + void disable_loop_if_idle_(); + /// Store the output to allow effects to have more access. LightOutput *output_; /// The currently active transformer for this light (transition/flash). From 04ec6a69995adb0e6277e7abf2e49c05486a0e37 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 24 Nov 2025 10:23:31 -0600 Subject: [PATCH 08/28] [api] Use stack buffer for MAC address in Noise handshake (#12072) --- esphome/components/api/api_frame_helper_noise.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/esphome/components/api/api_frame_helper_noise.cpp b/esphome/components/api/api_frame_helper_noise.cpp index 633b07a7fa1..8bcec0f9f32 100644 --- a/esphome/components/api/api_frame_helper_noise.cpp +++ b/esphome/components/api/api_frame_helper_noise.cpp @@ -239,12 +239,13 @@ APIError APINoiseFrameHelper::state_action_() { } if (state_ == State::SERVER_HELLO) { // send server hello + constexpr size_t mac_len = 13; // 12 hex chars + null terminator const std::string &name = App.get_name(); - const std::string &mac = get_mac_address(); + char mac[mac_len]; + get_mac_address_into_buffer(mac); // Calculate positions and sizes size_t name_len = name.size() + 1; // including null terminator - size_t mac_len = mac.size() + 1; // including null terminator size_t name_offset = 1; size_t mac_offset = name_offset + name_len; size_t total_size = 1 + name_len + mac_len; @@ -257,7 +258,7 @@ APIError APINoiseFrameHelper::state_action_() { // node name, terminated by null byte std::memcpy(msg.get() + name_offset, name.c_str(), name_len); // node mac, terminated by null byte - std::memcpy(msg.get() + mac_offset, mac.c_str(), mac_len); + std::memcpy(msg.get() + mac_offset, mac, mac_len); aerr = write_frame_(msg.get(), total_size); if (aerr != APIError::OK) From 06815fe177e5980ef049ec859c3fc8d26a28290d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 24 Nov 2025 10:41:24 -0600 Subject: [PATCH 09/28] [script][wait_until] Fix FIFO ordering and reentrancy bugs (#12049) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/script/script.h | 20 +-- esphome/core/base_automation.h | 9 +- .../fixtures/script_delay_with_params.yaml | 131 ++++++++++++++++++ .../fixtures/wait_until_fifo_ordering.yaml | 82 +++++++++++ tests/integration/test_script_delay_params.py | 121 ++++++++++++++++ tests/integration/test_wait_until_ordering.py | 90 ++++++++++++ 6 files changed, 441 insertions(+), 12 deletions(-) create mode 100644 tests/integration/fixtures/script_delay_with_params.yaml create mode 100644 tests/integration/fixtures/wait_until_fifo_ordering.yaml create mode 100644 tests/integration/test_script_delay_params.py create mode 100644 tests/integration/test_wait_until_ordering.py diff --git a/esphome/components/script/script.h b/esphome/components/script/script.h index 51cece01e41..d60ed657f70 100644 --- a/esphome/components/script/script.h +++ b/esphome/components/script/script.h @@ -1,8 +1,8 @@ #pragma once +#include #include #include -#include #include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/helpers.h" @@ -290,10 +290,10 @@ template class ScriptWaitAction : public Action, } // Store parameters for later execution - this->param_queue_.emplace_front(x...); - // Enable loop now that we have work to do + this->param_queue_.emplace_back(x...); + // Enable loop now that we have work to do - don't call loop() synchronously! + // Let the event loop call it to avoid reentrancy issues this->enable_loop(); - this->loop(); } void loop() override { @@ -303,13 +303,17 @@ template class ScriptWaitAction : public Action, if (this->script_->is_running()) return; - while (!this->param_queue_.empty()) { + // Only process ONE queued item per loop iteration + // Processing all items in a while loop causes infinite loops because + // play_next_() can trigger more items to be queued + if (!this->param_queue_.empty()) { auto ¶ms = this->param_queue_.front(); this->play_next_tuple_(params, typename gens::type()); this->param_queue_.pop_front(); + } else { + // Queue is now empty - disable loop until next play_complex + this->disable_loop(); } - // Queue is now empty - disable loop until next play_complex - this->disable_loop(); } void play(const Ts &...x) override { /* ignore - see play_complex */ @@ -326,7 +330,7 @@ template class ScriptWaitAction : public Action, } C *script_; - std::forward_list> param_queue_; + std::list> param_queue_; }; } // namespace script diff --git a/esphome/core/base_automation.h b/esphome/core/base_automation.h index c2519da839a..e8878ac251a 100644 --- a/esphome/core/base_automation.h +++ b/esphome/core/base_automation.h @@ -9,8 +9,8 @@ #include "esphome/core/application.h" #include "esphome/core/helpers.h" +#include #include -#include namespace esphome { @@ -445,9 +445,10 @@ template class WaitUntilAction : public Action, public Co // Store for later processing auto now = millis(); auto timeout = this->timeout_value_.optional_value(x...); - this->var_queue_.emplace_front(now, timeout, std::make_tuple(x...)); + this->var_queue_.emplace_back(now, timeout, std::make_tuple(x...)); - // Do immediate check with fresh timestamp + // Do immediate check with fresh timestamp - don't call loop() synchronously! + // Let the event loop call it to avoid reentrancy issues if (this->process_queue_(now)) { // Only enable loop if we still have pending items this->enable_loop(); @@ -499,7 +500,7 @@ template class WaitUntilAction : public Action, public Co } Condition *condition_; - std::forward_list, std::tuple>> var_queue_{}; + std::list, std::tuple>> var_queue_{}; }; template class UpdateComponentAction : public Action { diff --git a/tests/integration/fixtures/script_delay_with_params.yaml b/tests/integration/fixtures/script_delay_with_params.yaml new file mode 100644 index 00000000000..2a0f16d9fe2 --- /dev/null +++ b/tests/integration/fixtures/script_delay_with_params.yaml @@ -0,0 +1,131 @@ +esphome: + name: test-script-delay-params + +host: + +api: + actions: + # Test case from issue #12044: parent script with repeat calling child with delay + - action: test_repeat_with_delay + then: + - logger.log: "=== TEST: Repeat loop calling script with delay and parameters ===" + - script.execute: father_script + + # Test case from issue #12043: script.wait with delayed child script + - action: test_script_wait + then: + - logger.log: "=== TEST: script.wait with delayed child script ===" + - script.execute: show_start_page + - script.wait: show_start_page + - logger.log: "After wait: script completed successfully" + + # Test: Delay with different parameter types + - action: test_delay_param_types + then: + - logger.log: "=== TEST: Delay with various parameter types ===" + - script.execute: + id: delay_with_int + val: 42 + - delay: 50ms + - script.execute: + id: delay_with_string + msg: "test message" + - delay: 50ms + - script.execute: + id: delay_with_float + num: 3.14 + +logger: + level: DEBUG + +script: + # Reproduces issue #12044: child script with conditional delay + - id: son_script + mode: single + parameters: + iteration: int + then: + - logger.log: + format: "Son script started with iteration %d" + args: ['iteration'] + - if: + condition: + lambda: 'return iteration >= 5;' + then: + - logger.log: + format: "Son script delaying for iteration %d" + args: ['iteration'] + - delay: 100ms + - logger.log: + format: "Son script finished with iteration %d" + args: ['iteration'] + + # Reproduces issue #12044: parent script with repeat loop + - id: father_script + mode: single + then: + - repeat: + count: 10 + then: + - logger.log: + format: "Father iteration %d: calling son" + args: ['iteration'] + - script.execute: + id: son_script + iteration: !lambda 'return iteration;' + - script.wait: son_script + - logger.log: + format: "Father iteration %d: son finished, wait returned" + args: ['iteration'] + + # Reproduces issue #12043: script.wait hangs + - id: show_start_page + mode: single + then: + - logger.log: "Start page: beginning" + - delay: 100ms + - logger.log: "Start page: after delay" + - delay: 100ms + - logger.log: "Start page: completed" + + # Test delay with int parameter + - id: delay_with_int + mode: single + parameters: + val: int + then: + - logger.log: + format: "Int test: before delay, val=%d" + args: ['val'] + - delay: 50ms + - logger.log: + format: "Int test: after delay, val=%d" + args: ['val'] + + # Test delay with string parameter + - id: delay_with_string + mode: single + parameters: + msg: string + then: + - logger.log: + format: "String test: before delay, msg=%s" + args: ['msg.c_str()'] + - delay: 50ms + - logger.log: + format: "String test: after delay, msg=%s" + args: ['msg.c_str()'] + + # Test delay with float parameter + - id: delay_with_float + mode: single + parameters: + num: float + then: + - logger.log: + format: "Float test: before delay, num=%.2f" + args: ['num'] + - delay: 50ms + - logger.log: + format: "Float test: after delay, num=%.2f" + args: ['num'] diff --git a/tests/integration/fixtures/wait_until_fifo_ordering.yaml b/tests/integration/fixtures/wait_until_fifo_ordering.yaml new file mode 100644 index 00000000000..5dd60c8755e --- /dev/null +++ b/tests/integration/fixtures/wait_until_fifo_ordering.yaml @@ -0,0 +1,82 @@ +esphome: + name: test-wait-until-ordering + +host: + +api: + actions: + - action: test_wait_until_fifo + then: + - logger.log: "=== TEST: wait_until should execute in FIFO order ===" + - globals.set: + id: gate_open + value: 'false' + - delay: 100ms + # Start multiple parallel executions of coordinator script + # Each will call the shared waiter script, queueing in same wait_until + - script.execute: coordinator_0 + - script.execute: coordinator_1 + - script.execute: coordinator_2 + - script.execute: coordinator_3 + - script.execute: coordinator_4 + # Give scripts time to reach wait_until and queue + - delay: 200ms + - logger.log: "Opening gate - all wait_until should complete now" + - globals.set: + id: gate_open + value: 'true' + - delay: 500ms + - logger.log: "Test complete" + +globals: + - id: gate_open + type: bool + initial_value: 'false' + +script: + # Shared waiter with single wait_until action (all coordinators call this) + - id: waiter + mode: parallel + parameters: + iter: int + then: + - lambda: 'ESP_LOGD("main", "Queueing iteration %d", iter);' + - wait_until: + condition: + lambda: 'return id(gate_open);' + timeout: 5s + - lambda: 'ESP_LOGD("main", "Completed iteration %d", iter);' + + # Coordinator scripts - each calls shared waiter with different iteration number + - id: coordinator_0 + then: + - script.execute: + id: waiter + iter: 0 + + - id: coordinator_1 + then: + - script.execute: + id: waiter + iter: 1 + + - id: coordinator_2 + then: + - script.execute: + id: waiter + iter: 2 + + - id: coordinator_3 + then: + - script.execute: + id: waiter + iter: 3 + + - id: coordinator_4 + then: + - script.execute: + id: waiter + iter: 4 + +logger: + level: DEBUG diff --git a/tests/integration/test_script_delay_params.py b/tests/integration/test_script_delay_params.py new file mode 100644 index 00000000000..1b5d70863b6 --- /dev/null +++ b/tests/integration/test_script_delay_params.py @@ -0,0 +1,121 @@ +"""Integration test for script.wait FIFO ordering (issues #12043, #12044). + +This test verifies that ScriptWaitAction processes queued items in FIFO order. + +PR #7972 introduced bugs in ScriptWaitAction: +- Used emplace_front() causing LIFO ordering instead of FIFO +- Called loop() synchronously causing reentrancy issues +- Used while loop processing entire queue causing infinite loops + +These bugs manifested as: +- Scripts becoming "zombies" (stuck in running state) +- script.wait hanging forever +- Incorrect execution order +""" + +from __future__ import annotations + +import asyncio +import re + +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_script_delay_with_params( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test that script.wait processes queued items in FIFO order. + + This reproduces issues #12043 and #12044 where scripts would hang or become + zombies due to LIFO ordering bugs in ScriptWaitAction from PR #7972. + """ + test_complete = asyncio.Event() + + # Patterns to match in logs + father_calling_pattern = re.compile(r"Father iteration (\d+): calling son") + son_started_pattern = re.compile(r"Son script started with iteration (\d+)") + son_delaying_pattern = re.compile(r"Son script delaying for iteration (\d+)") + son_finished_pattern = re.compile(r"Son script finished with iteration (\d+)") + father_wait_returned_pattern = re.compile( + r"Father iteration (\d+): son finished, wait returned" + ) + + # Track which iterations completed + father_calling = set() + son_started = set() + son_delaying = set() + son_finished = set() + wait_returned = set() + + def check_output(line: str) -> None: + """Check log output for expected messages.""" + if test_complete.is_set(): + return + + if mo := father_calling_pattern.search(line): + father_calling.add(int(mo.group(1))) + elif mo := son_started_pattern.search(line): + son_started.add(int(mo.group(1))) + elif mo := son_delaying_pattern.search(line): + son_delaying.add(int(mo.group(1))) + elif mo := son_finished_pattern.search(line): + son_finished.add(int(mo.group(1))) + elif mo := father_wait_returned_pattern.search(line): + iteration = int(mo.group(1)) + wait_returned.add(iteration) + # Test completes when iteration 9 finishes + if iteration == 9: + test_complete.set() + + # Run with log monitoring + async with ( + run_compiled(yaml_config, line_callback=check_output), + api_client_connected() as client, + ): + # Verify device info + device_info = await client.device_info() + assert device_info is not None + assert device_info.name == "test-script-delay-params" + + # Get services + _, services = await client.list_entities_services() + test_service = next( + (s for s in services if s.name == "test_repeat_with_delay"), None + ) + assert test_service is not None, "test_repeat_with_delay service not found" + + # Execute the test + client.execute_service(test_service, {}) + + # Wait for test to complete (10 iterations * ~100ms each + margin) + try: + await asyncio.wait_for(test_complete.wait(), timeout=5.0) + except TimeoutError: + pytest.fail( + f"Test timed out. Completed iterations: {sorted(wait_returned)}. " + f"This likely indicates the script became a zombie (issue #12044)." + ) + + # Verify all 10 iterations completed successfully + expected_iterations = set(range(10)) + assert father_calling == expected_iterations, "Not all iterations started" + assert son_started == expected_iterations, ( + "Son script not started for all iterations" + ) + assert son_finished == expected_iterations, ( + "Son script not finished for all iterations" + ) + assert wait_returned == expected_iterations, ( + "script.wait did not return for all iterations" + ) + + # Verify delays were triggered for iterations >= 5 + expected_delays = set(range(5, 10)) + assert son_delaying == expected_delays, ( + "Delays not triggered for iterations >= 5" + ) diff --git a/tests/integration/test_wait_until_ordering.py b/tests/integration/test_wait_until_ordering.py new file mode 100644 index 00000000000..7c39913e5ae --- /dev/null +++ b/tests/integration/test_wait_until_ordering.py @@ -0,0 +1,90 @@ +"""Integration test for wait_until FIFO ordering. + +This test verifies that when multiple wait_until actions are queued, +they execute in FIFO (First In First Out) order, not LIFO. + +PR #7972 introduced a bug where emplace_front() was used, causing +LIFO ordering which is incorrect. +""" + +from __future__ import annotations + +import asyncio +import re + +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_wait_until_fifo_ordering( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test that wait_until executes queued items in FIFO order. + + With the bug (using emplace_front), the order would be 4,3,2,1,0 (LIFO). + With the fix (using emplace_back), the order should be 0,1,2,3,4 (FIFO). + """ + test_complete = asyncio.Event() + + # Track completion order + completed_order = [] + + # Patterns to match + queuing_pattern = re.compile(r"Queueing iteration (\d+)") + completed_pattern = re.compile(r"Completed iteration (\d+)") + + def check_output(line: str) -> None: + """Check log output for completion order.""" + if test_complete.is_set(): + return + + if mo := queuing_pattern.search(line): + iteration = int(mo.group(1)) + + elif mo := completed_pattern.search(line): + iteration = int(mo.group(1)) + completed_order.append(iteration) + + # Test completes when all 5 have completed + if len(completed_order) == 5: + test_complete.set() + + # Run with log monitoring + async with ( + run_compiled(yaml_config, line_callback=check_output), + api_client_connected() as client, + ): + # Verify device info + device_info = await client.device_info() + assert device_info is not None + assert device_info.name == "test-wait-until-ordering" + + # Get services + _, services = await client.list_entities_services() + test_service = next( + (s for s in services if s.name == "test_wait_until_fifo"), None + ) + assert test_service is not None, "test_wait_until_fifo service not found" + + # Execute the test + client.execute_service(test_service, {}) + + # Wait for test to complete + try: + await asyncio.wait_for(test_complete.wait(), timeout=5.0) + except TimeoutError: + pytest.fail( + f"Test timed out. Completed order: {completed_order}. " + f"Expected 5 completions but got {len(completed_order)}." + ) + + # Verify FIFO order + expected_order = [0, 1, 2, 3, 4] + assert completed_order == expected_order, ( + f"Unexpected order: {completed_order}. " + f"Expected FIFO order: {expected_order}" + ) From 0764f4da86dee9a325237ca1fa2e5f3d71d0f6c7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 24 Nov 2025 11:02:24 -0600 Subject: [PATCH 10/28] [esp_ldo,mipi_dsi,mipi_rgb] Fix dangling pointer bugs in mark_failed() (#12077) --- esphome/components/esp_ldo/esp_ldo.cpp | 4 ++-- esphome/components/mipi_dsi/mipi_dsi.cpp | 6 ++++++ esphome/components/mipi_dsi/mipi_dsi.h | 5 +---- esphome/components/mipi_rgb/mipi_rgb.cpp | 4 ++-- 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/esphome/components/esp_ldo/esp_ldo.cpp b/esphome/components/esp_ldo/esp_ldo.cpp index eb04670d7ea..9ea7000b706 100644 --- a/esphome/components/esp_ldo/esp_ldo.cpp +++ b/esphome/components/esp_ldo/esp_ldo.cpp @@ -14,8 +14,8 @@ void EspLdo::setup() { config.flags.adjustable = this->adjustable_; auto err = esp_ldo_acquire_channel(&config, &this->handle_); if (err != ESP_OK) { - auto msg = str_sprintf("Failed to acquire LDO channel %d with voltage %fV", this->channel_, this->voltage_); - this->mark_failed(msg.c_str()); + ESP_LOGE(TAG, "Failed to acquire LDO channel %d with voltage %fV", this->channel_, this->voltage_); + this->mark_failed("Failed to acquire LDO channel"); } else { ESP_LOGD(TAG, "Acquired LDO channel %d with voltage %fV", this->channel_, this->voltage_); } diff --git a/esphome/components/mipi_dsi/mipi_dsi.cpp b/esphome/components/mipi_dsi/mipi_dsi.cpp index fbe251de41c..7305435e4b2 100644 --- a/esphome/components/mipi_dsi/mipi_dsi.cpp +++ b/esphome/components/mipi_dsi/mipi_dsi.cpp @@ -11,6 +11,12 @@ static bool notify_refresh_ready(esp_lcd_panel_handle_t panel, esp_lcd_dpi_panel xSemaphoreGiveFromISR(sem, &need_yield); return (need_yield == pdTRUE); } + +void MIPI_DSI::smark_failed(const char *message, esp_err_t err) { + ESP_LOGE(TAG, "%s: %s", message, esp_err_to_name(err)); + this->mark_failed(message); +} + void MIPI_DSI::setup() { ESP_LOGCONFIG(TAG, "Running Setup"); diff --git a/esphome/components/mipi_dsi/mipi_dsi.h b/esphome/components/mipi_dsi/mipi_dsi.h index ce8a2a22364..98ee092ed11 100644 --- a/esphome/components/mipi_dsi/mipi_dsi.h +++ b/esphome/components/mipi_dsi/mipi_dsi.h @@ -62,10 +62,7 @@ class MIPI_DSI : public display::Display { void set_lanes(uint8_t lanes) { this->lanes_ = lanes; } void set_madctl(uint8_t madctl) { this->madctl_ = madctl; } - void smark_failed(const char *message, esp_err_t err) { - auto str = str_sprintf("Setup failed: %s: %s", message, esp_err_to_name(err)); - this->mark_failed(str.c_str()); - } + void smark_failed(const char *message, esp_err_t err); void update() override; diff --git a/esphome/components/mipi_rgb/mipi_rgb.cpp b/esphome/components/mipi_rgb/mipi_rgb.cpp index 080fb08c094..4c687724cf1 100644 --- a/esphome/components/mipi_rgb/mipi_rgb.cpp +++ b/esphome/components/mipi_rgb/mipi_rgb.cpp @@ -164,8 +164,8 @@ void MipiRgb::common_setup_() { if (err == ESP_OK) err = esp_lcd_panel_init(this->handle_); if (err != ESP_OK) { - auto msg = str_sprintf("lcd setup failed: %s", esp_err_to_name(err)); - this->mark_failed(msg.c_str()); + ESP_LOGE(TAG, "lcd setup failed: %s", esp_err_to_name(err)); + this->mark_failed("lcd setup failed"); } ESP_LOGCONFIG(TAG, "MipiRgb setup complete"); } From 66cda0466469531a7a9428db33251c6ad985c9bd Mon Sep 17 00:00:00 2001 From: Flo Date: Mon, 24 Nov 2025 18:19:38 +0100 Subject: [PATCH 11/28] [wifi] ap_active condition (#11852) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- esphome/components/wifi/__init__.py | 6 ++++++ esphome/components/wifi/wifi_component.cpp | 1 + esphome/components/wifi/wifi_component.h | 6 ++++++ tests/components/wifi/common.yaml | 4 ++++ 4 files changed, 17 insertions(+) diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index 2b21478f304..b9c0fa28a75 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -97,6 +97,7 @@ WIFI_MIN_AUTH_MODES = { VALIDATE_WIFI_MIN_AUTH_MODE = cv.enum(WIFI_MIN_AUTH_MODES, upper=True) WiFiConnectedCondition = wifi_ns.class_("WiFiConnectedCondition", Condition) WiFiEnabledCondition = wifi_ns.class_("WiFiEnabledCondition", Condition) +WiFiAPActiveCondition = wifi_ns.class_("WiFiAPActiveCondition", Condition) WiFiEnableAction = wifi_ns.class_("WiFiEnableAction", automation.Action) WiFiDisableAction = wifi_ns.class_("WiFiDisableAction", automation.Action) WiFiConfigureAction = wifi_ns.class_( @@ -590,6 +591,11 @@ async def wifi_enabled_to_code(config, condition_id, template_arg, args): return cg.new_Pvariable(condition_id, template_arg) +@automation.register_condition("wifi.ap_active", WiFiAPActiveCondition, cv.Schema({})) +async def wifi_ap_active_to_code(config, condition_id, template_arg, args): + return cg.new_Pvariable(condition_id, template_arg) + + @automation.register_action("wifi.enable", WiFiEnableAction, cv.Schema({})) async def wifi_enable_to_code(config, action_id, template_arg, args): return cg.new_Pvariable(action_id, template_arg) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 6f698bc2a89..23a40204536 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -530,6 +530,7 @@ 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->state_ == WIFI_COMPONENT_STATE_AP; } bool WiFiComponent::has_sta() const { return !this->sta_.empty(); } #ifdef USE_WIFI_11KV_SUPPORT void WiFiComponent::set_btm(bool btm) { this->btm_ = btm; } diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index b3548078bc4..441606a2c1d 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -308,6 +308,7 @@ class WiFiComponent : public Component { bool has_sta() const; bool has_ap() const; + bool is_ap_active() const; #ifdef USE_WIFI_11KV_SUPPORT void set_btm(bool btm); @@ -557,6 +558,11 @@ template class WiFiEnabledCondition : public Condition { bool check(const Ts &...x) override { return !global_wifi_component->is_disabled(); } }; +template class WiFiAPActiveCondition : public Condition { + public: + bool check(const Ts &...x) override { return global_wifi_component->is_ap_active(); } +}; + template class WiFiEnableAction : public Action { public: void play(const Ts &...x) override { global_wifi_component->enable(); } diff --git a/tests/components/wifi/common.yaml b/tests/components/wifi/common.yaml index 5d9973cbc80..7ce74ab00d3 100644 --- a/tests/components/wifi/common.yaml +++ b/tests/components/wifi/common.yaml @@ -10,6 +10,10 @@ esphome: - logger.log: "Connected to WiFi!" on_error: - logger.log: "Failed to connect to WiFi!" + - if: + condition: wifi.ap_active + then: + - logger.log: "WiFi AP is active!" wifi: networks: From d7a197b3a3444d996dcdd2b249ccaa7e88aa1421 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 24 Nov 2025 12:27:09 -0500 Subject: [PATCH 12/28] [esp32] Use the IDF I2C implementation on Arduino (#12076) --- esphome/components/i2c/__init__.py | 26 +++++++++++++--------- esphome/components/i2c/i2c_bus_arduino.cpp | 24 +++++--------------- esphome/components/i2c/i2c_bus_arduino.h | 7 +++--- esphome/components/i2c/i2c_bus_esp_idf.cpp | 4 ++-- esphome/components/i2c/i2c_bus_esp_idf.h | 4 ++-- 5 files changed, 27 insertions(+), 38 deletions(-) diff --git a/esphome/components/i2c/__init__.py b/esphome/components/i2c/__init__.py index 63089237590..738568cd3cb 100644 --- a/esphome/components/i2c/__init__.py +++ b/esphome/components/i2c/__init__.py @@ -47,18 +47,20 @@ MULTI_CONF = True def _bus_declare_type(value): + if CORE.is_esp32: + return cv.declare_id(IDFI2CBus)(value) if CORE.using_arduino: return cv.declare_id(ArduinoI2CBus)(value) - if CORE.using_esp_idf: - return cv.declare_id(IDFI2CBus)(value) if CORE.using_zephyr: return cv.declare_id(ZephyrI2CBus)(value) raise NotImplementedError def validate_config(config): - if CORE.using_esp_idf: - return cv.require_framework_version(esp_idf=cv.Version(5, 4, 2))(config) + if CORE.is_esp32: + return cv.require_framework_version( + esp_idf=cv.Version(5, 4, 2), esp32_arduino=cv.Version(3, 2, 1) + )(config) return config @@ -67,12 +69,12 @@ CONFIG_SCHEMA = cv.All( { cv.GenerateID(): _bus_declare_type, cv.Optional(CONF_SDA, default="SDA"): pins.internal_gpio_pin_number, - cv.SplitDefault(CONF_SDA_PULLUP_ENABLED, esp32_idf=True): cv.All( - cv.only_with_esp_idf, cv.boolean + cv.SplitDefault(CONF_SDA_PULLUP_ENABLED, esp32=True): cv.All( + cv.only_on_esp32, cv.boolean ), cv.Optional(CONF_SCL, default="SCL"): pins.internal_gpio_pin_number, - cv.SplitDefault(CONF_SCL_PULLUP_ENABLED, esp32_idf=True): cv.All( - cv.only_with_esp_idf, cv.boolean + cv.SplitDefault(CONF_SCL_PULLUP_ENABLED, esp32=True): cv.All( + cv.only_on_esp32, cv.boolean ), cv.SplitDefault( CONF_FREQUENCY, @@ -151,7 +153,7 @@ async def to_code(config): cg.add(var.set_scan(config[CONF_SCAN])) if CONF_TIMEOUT in config: cg.add(var.set_timeout(int(config[CONF_TIMEOUT].total_microseconds))) - if CORE.using_arduino: + if CORE.using_arduino and not CORE.is_esp32: cg.add_library("Wire", None) @@ -248,14 +250,16 @@ def final_validate_device_schema( FILTER_SOURCE_FILES = filter_source_files_from_platform( { "i2c_bus_arduino.cpp": { - PlatformFramework.ESP32_ARDUINO, PlatformFramework.ESP8266_ARDUINO, PlatformFramework.RP2040_ARDUINO, PlatformFramework.BK72XX_ARDUINO, PlatformFramework.RTL87XX_ARDUINO, PlatformFramework.LN882X_ARDUINO, }, - "i2c_bus_esp_idf.cpp": {PlatformFramework.ESP32_IDF}, + "i2c_bus_esp_idf.cpp": { + PlatformFramework.ESP32_ARDUINO, + PlatformFramework.ESP32_IDF, + }, "i2c_bus_zephyr.cpp": {PlatformFramework.NRF52_ZEPHYR}, } ) diff --git a/esphome/components/i2c/i2c_bus_arduino.cpp b/esphome/components/i2c/i2c_bus_arduino.cpp index 221423418bd..1579020c9be 100644 --- a/esphome/components/i2c/i2c_bus_arduino.cpp +++ b/esphome/components/i2c/i2c_bus_arduino.cpp @@ -1,4 +1,4 @@ -#ifdef USE_ARDUINO +#if defined(USE_ARDUINO) && !defined(USE_ESP32) #include "i2c_bus_arduino.h" #include @@ -15,16 +15,7 @@ static const char *const TAG = "i2c.arduino"; void ArduinoI2CBus::setup() { recover_(); -#if defined(USE_ESP32) - static uint8_t next_bus_num = 0; - if (next_bus_num == 0) { - wire_ = &Wire; - } else { - wire_ = new TwoWire(next_bus_num); // NOLINT(cppcoreguidelines-owning-memory) - } - this->port_ = next_bus_num; - next_bus_num++; -#elif defined(USE_ESP8266) +#if defined(USE_ESP8266) wire_ = new TwoWire(); // NOLINT(cppcoreguidelines-owning-memory) #elif defined(USE_RP2040) static bool first = true; @@ -54,10 +45,7 @@ void ArduinoI2CBus::set_pins_and_clock_() { wire_->begin(static_cast(sda_pin_), static_cast(scl_pin_)); #endif if (timeout_ > 0) { // if timeout specified in yaml -#if defined(USE_ESP32) - // https://github.com/espressif/arduino-esp32/blob/master/libraries/Wire/src/Wire.cpp - wire_->setTimeOut(timeout_ / 1000); // unit: ms -#elif defined(USE_ESP8266) +#if defined(USE_ESP8266) // https://github.com/esp8266/Arduino/blob/master/libraries/Wire/Wire.h wire_->setClockStretchLimit(timeout_); // unit: us #elif defined(USE_RP2040) @@ -76,9 +64,7 @@ void ArduinoI2CBus::dump_config() { " Frequency: %u Hz", this->sda_pin_, this->scl_pin_, this->frequency_); if (timeout_ > 0) { -#if defined(USE_ESP32) - ESP_LOGCONFIG(TAG, " Timeout: %u ms", this->timeout_ / 1000); -#elif defined(USE_ESP8266) +#if defined(USE_ESP8266) ESP_LOGCONFIG(TAG, " Timeout: %u us", this->timeout_); #elif defined(USE_RP2040) ESP_LOGCONFIG(TAG, " Timeout: %u ms", this->timeout_ / 1000); @@ -275,4 +261,4 @@ void ArduinoI2CBus::recover_() { } // namespace i2c } // namespace esphome -#endif // USE_ESP_IDF +#endif // defined(USE_ARDUINO) && !defined(USE_ESP32) diff --git a/esphome/components/i2c/i2c_bus_arduino.h b/esphome/components/i2c/i2c_bus_arduino.h index b4418283539..2d69e7684ca 100644 --- a/esphome/components/i2c/i2c_bus_arduino.h +++ b/esphome/components/i2c/i2c_bus_arduino.h @@ -1,6 +1,6 @@ #pragma once -#ifdef USE_ARDUINO +#if defined(USE_ARDUINO) && !defined(USE_ESP32) #include #include "esphome/core/component.h" @@ -29,7 +29,7 @@ class ArduinoI2CBus : public InternalI2CBus, public Component { void set_frequency(uint32_t frequency) { frequency_ = frequency; } void set_timeout(uint32_t timeout) { timeout_ = timeout; } - int get_port() const override { return this->port_; } + int get_port() const override { return 0; } private: void recover_(); @@ -37,7 +37,6 @@ class ArduinoI2CBus : public InternalI2CBus, public Component { RecoveryCode recovery_result_; protected: - int8_t port_{-1}; TwoWire *wire_; uint8_t sda_pin_; uint8_t scl_pin_; @@ -49,4 +48,4 @@ class ArduinoI2CBus : public InternalI2CBus, public Component { } // namespace i2c } // namespace esphome -#endif // USE_ARDUINO +#endif // defined(USE_ARDUINO) && !defined(USE_ESP32) diff --git a/esphome/components/i2c/i2c_bus_esp_idf.cpp b/esphome/components/i2c/i2c_bus_esp_idf.cpp index bf50ea0586e..c22db51c689 100644 --- a/esphome/components/i2c/i2c_bus_esp_idf.cpp +++ b/esphome/components/i2c/i2c_bus_esp_idf.cpp @@ -1,4 +1,4 @@ -#ifdef USE_ESP_IDF +#ifdef USE_ESP32 #include "i2c_bus_esp_idf.h" @@ -299,4 +299,4 @@ void IDFI2CBus::recover_() { } // namespace i2c } // namespace esphome -#endif // USE_ESP_IDF +#endif // USE_ESP32 diff --git a/esphome/components/i2c/i2c_bus_esp_idf.h b/esphome/components/i2c/i2c_bus_esp_idf.h index f565be45359..63fe8b701cf 100644 --- a/esphome/components/i2c/i2c_bus_esp_idf.h +++ b/esphome/components/i2c/i2c_bus_esp_idf.h @@ -1,6 +1,6 @@ #pragma once -#ifdef USE_ESP_IDF +#ifdef USE_ESP32 #include "esphome/core/component.h" #include "i2c_bus.h" @@ -53,4 +53,4 @@ class IDFI2CBus : public InternalI2CBus, public Component { } // namespace i2c } // namespace esphome -#endif // USE_ESP_IDF +#endif // USE_ESP32 From d7da55988552ca5a044e57b84a5c284763efb66c Mon Sep 17 00:00:00 2001 From: Sascha Ittner Date: Mon, 24 Nov 2025 18:31:26 +0100 Subject: [PATCH 13/28] [thermopro_ble] Add thermopro ble support (#11835) Co-authored-by: J. Nick Koston Co-authored-by: J. Nick Koston --- CODEOWNERS | 1 + esphome/components/thermopro_ble/__init__.py | 0 esphome/components/thermopro_ble/sensor.py | 97 +++++++++ .../thermopro_ble/thermopro_ble.cpp | 204 ++++++++++++++++++ .../components/thermopro_ble/thermopro_ble.h | 49 +++++ tests/components/thermopro_ble/common.yaml | 13 ++ .../thermopro_ble/test.esp32-idf.yaml | 4 + 7 files changed, 368 insertions(+) create mode 100644 esphome/components/thermopro_ble/__init__.py create mode 100644 esphome/components/thermopro_ble/sensor.py create mode 100644 esphome/components/thermopro_ble/thermopro_ble.cpp create mode 100644 esphome/components/thermopro_ble/thermopro_ble.h create mode 100644 tests/components/thermopro_ble/common.yaml create mode 100644 tests/components/thermopro_ble/test.esp32-idf.yaml diff --git a/CODEOWNERS b/CODEOWNERS index d6ec7b882e7..c6332e39335 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -484,6 +484,7 @@ esphome/components/template/datetime/* @rfdarter esphome/components/template/event/* @nohat esphome/components/template/fan/* @ssieb esphome/components/text/* @mauritskorse +esphome/components/thermopro_ble/* @sittner esphome/components/thermostat/* @kbx81 esphome/components/time/* @esphome/core esphome/components/tinyusb/* @kbx81 diff --git a/esphome/components/thermopro_ble/__init__.py b/esphome/components/thermopro_ble/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/esphome/components/thermopro_ble/sensor.py b/esphome/components/thermopro_ble/sensor.py new file mode 100644 index 00000000000..de63229621e --- /dev/null +++ b/esphome/components/thermopro_ble/sensor.py @@ -0,0 +1,97 @@ +import esphome.codegen as cg +from esphome.components import esp32_ble_tracker, sensor +import esphome.config_validation as cv +from esphome.const import ( + CONF_BATTERY_LEVEL, + CONF_EXTERNAL_TEMPERATURE, + CONF_HUMIDITY, + CONF_ID, + CONF_MAC_ADDRESS, + CONF_SIGNAL_STRENGTH, + CONF_TEMPERATURE, + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_SIGNAL_STRENGTH, + DEVICE_CLASS_TEMPERATURE, + ENTITY_CATEGORY_DIAGNOSTIC, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_DECIBEL_MILLIWATT, + UNIT_PERCENT, +) + +CODEOWNERS = ["@sittner"] + +DEPENDENCIES = ["esp32_ble_tracker"] + +thermopro_ble_ns = cg.esphome_ns.namespace("thermopro_ble") +ThermoProBLE = thermopro_ble_ns.class_( + "ThermoProBLE", esp32_ble_tracker.ESPBTDeviceListener, cg.Component +) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(ThermoProBLE), + cv.Required(CONF_MAC_ADDRESS): cv.mac_address, + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_EXTERNAL_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_BATTERY, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_SIGNAL_STRENGTH): sensor.sensor_schema( + unit_of_measurement=UNIT_DECIBEL_MILLIWATT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_SIGNAL_STRENGTH, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + } + ) + .extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA) + .extend(cv.COMPONENT_SCHEMA) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await esp32_ble_tracker.register_ble_device(var, config) + + cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) + + if temperature_config := config.get(CONF_TEMPERATURE): + sens = await sensor.new_sensor(temperature_config) + cg.add(var.set_temperature(sens)) + if external_temperature_config := config.get(CONF_EXTERNAL_TEMPERATURE): + sens = await sensor.new_sensor(external_temperature_config) + cg.add(var.set_external_temperature(sens)) + if humidity_config := config.get(CONF_HUMIDITY): + sens = await sensor.new_sensor(humidity_config) + cg.add(var.set_humidity(sens)) + if battery_level_config := config.get(CONF_BATTERY_LEVEL): + sens = await sensor.new_sensor(battery_level_config) + cg.add(var.set_battery_level(sens)) + if signal_strength_config := config.get(CONF_SIGNAL_STRENGTH): + sens = await sensor.new_sensor(signal_strength_config) + cg.add(var.set_signal_strength(sens)) diff --git a/esphome/components/thermopro_ble/thermopro_ble.cpp b/esphome/components/thermopro_ble/thermopro_ble.cpp new file mode 100644 index 00000000000..4b43c9b39e2 --- /dev/null +++ b/esphome/components/thermopro_ble/thermopro_ble.cpp @@ -0,0 +1,204 @@ +#include "thermopro_ble.h" +#include "esphome/core/log.h" + +#ifdef USE_ESP32 + +namespace esphome::thermopro_ble { + +// this size must be large enough to hold the largest data frame +// of all supported devices +static constexpr std::size_t MAX_DATA_SIZE = 24; + +struct DeviceParserMapping { + const char *prefix; + DeviceParser parser; +}; + +static float tp96_battery(uint16_t voltage); + +static optional parse_tp972(const uint8_t *data, std::size_t data_size); +static optional parse_tp96(const uint8_t *data, std::size_t data_size); +static optional parse_tp3(const uint8_t *data, std::size_t data_size); + +static const char *const TAG = "thermopro_ble"; + +static const struct DeviceParserMapping DEVICE_PARSER_MAP[] = { + {"TP972", parse_tp972}, {"TP970", parse_tp96}, {"TP96", parse_tp96}, {"TP3", parse_tp3}}; + +void ThermoProBLE::dump_config() { + ESP_LOGCONFIG(TAG, "ThermoPro BLE"); + LOG_SENSOR(" ", "Temperature", this->temperature_); + LOG_SENSOR(" ", "External temperature", this->external_temperature_); + LOG_SENSOR(" ", "Humidity", this->humidity_); + LOG_SENSOR(" ", "Battery Level", this->battery_level_); +} + +bool ThermoProBLE::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { + // check for matching mac address + if (device.address_uint64() != this->address_) { + ESP_LOGVV(TAG, "parse_device(): unknown MAC address."); + return false; + } + + // check for valid device type + update_device_type_(device.get_name()); + if (this->device_parser_ == nullptr) { + ESP_LOGVV(TAG, "parse_device(): invalid device type."); + return false; + } + + ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", device.address_str().c_str()); + + // publish signal strength + float signal_strength = float(device.get_rssi()); + if (this->signal_strength_ != nullptr) + this->signal_strength_->publish_state(signal_strength); + + bool success = false; + for (auto &service_data : device.get_manufacturer_datas()) { + // check maximum data size + std::size_t data_size = service_data.data.size() + 2; + if (data_size > MAX_DATA_SIZE) { + ESP_LOGVV(TAG, "parse_device(): maximum data size exceeded!"); + continue; + } + + // reconstruct whole record from 2 byte uuid and data + esp_bt_uuid_t uuid = service_data.uuid.get_uuid(); + uint8_t data[MAX_DATA_SIZE] = {static_cast(uuid.uuid.uuid16), static_cast(uuid.uuid.uuid16 >> 8)}; + std::copy(service_data.data.begin(), service_data.data.end(), std::begin(data) + 2); + + // dispatch data to parser + optional result = this->device_parser_(data, data_size); + if (!result.has_value()) { + continue; + } + + // publish sensor values + if (result->temperature.has_value() && this->temperature_ != nullptr) + this->temperature_->publish_state(*result->temperature); + if (result->external_temperature.has_value() && this->external_temperature_ != nullptr) + this->external_temperature_->publish_state(*result->external_temperature); + if (result->humidity.has_value() && this->humidity_ != nullptr) + this->humidity_->publish_state(*result->humidity); + if (result->battery_level.has_value() && this->battery_level_ != nullptr) + this->battery_level_->publish_state(*result->battery_level); + + success = true; + } + + return success; +} + +void ThermoProBLE::update_device_type_(const std::string &device_name) { + // check for changed device name (should only happen on initial call) + if (this->device_name_ == device_name) { + return; + } + + // remember device name + this->device_name_ = device_name; + + // try to find device parser + for (const auto &mapping : DEVICE_PARSER_MAP) { + if (device_name.starts_with(mapping.prefix)) { + this->device_parser_ = mapping.parser; + return; + } + } + + // device type unknown + this->device_parser_ = nullptr; + ESP_LOGVV(TAG, "update_device_type_(): unknown device type %s.", device_name.c_str()); +} + +static inline uint16_t read_uint16(const uint8_t *data, std::size_t offset) { + return static_cast(data[offset + 0]) | (static_cast(data[offset + 1]) << 8); +} + +static inline int16_t read_int16(const uint8_t *data, std::size_t offset) { + return static_cast(read_uint16(data, offset)); +} + +static inline uint32_t read_uint32(const uint8_t *data, std::size_t offset) { + return static_cast(data[offset + 0]) | (static_cast(data[offset + 1]) << 8) | + (static_cast(data[offset + 2]) << 16) | (static_cast(data[offset + 3]) << 24); +} + +// Battery calculation used with permission from: +// https://github.com/Bluetooth-Devices/thermopro-ble/blob/main/src/thermopro_ble/parser.py +// +// TP96x battery values appear to be a voltage reading, probably in millivolts. +// This means that calculating battery life from it is a non-linear function. +// Examining the curve, it looked fairly close to a curve from the tanh function. +// So, I created a script to use Tensorflow to optimize an equation in the format +// A*tanh(B*x+C)+D +// Where A,B,C,D are the variables to optimize for. This yielded the below function +static float tp96_battery(uint16_t voltage) { + float level = 52.317286f * tanh(static_cast(voltage) / 273.624277936f - 8.76485439394f) + 51.06925f; + return std::max(0.0f, std::min(level, 100.0f)); +} + +static optional parse_tp972(const uint8_t *data, std::size_t data_size) { + if (data_size != 23) { + ESP_LOGVV(TAG, "parse_tp972(): payload has wrong size of %d (!= 23)!", data_size); + return {}; + } + + ParseResult result; + + // ambient temperature, 2 bytes, 16-bit unsigned integer, -54 °C offset + result.external_temperature = static_cast(read_uint16(data, 1)) - 54.0f; + + // battery level, 2 bytes, 16-bit unsigned integer, voltage (convert to percentage) + result.battery_level = tp96_battery(read_uint16(data, 3)); + + // internal temperature, 4 bytes, float, -54 °C offset + result.temperature = static_cast(read_uint32(data, 9)) - 54.0f; + + return result; +} + +static optional parse_tp96(const uint8_t *data, std::size_t data_size) { + if (data_size != 7) { + ESP_LOGVV(TAG, "parse_tp96(): payload has wrong size of %d (!= 7)!", data_size); + return {}; + } + + ParseResult result; + + // internal temperature, 2 bytes, 16-bit unsigned integer, -30 °C offset + result.temperature = static_cast(read_uint16(data, 1)) - 30.0f; + + // battery level, 2 bytes, 16-bit unsigned integer, voltage (convert to percentage) + result.battery_level = tp96_battery(read_uint16(data, 3)); + + // ambient temperature, 2 bytes, 16-bit unsigned integer, -30 °C offset + result.external_temperature = static_cast(read_uint16(data, 5)) - 30.0f; + + return result; +} + +static optional parse_tp3(const uint8_t *data, std::size_t data_size) { + if (data_size < 6) { + ESP_LOGVV(TAG, "parse_tp3(): payload has wrong size of %d (< 6)!", data_size); + return {}; + } + + ParseResult result; + + // temperature, 2 bytes, 16-bit signed integer, 0.1 °C + result.temperature = static_cast(read_int16(data, 1)) * 0.1f; + + // humidity, 1 byte, 8-bit unsigned integer, 1.0 % + result.humidity = static_cast(data[3]); + + // battery level, 2 bits (0-2) + result.battery_level = static_cast(data[4] & 0x3) * 50.0; + + return result; +} + +} // namespace esphome::thermopro_ble + +#endif diff --git a/esphome/components/thermopro_ble/thermopro_ble.h b/esphome/components/thermopro_ble/thermopro_ble.h new file mode 100644 index 00000000000..38bed821025 --- /dev/null +++ b/esphome/components/thermopro_ble/thermopro_ble.h @@ -0,0 +1,49 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" + +#ifdef USE_ESP32 + +namespace esphome::thermopro_ble { + +struct ParseResult { + optional temperature; + optional external_temperature; + optional humidity; + optional battery_level; +}; + +using DeviceParser = optional (*)(const uint8_t *data, std::size_t data_size); + +class ThermoProBLE : public Component, public esp32_ble_tracker::ESPBTDeviceListener { + public: + void set_address(uint64_t address) { this->address_ = address; }; + + bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; + void dump_config() override; + void set_signal_strength(sensor::Sensor *signal_strength) { this->signal_strength_ = signal_strength; } + void set_temperature(sensor::Sensor *temperature) { this->temperature_ = temperature; } + void set_external_temperature(sensor::Sensor *external_temperature) { + this->external_temperature_ = external_temperature; + } + void set_humidity(sensor::Sensor *humidity) { this->humidity_ = humidity; } + void set_battery_level(sensor::Sensor *battery_level) { this->battery_level_ = battery_level; } + + protected: + uint64_t address_; + std::string device_name_; + DeviceParser device_parser_{nullptr}; + sensor::Sensor *signal_strength_{nullptr}; + sensor::Sensor *temperature_{nullptr}; + sensor::Sensor *external_temperature_{nullptr}; + sensor::Sensor *humidity_{nullptr}; + sensor::Sensor *battery_level_{nullptr}; + + void update_device_type_(const std::string &device_name); +}; + +} // namespace esphome::thermopro_ble + +#endif diff --git a/tests/components/thermopro_ble/common.yaml b/tests/components/thermopro_ble/common.yaml new file mode 100644 index 00000000000..297725e1c3a --- /dev/null +++ b/tests/components/thermopro_ble/common.yaml @@ -0,0 +1,13 @@ +esp32_ble_tracker: + +sensor: + - platform: thermopro_ble + mac_address: FE:74:B8:6A:97:B7 + temperature: + name: "ThermoPro Temperature" + humidity: + name: "ThermoPro Humidity" + battery_level: + name: "ThermoPro Battery Level" + signal_strength: + name: "ThermoPro Signal Strength" diff --git a/tests/components/thermopro_ble/test.esp32-idf.yaml b/tests/components/thermopro_ble/test.esp32-idf.yaml new file mode 100644 index 00000000000..7a6541ae76f --- /dev/null +++ b/tests/components/thermopro_ble/test.esp32-idf.yaml @@ -0,0 +1,4 @@ +packages: + ble: !include ../../test_build_components/common/ble/esp32-idf.yaml + +<<: !include common.yaml From b820e676161295d7d135128ff38a99016ecb0e5e Mon Sep 17 00:00:00 2001 From: Jordan Zucker Date: Mon, 24 Nov 2025 09:42:07 -0800 Subject: [PATCH 14/28] [prometheus] Add event and text base components metrics (#10240) Co-authored-by: Jordan Zucker Co-authored-by: J. Nick Koston --- .../prometheus/prometheus_handler.cpp | 106 ++++++++++++++++++ .../prometheus/prometheus_handler.h | 16 +++ tests/components/prometheus/common.yaml | 28 +++++ 3 files changed, 150 insertions(+) diff --git a/esphome/components/prometheus/prometheus_handler.cpp b/esphome/components/prometheus/prometheus_handler.cpp index 5cfcacf0cbb..6b57a3f7182 100644 --- a/esphome/components/prometheus/prometheus_handler.cpp +++ b/esphome/components/prometheus/prometheus_handler.cpp @@ -53,6 +53,18 @@ void PrometheusHandler::handleRequest(AsyncWebServerRequest *req) { this->lock_row_(stream, obj, area, node, friendly_name); #endif +#ifdef USE_EVENT + this->event_type_(stream); + for (auto *obj : App.get_events()) + this->event_row_(stream, obj, area, node, friendly_name); +#endif + +#ifdef USE_TEXT + this->text_type_(stream); + for (auto *obj : App.get_texts()) + this->text_row_(stream, obj, area, node, friendly_name); +#endif + #ifdef USE_TEXT_SENSOR this->text_sensor_type_(stream); for (auto *obj : App.get_text_sensors()) @@ -547,6 +559,100 @@ void PrometheusHandler::text_sensor_row_(AsyncResponseStream *stream, text_senso } #endif +// Type-specific implementation +#ifdef USE_TEXT +void PrometheusHandler::text_type_(AsyncResponseStream *stream) { + stream->print(ESPHOME_F("#TYPE esphome_text_value gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_text_failed gauge\n")); +} +void PrometheusHandler::text_row_(AsyncResponseStream *stream, text::Text *obj, std::string &area, std::string &node, + std::string &friendly_name) { + if (obj->is_internal() && !this->include_internal_) + return; + if (obj->has_state()) { + // We have a valid value, output this value + stream->print(ESPHOME_F("esphome_text_failed{id=\"")); + stream->print(relabel_id_(obj).c_str()); + add_area_label_(stream, area); + add_node_label_(stream, node); + add_friendly_name_label_(stream, friendly_name); + stream->print(ESPHOME_F("\",name=\"")); + stream->print(relabel_name_(obj).c_str()); + stream->print(ESPHOME_F("\"} 0\n")); + // Data itself + stream->print(ESPHOME_F("esphome_text_value{id=\"")); + stream->print(relabel_id_(obj).c_str()); + add_area_label_(stream, area); + add_node_label_(stream, node); + add_friendly_name_label_(stream, friendly_name); + stream->print(ESPHOME_F("\",name=\"")); + stream->print(relabel_name_(obj).c_str()); + stream->print(ESPHOME_F("\",value=\"")); + stream->print(obj->state.c_str()); + stream->print(ESPHOME_F("\"} ")); + stream->print(ESPHOME_F("1.0")); + stream->print(ESPHOME_F("\n")); + } else { + // Invalid state + stream->print(ESPHOME_F("esphome_text_failed{id=\"")); + stream->print(relabel_id_(obj).c_str()); + add_area_label_(stream, area); + add_node_label_(stream, node); + add_friendly_name_label_(stream, friendly_name); + stream->print(ESPHOME_F("\",name=\"")); + stream->print(relabel_name_(obj).c_str()); + stream->print(ESPHOME_F("\"} 1\n")); + } +} +#endif + +// Type-specific implementation +#ifdef USE_EVENT +void PrometheusHandler::event_type_(AsyncResponseStream *stream) { + stream->print(ESPHOME_F("#TYPE esphome_event_value gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_event_failed gauge\n")); +} +void PrometheusHandler::event_row_(AsyncResponseStream *stream, event::Event *obj, std::string &area, std::string &node, + std::string &friendly_name) { + if (obj->is_internal() && !this->include_internal_) + return; + if (obj->get_last_event_type() != nullptr) { + // We have a valid event type, output this value + stream->print(ESPHOME_F("esphome_event_failed{id=\"")); + stream->print(relabel_id_(obj).c_str()); + add_area_label_(stream, area); + add_node_label_(stream, node); + add_friendly_name_label_(stream, friendly_name); + stream->print(ESPHOME_F("\",name=\"")); + stream->print(relabel_name_(obj).c_str()); + stream->print(ESPHOME_F("\"} 0\n")); + // Data itself + stream->print(ESPHOME_F("esphome_event_value{id=\"")); + stream->print(relabel_id_(obj).c_str()); + add_area_label_(stream, area); + add_node_label_(stream, node); + add_friendly_name_label_(stream, friendly_name); + stream->print(ESPHOME_F("\",name=\"")); + stream->print(relabel_name_(obj).c_str()); + stream->print(ESPHOME_F("\",last_event_type=\"")); + stream->print(obj->get_last_event_type()); + stream->print(ESPHOME_F("\"} ")); + stream->print(ESPHOME_F("1.0")); + stream->print(ESPHOME_F("\n")); + } else { + // No event triggered yet + stream->print(ESPHOME_F("esphome_event_failed{id=\"")); + stream->print(relabel_id_(obj).c_str()); + add_area_label_(stream, area); + add_node_label_(stream, node); + add_friendly_name_label_(stream, friendly_name); + stream->print(ESPHOME_F("\",name=\"")); + stream->print(relabel_name_(obj).c_str()); + stream->print(ESPHOME_F("\"} 1\n")); + } +} +#endif + // Type-specific implementation #ifdef USE_NUMBER void PrometheusHandler::number_type_(AsyncResponseStream *stream) { diff --git a/esphome/components/prometheus/prometheus_handler.h b/esphome/components/prometheus/prometheus_handler.h index c4598f44b0d..45cc81b899c 100644 --- a/esphome/components/prometheus/prometheus_handler.h +++ b/esphome/components/prometheus/prometheus_handler.h @@ -123,6 +123,22 @@ class PrometheusHandler : public AsyncWebHandler, public Component { std::string &friendly_name); #endif +#ifdef USE_EVENT + /// Return the type for prometheus + void event_type_(AsyncResponseStream *stream); + /// Return the event values state as prometheus data point + void event_row_(AsyncResponseStream *stream, event::Event *obj, std::string &area, std::string &node, + std::string &friendly_name); +#endif + +#ifdef USE_TEXT + /// Return the type for prometheus + void text_type_(AsyncResponseStream *stream); + /// Return the text values state as prometheus data point + void text_row_(AsyncResponseStream *stream, text::Text *obj, std::string &area, std::string &node, + std::string &friendly_name); +#endif + #ifdef USE_TEXT_SENSOR /// Return the type for prometheus void text_sensor_type_(AsyncResponseStream *stream); diff --git a/tests/components/prometheus/common.yaml b/tests/components/prometheus/common.yaml index cf46e882a76..0b90d614ddb 100644 --- a/tests/components/prometheus/common.yaml +++ b/tests/components/prometheus/common.yaml @@ -39,6 +39,15 @@ sensor: return 0.0; update_interval: 60s +text: + - platform: template + name: "Template text" + optimistic: true + min_length: 0 + max_length: 100 + mode: text + initial_value: "Hello World" + text_sensor: - platform: version name: "ESPHome Version" @@ -52,6 +61,25 @@ text_sensor: return {"Goodbye (cruel) World"}; update_interval: 60s +event: + - platform: template + name: "Template Event" + id: template_event1 + event_types: + - "custom_event_1" + - "custom_event_2" + +button: + - platform: template + name: "Template Event Button" + on_press: + - logger.log: "Template Event Button pressed" + - lambda: |- + ESP_LOGD("template_event_button", "Template Event Button pressed"); + - event.trigger: + id: template_event1 + event_type: custom_event_1 + binary_sensor: - platform: template id: template_binary_sensor1 From 09f3f6219493ec28ed12fe3495e7c85c608620e1 Mon Sep 17 00:00:00 2001 From: Flo Date: Mon, 24 Nov 2025 18:49:16 +0100 Subject: [PATCH 15/28] [api] Connected Condition - state_subscription_only flag (#11906) 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/components/api/__init__.py | 20 ++++++++++++++++++-- esphome/components/api/api_server.cpp | 13 ++++++++++++- esphome/components/api/api_server.h | 7 +++++-- tests/components/api/common-base.yaml | 4 ++++ 4 files changed, 39 insertions(+), 5 deletions(-) diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index 7f84f2f2472..2910643dfb7 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -85,6 +85,7 @@ CONF_HOMEASSISTANT_SERVICES = "homeassistant_services" CONF_HOMEASSISTANT_STATES = "homeassistant_states" CONF_LISTEN_BACKLOG = "listen_backlog" CONF_MAX_SEND_QUEUE = "max_send_queue" +CONF_STATE_SUBSCRIPTION_ONLY = "state_subscription_only" def validate_encryption_key(value): @@ -537,9 +538,24 @@ async def homeassistant_tag_scanned_to_code(config, action_id, template_arg, arg return var -@automation.register_condition("api.connected", APIConnectedCondition, {}) +API_CONNECTED_CONDITION_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.use_id(APIServer), + cv.Optional(CONF_STATE_SUBSCRIPTION_ONLY, default=False): cv.templatable( + cv.boolean + ), + } +) + + +@automation.register_condition( + "api.connected", APIConnectedCondition, API_CONNECTED_CONDITION_SCHEMA +) async def api_connected_to_code(config, condition_id, template_arg, args): - return cg.new_Pvariable(condition_id, template_arg) + var = cg.new_Pvariable(condition_id, template_arg) + templ = await cg.templatable(config[CONF_STATE_SUBSCRIPTION_ONLY], args, cg.bool_) + cg.add(var.set_state_subscription_only(templ)) + return var def FILTER_SOURCE_FILES() -> list[str]: diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 18601d74ff4..d33c98abc90 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -528,7 +528,18 @@ void APIServer::request_time() { } #endif -bool APIServer::is_connected() const { return !this->clients_.empty(); } +bool APIServer::is_connected(bool state_subscription_only) const { + if (!state_subscription_only) { + return !this->clients_.empty(); + } + + for (const auto &client : this->clients_) { + if (client->flags_.state_subscription) { + return true; + } + } + return false; +} void APIServer::on_shutdown() { this->shutting_down_ = true; diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index a3a082e1651..786cd63f448 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -150,7 +150,7 @@ class APIServer : public Component, public Controller { void on_zwave_proxy_request(const esphome::api::ProtoMessage &msg); #endif - bool is_connected() const; + bool is_connected(bool state_subscription_only = false) const; #ifdef USE_API_HOMEASSISTANT_STATES struct HomeAssistantStateSubscription { @@ -236,8 +236,11 @@ class APIServer : public Component, public Controller { extern APIServer *global_api_server; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) template class APIConnectedCondition : public Condition { + TEMPLATABLE_VALUE(bool, state_subscription_only) public: - bool check(const Ts &...x) override { return global_api_server->is_connected(); } + bool check(const Ts &...x) override { + return global_api_server->is_connected(this->state_subscription_only_.value(x...)); + } }; } // namespace esphome::api diff --git a/tests/components/api/common-base.yaml b/tests/components/api/common-base.yaml index fc53b8ac7e0..0416cebf9b5 100644 --- a/tests/components/api/common-base.yaml +++ b/tests/components/api/common-base.yaml @@ -1,6 +1,10 @@ esphome: on_boot: then: + - wait_until: + condition: + api.connected: + state_subscription_only: true - homeassistant.event: event: esphome.button_pressed data: From c888becfa7369396c96fd1d4e8f807ebde57cc7b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 24 Nov 2025 11:52:15 -0600 Subject: [PATCH 16/28] [api] Optimize APINoiseContext memory usage by removing shared_ptr overhead (#11981) --- esphome/components/api/api_connection.cpp | 4 ++-- esphome/components/api/api_frame_helper_noise.cpp | 2 +- esphome/components/api/api_frame_helper_noise.h | 9 ++++----- esphome/components/api/api_server.cpp | 6 +++--- esphome/components/api/api_server.h | 6 +++--- esphome/components/mdns/mdns_component.cpp | 2 +- esphome/components/mqtt/mqtt_client.cpp | 2 +- 7 files changed, 15 insertions(+), 16 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 04221a237b3..ebfc6415377 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -90,8 +90,8 @@ static const int CAMERA_STOP_STREAM = 5000; APIConnection::APIConnection(std::unique_ptr sock, APIServer *parent) : parent_(parent), initial_state_iterator_(this), list_entities_iterator_(this) { #if defined(USE_API_PLAINTEXT) && defined(USE_API_NOISE) - auto noise_ctx = parent->get_noise_ctx(); - if (noise_ctx->has_psk()) { + auto &noise_ctx = parent->get_noise_ctx(); + if (noise_ctx.has_psk()) { this->helper_ = std::unique_ptr{new APINoiseFrameHelper(std::move(sock), noise_ctx, &this->client_info_)}; } else { diff --git a/esphome/components/api/api_frame_helper_noise.cpp b/esphome/components/api/api_frame_helper_noise.cpp index 8bcec0f9f32..f1028fa2991 100644 --- a/esphome/components/api/api_frame_helper_noise.cpp +++ b/esphome/components/api/api_frame_helper_noise.cpp @@ -528,7 +528,7 @@ APIError APINoiseFrameHelper::init_handshake_() { if (aerr != APIError::OK) return aerr; - const auto &psk = ctx_->get_psk(); + const auto &psk = this->ctx_.get_psk(); err = noise_handshakestate_set_pre_shared_key(handshake_, psk.data(), psk.size()); aerr = handle_noise_error_(err, LOG_STR("noise_handshakestate_set_pre_shared_key"), APIError::HANDSHAKESTATE_SETUP_FAILED); diff --git a/esphome/components/api/api_frame_helper_noise.h b/esphome/components/api/api_frame_helper_noise.h index e3243e4fa5c..7eb01058db4 100644 --- a/esphome/components/api/api_frame_helper_noise.h +++ b/esphome/components/api/api_frame_helper_noise.h @@ -9,9 +9,8 @@ namespace esphome::api { class APINoiseFrameHelper final : public APIFrameHelper { public: - APINoiseFrameHelper(std::unique_ptr socket, std::shared_ptr ctx, - const ClientInfo *client_info) - : APIFrameHelper(std::move(socket), client_info), ctx_(std::move(ctx)) { + APINoiseFrameHelper(std::unique_ptr socket, APINoiseContext &ctx, const ClientInfo *client_info) + : APIFrameHelper(std::move(socket), client_info), ctx_(ctx) { // Noise header structure: // Pos 0: indicator (0x01) // Pos 1-2: encrypted payload size (16-bit big-endian) @@ -41,8 +40,8 @@ class APINoiseFrameHelper final : public APIFrameHelper { NoiseCipherState *send_cipher_{nullptr}; NoiseCipherState *recv_cipher_{nullptr}; - // Shared pointer (8 bytes on 32-bit = 4 bytes control block pointer + 4 bytes object pointer) - std::shared_ptr ctx_; + // Reference to noise context (4 bytes on 32-bit) + APINoiseContext &ctx_; // Vector (12 bytes on 32-bit) std::vector prologue_; diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index d33c98abc90..64f8751c35a 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -227,8 +227,8 @@ void APIServer::dump_config() { " Max connections: %u", network::get_use_address(), this->port_, this->listen_backlog_, this->max_connections_); #ifdef USE_API_NOISE - ESP_LOGCONFIG(TAG, " Noise encryption: %s", YESNO(this->noise_ctx_->has_psk())); - if (!this->noise_ctx_->has_psk()) { + ESP_LOGCONFIG(TAG, " Noise encryption: %s", YESNO(this->noise_ctx_.has_psk())); + if (!this->noise_ctx_.has_psk()) { ESP_LOGCONFIG(TAG, " Supports encryption: YES"); } #else @@ -493,7 +493,7 @@ bool APIServer::save_noise_psk(psk_t psk, bool make_active) { ESP_LOGW(TAG, "Key set in YAML"); return false; #else - auto &old_psk = this->noise_ctx_->get_psk(); + auto &old_psk = this->noise_ctx_.get_psk(); if (std::equal(old_psk.begin(), old_psk.end(), psk.begin())) { ESP_LOGW(TAG, "New PSK matches old"); return true; diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index 786cd63f448..428429418ae 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -54,8 +54,8 @@ class APIServer : public Component, public Controller { #ifdef USE_API_NOISE bool save_noise_psk(psk_t psk, bool make_active = true); bool clear_noise_psk(bool make_active = true); - void set_noise_psk(psk_t psk) { noise_ctx_->set_psk(psk); } - std::shared_ptr get_noise_ctx() { return noise_ctx_; } + void set_noise_psk(psk_t psk) { this->noise_ctx_.set_psk(psk); } + APINoiseContext &get_noise_ctx() { return this->noise_ctx_; } #endif // USE_API_NOISE void handle_disconnect(APIConnection *conn); @@ -228,7 +228,7 @@ class APIServer : public Component, public Controller { // 7 bytes used, 1 byte padding #ifdef USE_API_NOISE - std::shared_ptr noise_ctx_ = std::make_shared(); + APINoiseContext noise_ctx_; ESPPreferenceObject noise_pref_; #endif // USE_API_NOISE }; diff --git a/esphome/components/mdns/mdns_component.cpp b/esphome/components/mdns/mdns_component.cpp index c81defd19fe..4655907983a 100644 --- a/esphome/components/mdns/mdns_component.cpp +++ b/esphome/components/mdns/mdns_component.cpp @@ -118,7 +118,7 @@ void MDNSComponent::compile_records_(StaticVectorget_noise_ctx()->has_psk(); + bool has_psk = api::global_api_server->get_noise_ctx().has_psk(); const char *encryption_key = has_psk ? TXT_API_ENCRYPTION : TXT_API_ENCRYPTION_SUPPORTED; txt_records.push_back({MDNS_STR(encryption_key), MDNS_STR(NOISE_ENCRYPTION)}); #endif diff --git a/esphome/components/mqtt/mqtt_client.cpp b/esphome/components/mqtt/mqtt_client.cpp index 9055b4421ef..a810d98adf9 100644 --- a/esphome/components/mqtt/mqtt_client.cpp +++ b/esphome/components/mqtt/mqtt_client.cpp @@ -140,7 +140,7 @@ void MQTTClientComponent::send_device_info_() { #endif #ifdef USE_API_NOISE - root[api::global_api_server->get_noise_ctx()->has_psk() ? "api_encryption" : "api_encryption_supported"] = + root[api::global_api_server->get_noise_ctx().has_psk() ? "api_encryption" : "api_encryption_supported"] = "Noise_NNpsk0_25519_ChaChaPoly_SHA256"; #endif }, From c146d924255eebc958e4f5b8fc5706c2021af494 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 24 Nov 2025 11:53:42 -0600 Subject: [PATCH 17/28] [api] Remove redundant socket pointer from APIFrameHelper (#11985) --- esphome/components/api/api_frame_helper.h | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/esphome/components/api/api_frame_helper.h b/esphome/components/api/api_frame_helper.h index 9aaada3cf74..d931a6e3a95 100644 --- a/esphome/components/api/api_frame_helper.h +++ b/esphome/components/api/api_frame_helper.h @@ -84,9 +84,7 @@ class APIFrameHelper { public: APIFrameHelper() = default; explicit APIFrameHelper(std::unique_ptr socket, const ClientInfo *client_info) - : socket_owned_(std::move(socket)), client_info_(client_info) { - socket_ = socket_owned_.get(); - } + : socket_(std::move(socket)), client_info_(client_info) {} virtual ~APIFrameHelper() = default; virtual APIError init() = 0; virtual APIError loop(); @@ -149,9 +147,8 @@ class APIFrameHelper { APIError write_raw_(const struct iovec *iov, int iovcnt, socket::Socket *socket, std::vector &tx_buf, const std::string &info, StateEnum &state, StateEnum failed_state); - // Pointers first (4 bytes each) - socket::Socket *socket_{nullptr}; - std::unique_ptr socket_owned_; + // Socket ownership (4 bytes on 32-bit, 8 bytes on 64-bit) + std::unique_ptr socket_; // Common state enum for all frame helpers // Note: Not all states are used by all implementations From d1a1bb446b9014ff4e591580102dfc07931099d9 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Mon, 24 Nov 2025 12:55:04 -0500 Subject: [PATCH 18/28] [wifi] Add runtime power saving mode control (#11478) Co-authored-by: J. Nick Koston --- esphome/components/wifi/__init__.py | 18 ++++- esphome/components/wifi/wifi_component.cpp | 90 +++++++++++++++++++++- esphome/components/wifi/wifi_component.h | 42 ++++++++++ esphome/core/defines.h | 1 + tests/components/wifi/test.esp32-idf.yaml | 11 +++ 5 files changed, 160 insertions(+), 2 deletions(-) diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index b9c0fa28a75..8a5e5329f13 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -607,6 +607,7 @@ async def wifi_disable_to_code(config, action_id, template_arg, args): KEEP_SCAN_RESULTS_KEY = "wifi_keep_scan_results" +RUNTIME_POWER_SAVE_KEY = "wifi_runtime_power_save" def request_wifi_scan_results(): @@ -619,13 +620,28 @@ def request_wifi_scan_results(): CORE.data[KEEP_SCAN_RESULTS_KEY] = True +def enable_runtime_power_save_control(): + """Enable runtime WiFi power save control. + + Components that need to dynamically switch WiFi power saving on/off for latency + performance (e.g., audio streaming, large data transfers) should call this + function during their code generation. This enables the request_high_performance() + and release_high_performance() APIs. + + Only supported on ESP32. + """ + CORE.data[RUNTIME_POWER_SAVE_KEY] = True + + @coroutine_with_priority(CoroPriority.FINAL) async def final_step(): - """Final code generation step to configure scan result retention.""" + """Final code generation step to configure optional WiFi features.""" if CORE.data.get(KEEP_SCAN_RESULTS_KEY, False): cg.add( cg.RawExpression("wifi::global_wifi_component->set_keep_scan_results(true)") ) + if CORE.data.get(RUNTIME_POWER_SAVE_KEY, False): + cg.add_define("USE_WIFI_RUNTIME_POWER_SAVE") @automation.register_action( diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 23a40204536..41931a77857 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -330,6 +330,19 @@ float WiFiComponent::get_setup_priority() const { return setup_priority::WIFI; } void WiFiComponent::setup() { this->wifi_pre_setup_(); + +#if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE) + // Create semaphore for high-performance mode requests + // Start at 0, increment on request, decrement on release + this->high_performance_semaphore_ = xSemaphoreCreateCounting(UINT32_MAX, 0); + if (this->high_performance_semaphore_ == nullptr) { + ESP_LOGE(TAG, "Failed semaphore"); + } + + // Store the configured power save mode as baseline + this->configured_power_save_ = this->power_save_; +#endif + if (this->enable_on_boot_) { this->start(); } else { @@ -371,6 +384,19 @@ void WiFiComponent::start() { ESP_LOGV(TAG, "Setting Output Power Option failed"); } +#if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE) + // Synchronize power_save_ with semaphore state before applying + if (this->high_performance_semaphore_ != nullptr) { + UBaseType_t semaphore_count = uxSemaphoreGetCount(this->high_performance_semaphore_); + if (semaphore_count > 0) { + this->power_save_ = WIFI_POWER_SAVE_NONE; + this->is_high_performance_mode_ = true; + } else { + this->power_save_ = this->configured_power_save_; + this->is_high_performance_mode_ = false; + } + } +#endif if (!this->wifi_apply_power_save_()) { ESP_LOGV(TAG, "Setting Power Save Option failed"); } @@ -525,6 +551,31 @@ void WiFiComponent::loop() { } } } + +#if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE) + // Check if power save mode needs to be updated based on high-performance requests + if (this->high_performance_semaphore_ != nullptr) { + // Semaphore count directly represents active requests (starts at 0, increments on request) + UBaseType_t semaphore_count = uxSemaphoreGetCount(this->high_performance_semaphore_); + + if (semaphore_count > 0 && !this->is_high_performance_mode_) { + // Transition to high-performance mode (no power save) + ESP_LOGV(TAG, "Switching to high-performance mode (%" PRIu32 " active %s)", (uint32_t) semaphore_count, + semaphore_count == 1 ? "request" : "requests"); + this->power_save_ = WIFI_POWER_SAVE_NONE; + if (this->wifi_apply_power_save_()) { + this->is_high_performance_mode_ = true; + } + } else if (semaphore_count == 0 && this->is_high_performance_mode_) { + // Restore to configured power save mode + ESP_LOGV(TAG, "Restoring power save mode to configured setting"); + this->power_save_ = this->configured_power_save_; + if (this->wifi_apply_power_save_()) { + this->is_high_performance_mode_ = false; + } + } + } +#endif } WiFiComponent::WiFiComponent() { global_wifi_component = this; } @@ -1567,7 +1618,12 @@ bool WiFiComponent::is_connected() { return this->state_ == WIFI_COMPONENT_STATE_STA_CONNECTED && this->wifi_sta_connect_status_() == WiFiSTAConnectStatus::CONNECTED && !this->error_from_callback_; } -void WiFiComponent::set_power_save_mode(WiFiPowerSaveMode power_save) { this->power_save_ = power_save; } +void WiFiComponent::set_power_save_mode(WiFiPowerSaveMode power_save) { + this->power_save_ = power_save; +#if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE) + this->configured_power_save_ = power_save; +#endif +} void WiFiComponent::set_passive_scan(bool passive) { this->passive_scan_ = passive; } @@ -1586,6 +1642,38 @@ bool WiFiComponent::is_esp32_improv_active_() { #endif } +#if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE) +bool WiFiComponent::request_high_performance() { + // Already configured for high performance - request satisfied + if (this->configured_power_save_ == WIFI_POWER_SAVE_NONE) { + return true; + } + + // Semaphore initialization failed + if (this->high_performance_semaphore_ == nullptr) { + return false; + } + + // Give the semaphore (non-blocking). This increments the count. + return xSemaphoreGive(this->high_performance_semaphore_) == pdTRUE; +} + +bool WiFiComponent::release_high_performance() { + // Already configured for high performance - nothing to release + if (this->configured_power_save_ == WIFI_POWER_SAVE_NONE) { + return true; + } + + // Semaphore initialization failed + if (this->high_performance_semaphore_ == nullptr) { + return false; + } + + // Take the semaphore (non-blocking). This decrements the count. + return xSemaphoreTake(this->high_performance_semaphore_, 0) == pdTRUE; +} +#endif // USE_ESP32 && USE_WIFI_RUNTIME_POWER_SAVE + #ifdef USE_WIFI_FAST_CONNECT bool WiFiComponent::load_fast_connect_settings_(WiFiAP ¶ms) { SavedWifiFastConnectSettings fast_connect_save{}; diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 441606a2c1d..0dac80ad21e 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -49,6 +49,11 @@ extern "C" { #include #endif +#if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE) +#include +#include +#endif + namespace esphome { namespace wifi { @@ -365,6 +370,37 @@ class WiFiComponent : public Component { int32_t get_wifi_channel(); +#ifdef USE_WIFI_RUNTIME_POWER_SAVE + /** Request high-performance mode (no power saving) for improved WiFi latency. + * + * Components that need maximum WiFi performance (e.g., audio streaming, large data transfers) + * can call this method to temporarily disable WiFi power saving. Multiple components can + * request high performance simultaneously using a counting semaphore. + * + * Power saving will be restored to the YAML-configured mode when all components have + * called release_high_performance(). + * + * Note: Only supported on ESP32. + * + * @return true if request was satisfied (high-performance mode active or already configured), + * false if operation failed (semaphore error) + */ + bool request_high_performance(); + + /** Release a high-performance mode request. + * + * Should be called when a component no longer needs maximum WiFi latency. + * When all requests are released (semaphore count reaches zero), WiFi power saving + * is restored to the YAML-configured mode. + * + * Note: Only supported on ESP32. + * + * @return true if release was successful (or already in high-performance config), + * false if operation failed (semaphore error) + */ + bool release_high_performance(); +#endif // USE_WIFI_RUNTIME_POWER_SAVE + protected: #ifdef USE_WIFI_AP void setup_ap_config_(); @@ -535,6 +571,12 @@ class WiFiComponent : public Component { bool keep_scan_results_{false}; bool did_scan_this_cycle_{false}; bool skip_cooldown_next_cycle_{false}; +#if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE) + WiFiPowerSaveMode configured_power_save_{WIFI_POWER_SAVE_NONE}; + bool is_high_performance_mode_{false}; + + SemaphoreHandle_t high_performance_semaphore_{nullptr}; +#endif // Pointers at the end (naturally aligned) Trigger<> *connect_trigger_{new Trigger<>()}; diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 5e7f51e04ca..4b24c395b90 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -210,6 +210,7 @@ #define USE_WEBSERVER_SORTING #define USE_WIFI_11KV_SUPPORT #define USE_WIFI_FAST_CONNECT +#define USE_WIFI_RUNTIME_POWER_SAVE #define USB_HOST_MAX_REQUESTS 16 #ifdef USE_ARDUINO diff --git a/tests/components/wifi/test.esp32-idf.yaml b/tests/components/wifi/test.esp32-idf.yaml index 6b3ef20963c..3e01d7f990b 100644 --- a/tests/components/wifi/test.esp32-idf.yaml +++ b/tests/components/wifi/test.esp32-idf.yaml @@ -1,5 +1,16 @@ psram: +# Tests the high performance request and release; requires the USE_WIFI_RUNTIME_POWER_SAVE define +esphome: + platformio_options: + build_flags: + - "-DUSE_WIFI_RUNTIME_POWER_SAVE" + on_boot: + - then: + - lambda: |- + esphome::wifi::global_wifi_component->request_high_performance(); + esphome::wifi::global_wifi_component->release_high_performance(); + wifi: use_psram: true min_auth_mode: WPA From 7a73a524b94008c31cdcd895feaa5131335af975 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 24 Nov 2025 12:21:09 -0600 Subject: [PATCH 19/28] [logger] Eliminate strlen overhead on LibreTiny (#11938) --- esphome/components/logger/logger.h | 4 ++-- esphome/components/logger/logger_libretiny.cpp | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index 8ba3dacacb7..6a8b640331f 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -72,11 +72,11 @@ static constexpr uint16_t MAX_HEADER_SIZE = 128; static constexpr size_t MAX_POINTER_REPRESENTATION = 2 + sizeof(void *) * 2 + 1; // Platform-specific: does write_msg_ add its own newline? -// false: Caller must add newline to buffer before calling write_msg_ (ESP32, ESP8266) +// false: Caller must add newline to buffer before calling write_msg_ (ESP32, ESP8266, LibreTiny) // Allows single write call with newline included for efficiency // true: write_msg_ adds newline itself via puts()/println() (other platforms) // Newline should NOT be added to buffer -#if defined(USE_ESP32) || defined(USE_ESP8266) +#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_LIBRETINY) static constexpr bool WRITE_MSG_ADDS_NEWLINE = false; #else static constexpr bool WRITE_MSG_ADDS_NEWLINE = true; diff --git a/esphome/components/logger/logger_libretiny.cpp b/esphome/components/logger/logger_libretiny.cpp index b8017b841dc..cdf55e710cb 100644 --- a/esphome/components/logger/logger_libretiny.cpp +++ b/esphome/components/logger/logger_libretiny.cpp @@ -49,7 +49,7 @@ void Logger::pre_setup() { ESP_LOGI(TAG, "Log initialized"); } -void HOT Logger::write_msg_(const char *msg, size_t) { this->hw_serial_->println(msg); } +void HOT Logger::write_msg_(const char *msg, size_t len) { this->hw_serial_->write(msg, len); } const LogString *Logger::get_uart_selection_() { switch (this->uart_) { From 0dd842744a1c5ab50a0bae97fa51766f82bbd506 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Nov 2025 13:44:09 -0600 Subject: [PATCH 20/28] Bump github/codeql-action from 4.31.4 to 4.31.5 (#12080) 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 80fab8819ac..d10c8bf2672 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@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v4.31.4 + uses: github/codeql-action/init@fdbfb4d2750291e159f0156def62b853c2798ca2 # v4.31.5 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@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v4.31.4 + uses: github/codeql-action/analyze@fdbfb4d2750291e159f0156def62b853c2798ca2 # v4.31.5 with: category: "/language:${{matrix.language}}" From 378fc4120ae7621bd03ecebc8a51808dc890b535 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Nov 2025 13:44:27 -0600 Subject: [PATCH 21/28] Bump peter-evans/create-pull-request from 7.0.8 to 7.0.9 (#12082) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/sync-device-classes.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/sync-device-classes.yml b/.github/workflows/sync-device-classes.yml index 2e36dc517d9..8f95fa68ee7 100644 --- a/.github/workflows/sync-device-classes.yml +++ b/.github/workflows/sync-device-classes.yml @@ -41,7 +41,7 @@ jobs: python script/run-in-env.py pre-commit run --all-files - name: Commit changes - uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8 + uses: peter-evans/create-pull-request@84ae59a2cdc2258d6fa0732dd66352dddae2a412 # v7.0.9 with: commit-message: "Synchronise Device Classes from Home Assistant" committer: esphomebot From e2cd0ccd0e05a43fff011c008425cb6bcfa457c6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Nov 2025 13:44:43 -0600 Subject: [PATCH 22/28] Bump actions/create-github-app-token from 2.1.4 to 2.2.0 (#12081) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/auto-label-pr.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/auto-label-pr.yml b/.github/workflows/auto-label-pr.yml index 8d8e08a5fc0..998f3315c69 100644 --- a/.github/workflows/auto-label-pr.yml +++ b/.github/workflows/auto-label-pr.yml @@ -26,7 +26,7 @@ jobs: - name: Generate a token id: generate-token - uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # v2 + uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94 # v2 with: app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }} private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }} From a0440603b7ea6585680a28de05bc169dbead0739 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 24 Nov 2025 13:45:06 -0600 Subject: [PATCH 23/28] [wifi] Use ESP-IDF IP formatting macros directly to eliminate heap allocations (#12078) --- esphome/components/wifi/wifi_component_esp_idf.cpp | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index 4aac03885ae..e6e914c0b41 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -603,10 +603,6 @@ const char *get_auth_mode_str(uint8_t mode) { } } -std::string format_ip4_addr(const esp_ip4_addr_t &ip) { return str_snprintf(IPSTR, 15, IP2STR(&ip)); } -#if LWIP_IPV6 -std::string format_ip6_addr(const esp_ip6_addr_t &ip) { return str_snprintf(IPV6STR, 39, IPV62STR(ip)); } -#endif /* LWIP_IPV6 */ const char *get_disconnect_reason_str(uint8_t reason) { switch (reason) { case WIFI_REASON_AUTH_EXPIRE: @@ -761,14 +757,13 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { #if USE_NETWORK_IPV6 esp_netif_create_ip6_linklocal(s_sta_netif); #endif /* USE_NETWORK_IPV6 */ - ESP_LOGV(TAG, "static_ip=%s gateway=%s", format_ip4_addr(it.ip_info.ip).c_str(), - format_ip4_addr(it.ip_info.gw).c_str()); + ESP_LOGV(TAG, "static_ip=" IPSTR " gateway=" IPSTR, IP2STR(&it.ip_info.ip), IP2STR(&it.ip_info.gw)); this->got_ipv4_address_ = true; #if USE_NETWORK_IPV6 } else if (data->event_base == IP_EVENT && data->event_id == IP_EVENT_GOT_IP6) { const auto &it = data->data.ip_got_ip6; - ESP_LOGV(TAG, "IPv6 address=%s", format_ip6_addr(it.ip6_info.ip).c_str()); + ESP_LOGV(TAG, "IPv6 address=" IPV6STR, IPV62STR(it.ip6_info.ip)); this->num_ipv6_addresses_++; #endif /* USE_NETWORK_IPV6 */ @@ -832,7 +827,7 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { } else if (data->event_base == IP_EVENT && data->event_id == IP_EVENT_AP_STAIPASSIGNED) { const auto &it = data->data.ip_ap_staipassigned; - ESP_LOGV(TAG, "AP client assigned IP %s", format_ip4_addr(it.ip).c_str()); + ESP_LOGV(TAG, "AP client assigned IP " IPSTR, IP2STR(&it.ip)); } } From 909baf5e7a8daee99775775220367cf994a377db Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 24 Nov 2025 13:45:29 -0600 Subject: [PATCH 24/28] [prometheus] Use current_option() instead of deprecated .state for select entities (#12079) --- esphome/components/prometheus/prometheus_handler.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/prometheus/prometheus_handler.cpp b/esphome/components/prometheus/prometheus_handler.cpp index 6b57a3f7182..812b5478607 100644 --- a/esphome/components/prometheus/prometheus_handler.cpp +++ b/esphome/components/prometheus/prometheus_handler.cpp @@ -726,7 +726,7 @@ void PrometheusHandler::select_row_(AsyncResponseStream *stream, select::Select stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); stream->print(ESPHOME_F("\",value=\"")); - stream->print(obj->state.c_str()); + stream->print(obj->current_option()); stream->print(ESPHOME_F("\"} ")); stream->print(ESPHOME_F("1.0")); stream->print(ESPHOME_F("\n")); From 97ba67f4eee3a85e0e416565cba3023900f66f0d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 24 Nov 2025 13:45:56 -0600 Subject: [PATCH 25/28] [core] Deprecate unsafe const char* APIs in mark_failed() and status_set_error(), add LogString* overloads (#12021) --- .../absolute_humidity/absolute_humidity.cpp | 2 +- esphome/components/aht10/aht10.cpp | 2 +- esphome/components/bh1900nux/bh1900nux.cpp | 2 +- .../components/bme280_base/bme280_base.cpp | 18 +++--- .../components/bmp280_base/bmp280_base.cpp | 16 +++--- esphome/components/camera/camera.cpp | 2 +- .../cst816/touchscreen/cst816_touchscreen.cpp | 4 +- esphome/components/epaper_spi/epaper_spi.cpp | 4 +- .../update/esp32_hosted_update.cpp | 10 ++-- esphome/components/esp_ldo/esp_ldo.cpp | 2 +- esphome/components/gdk101/gdk101.cpp | 6 +- .../gt911/touchscreen/gt911_touchscreen.cpp | 4 +- .../update/http_request_update.cpp | 9 +-- esphome/components/lvgl/lvgl_esphome.cpp | 4 +- esphome/components/max17043/max17043.cpp | 4 +- esphome/components/mipi_dsi/mipi_dsi.cpp | 22 ++++---- esphome/components/mipi_dsi/mipi_dsi.h | 2 +- esphome/components/mipi_rgb/mipi_rgb.cpp | 8 +-- esphome/components/mipi_spi/mipi_spi.h | 2 +- .../mixer/speaker/mixer_speaker.cpp | 13 +++-- esphome/components/nau7802/nau7802.cpp | 2 +- .../packet_transport/packet_transport.cpp | 2 +- esphome/components/qmp6988/qmp6988.cpp | 2 +- .../resampler/speaker/resampler_speaker.cpp | 12 ++-- esphome/components/sht4x/sht4x.cpp | 2 +- esphome/components/stts22h/stts22h.cpp | 12 ++-- esphome/components/udp/udp_component.cpp | 10 ++-- .../components/usb_host/usb_host_client.cpp | 2 +- .../usb_host/usb_host_component.cpp | 2 +- esphome/components/usb_uart/usb_uart.cpp | 4 +- .../voice_assistant/voice_assistant.cpp | 2 +- .../components/wake_on_lan/wake_on_lan.cpp | 2 +- esphome/core/component.cpp | 55 ++++++++++++++----- esphome/core/component.h | 21 ++++++- 34 files changed, 157 insertions(+), 109 deletions(-) diff --git a/esphome/components/absolute_humidity/absolute_humidity.cpp b/esphome/components/absolute_humidity/absolute_humidity.cpp index 2c5603ee3d2..d16a024d869 100644 --- a/esphome/components/absolute_humidity/absolute_humidity.cpp +++ b/esphome/components/absolute_humidity/absolute_humidity.cpp @@ -87,7 +87,7 @@ void AbsoluteHumidityComponent::loop() { break; default: this->publish_state(NAN); - this->status_set_error("Invalid saturation vapor pressure equation selection!"); + this->status_set_error(LOG_STR("Invalid saturation vapor pressure equation selection!")); return; } ESP_LOGD(TAG, "Saturation vapor pressure %f kPa", es); diff --git a/esphome/components/aht10/aht10.cpp b/esphome/components/aht10/aht10.cpp index 53c712a7a74..03d9d9cd9ed 100644 --- a/esphome/components/aht10/aht10.cpp +++ b/esphome/components/aht10/aht10.cpp @@ -83,7 +83,7 @@ void AHT10Component::setup() { void AHT10Component::restart_read_() { if (this->read_count_ == AHT10_ATTEMPTS) { this->read_count_ = 0; - this->status_set_error("Reading timed out"); + this->status_set_error(LOG_STR("Reading timed out")); return; } this->read_count_++; diff --git a/esphome/components/bh1900nux/bh1900nux.cpp b/esphome/components/bh1900nux/bh1900nux.cpp index 96a06adaa0e..0e71bd6532b 100644 --- a/esphome/components/bh1900nux/bh1900nux.cpp +++ b/esphome/components/bh1900nux/bh1900nux.cpp @@ -23,7 +23,7 @@ void BH1900NUXSensor::setup() { i2c::ErrorCode result_code = this->write_register(SOFT_RESET_REG, &SOFT_RESET_PAYLOAD, 1); // Software Reset to check communication if (result_code != i2c::ERROR_OK) { - this->mark_failed(ESP_LOG_MSG_COMM_FAIL); + this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL)); return; } } diff --git a/esphome/components/bme280_base/bme280_base.cpp b/esphome/components/bme280_base/bme280_base.cpp index 86b65d361d3..c5d4c9c0a5b 100644 --- a/esphome/components/bme280_base/bme280_base.cpp +++ b/esphome/components/bme280_base/bme280_base.cpp @@ -100,18 +100,18 @@ void BME280Component::setup() { if (!this->read_byte(BME280_REGISTER_CHIPID, &chip_id)) { this->error_code_ = COMMUNICATION_FAILED; - this->mark_failed(ESP_LOG_MSG_COMM_FAIL); + this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL)); return; } if (chip_id != 0x60) { this->error_code_ = WRONG_CHIP_ID; - this->mark_failed(BME280_ERROR_WRONG_CHIP_ID); + this->mark_failed(LOG_STR(BME280_ERROR_WRONG_CHIP_ID)); return; } // Send a soft reset. if (!this->write_byte(BME280_REGISTER_RESET, BME280_SOFT_RESET)) { - this->mark_failed("Reset failed"); + this->mark_failed(LOG_STR("Reset failed")); return; } // Wait until the NVM data has finished loading. @@ -120,12 +120,12 @@ void BME280Component::setup() { do { // NOLINT delay(2); if (!this->read_byte(BME280_REGISTER_STATUS, &status)) { - this->mark_failed("Error reading status register"); + this->mark_failed(LOG_STR("Error reading status register")); return; } } while ((status & BME280_STATUS_IM_UPDATE) && (--retry)); if (status & BME280_STATUS_IM_UPDATE) { - this->mark_failed("Timeout loading NVM"); + this->mark_failed(LOG_STR("Timeout loading NVM")); return; } @@ -153,26 +153,26 @@ void BME280Component::setup() { uint8_t humid_control_val = 0; if (!this->read_byte(BME280_REGISTER_CONTROLHUMID, &humid_control_val)) { - this->mark_failed("Read humidity control"); + this->mark_failed(LOG_STR("Read humidity control")); return; } humid_control_val &= ~0b00000111; humid_control_val |= this->humidity_oversampling_ & 0b111; if (!this->write_byte(BME280_REGISTER_CONTROLHUMID, humid_control_val)) { - this->mark_failed("Write humidity control"); + this->mark_failed(LOG_STR("Write humidity control")); return; } uint8_t config_register = 0; if (!this->read_byte(BME280_REGISTER_CONFIG, &config_register)) { - this->mark_failed("Read config"); + this->mark_failed(LOG_STR("Read config")); return; } config_register &= ~0b11111100; config_register |= 0b101 << 5; // 1000 ms standby time config_register |= (this->iir_filter_ & 0b111) << 2; if (!this->write_byte(BME280_REGISTER_CONFIG, config_register)) { - this->mark_failed("Write config"); + this->mark_failed(LOG_STR("Write config")); return; } } diff --git a/esphome/components/bmp280_base/bmp280_base.cpp b/esphome/components/bmp280_base/bmp280_base.cpp index 39654f5875e..728eead521a 100644 --- a/esphome/components/bmp280_base/bmp280_base.cpp +++ b/esphome/components/bmp280_base/bmp280_base.cpp @@ -65,23 +65,23 @@ void BMP280Component::setup() { // https://community.st.com/t5/stm32-mcus-products/issue-with-reading-bmp280-chip-id-using-spi/td-p/691855 if (!this->bmp_read_byte(0xD0, &chip_id)) { this->error_code_ = COMMUNICATION_FAILED; - this->mark_failed(ESP_LOG_MSG_COMM_FAIL); + this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL)); return; } if (!this->bmp_read_byte(0xD0, &chip_id)) { this->error_code_ = COMMUNICATION_FAILED; - this->mark_failed(ESP_LOG_MSG_COMM_FAIL); + this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL)); return; } if (chip_id != 0x58) { this->error_code_ = WRONG_CHIP_ID; - this->mark_failed(BMP280_ERROR_WRONG_CHIP_ID); + this->mark_failed(LOG_STR(BMP280_ERROR_WRONG_CHIP_ID)); return; } // Send a soft reset. if (!this->bmp_write_byte(BMP280_REGISTER_RESET, BMP280_SOFT_RESET)) { - this->mark_failed("Reset failed"); + this->mark_failed(LOG_STR("Reset failed")); return; } // Wait until the NVM data has finished loading. @@ -90,12 +90,12 @@ void BMP280Component::setup() { do { delay(2); if (!this->bmp_read_byte(BMP280_REGISTER_STATUS, &status)) { - this->mark_failed("Error reading status register"); + this->mark_failed(LOG_STR("Error reading status register")); return; } } while ((status & BMP280_STATUS_IM_UPDATE) && (--retry)); if (status & BMP280_STATUS_IM_UPDATE) { - this->mark_failed("Timeout loading NVM"); + this->mark_failed(LOG_STR("Timeout loading NVM")); return; } @@ -116,14 +116,14 @@ void BMP280Component::setup() { uint8_t config_register = 0; if (!this->bmp_read_byte(BMP280_REGISTER_CONFIG, &config_register)) { - this->mark_failed("Read config"); + this->mark_failed(LOG_STR("Read config")); return; } config_register &= ~0b11111100; config_register |= 0b000 << 5; // 0.5 ms standby time config_register |= (this->iir_filter_ & 0b111) << 2; if (!this->bmp_write_byte(BMP280_REGISTER_CONFIG, config_register)) { - this->mark_failed("Write config"); + this->mark_failed(LOG_STR("Write config")); return; } } diff --git a/esphome/components/camera/camera.cpp b/esphome/components/camera/camera.cpp index 3bd632af5c8..66b8138f38b 100644 --- a/esphome/components/camera/camera.cpp +++ b/esphome/components/camera/camera.cpp @@ -8,7 +8,7 @@ Camera *Camera::global_camera = nullptr; Camera::Camera() { if (global_camera != nullptr) { - this->status_set_error("Multiple cameras are configured, but only one is supported."); + this->status_set_error(LOG_STR("Multiple cameras are configured, but only one is supported.")); this->mark_failed(); return; } diff --git a/esphome/components/cst816/touchscreen/cst816_touchscreen.cpp b/esphome/components/cst816/touchscreen/cst816_touchscreen.cpp index 0560f1b4756..f6280a75a1e 100644 --- a/esphome/components/cst816/touchscreen/cst816_touchscreen.cpp +++ b/esphome/components/cst816/touchscreen/cst816_touchscreen.cpp @@ -20,13 +20,13 @@ void CST816Touchscreen::continue_setup_() { break; default: ESP_LOGE(TAG, "Unknown chip ID: 0x%02X", this->chip_id_); - this->status_set_error("Unknown chip ID"); + this->status_set_error(LOG_STR("Unknown chip ID")); this->mark_failed(); return; } this->write_byte(REG_IRQ_CTL, IRQ_EN_MOTION); } else if (!this->skip_probe_) { - this->status_set_error("Failed to read chip id"); + this->status_set_error(LOG_STR("Failed to read chip id")); this->mark_failed(); return; } diff --git a/esphome/components/epaper_spi/epaper_spi.cpp b/esphome/components/epaper_spi/epaper_spi.cpp index cf6a0b0c3d5..39959cd7437 100644 --- a/esphome/components/epaper_spi/epaper_spi.cpp +++ b/esphome/components/epaper_spi/epaper_spi.cpp @@ -22,7 +22,7 @@ const char *EPaperBase::epaper_state_to_string_() { void EPaperBase::setup() { if (!this->init_buffer_(this->buffer_length_)) { - this->mark_failed("Failed to initialise buffer"); + this->mark_failed(LOG_STR("Failed to initialise buffer")); return; } this->setup_pins_(); @@ -246,7 +246,7 @@ void EPaperBase::initialise_() { auto length = this->init_sequence_length_; while (index != length) { if (length - index < 2) { - this->mark_failed("Malformed init sequence"); + this->mark_failed(LOG_STR("Malformed init sequence")); return; } const uint8_t cmd = sequence[index++]; diff --git a/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp b/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp index adbcc5bf115..f34a0ae10e7 100644 --- a/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp +++ b/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp @@ -88,7 +88,7 @@ void Esp32HostedUpdate::perform(bool force) { hasher.add(this->firmware_data_, this->firmware_size_); hasher.calculate(); if (!hasher.equals_bytes(this->firmware_sha256_.data())) { - this->status_set_error("SHA256 verification failed"); + this->status_set_error(LOG_STR("SHA256 verification failed")); this->publish_state(); return; } @@ -105,7 +105,7 @@ void Esp32HostedUpdate::perform(bool force) { if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to begin OTA: %s", esp_err_to_name(err)); this->state_ = prev_state; - this->status_set_error("Failed to begin OTA"); + this->status_set_error(LOG_STR("Failed to begin OTA")); this->publish_state(); return; } @@ -121,7 +121,7 @@ void Esp32HostedUpdate::perform(bool force) { ESP_LOGE(TAG, "Failed to write OTA data: %s", esp_err_to_name(err)); esp_hosted_slave_ota_end(); // NOLINT this->state_ = prev_state; - this->status_set_error("Failed to write OTA data"); + this->status_set_error(LOG_STR("Failed to write OTA data")); this->publish_state(); return; } @@ -134,7 +134,7 @@ void Esp32HostedUpdate::perform(bool force) { if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to end OTA: %s", esp_err_to_name(err)); this->state_ = prev_state; - this->status_set_error("Failed to end OTA"); + this->status_set_error(LOG_STR("Failed to end OTA")); this->publish_state(); return; } @@ -144,7 +144,7 @@ void Esp32HostedUpdate::perform(bool force) { if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to activate OTA: %s", esp_err_to_name(err)); this->state_ = prev_state; - this->status_set_error("Failed to activate OTA"); + this->status_set_error(LOG_STR("Failed to activate OTA")); this->publish_state(); return; } diff --git a/esphome/components/esp_ldo/esp_ldo.cpp b/esphome/components/esp_ldo/esp_ldo.cpp index 9ea7000b706..5e3d4159f39 100644 --- a/esphome/components/esp_ldo/esp_ldo.cpp +++ b/esphome/components/esp_ldo/esp_ldo.cpp @@ -15,7 +15,7 @@ void EspLdo::setup() { auto err = esp_ldo_acquire_channel(&config, &this->handle_); if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to acquire LDO channel %d with voltage %fV", this->channel_, this->voltage_); - this->mark_failed("Failed to acquire LDO channel"); + this->mark_failed(LOG_STR("Failed to acquire LDO channel")); } else { ESP_LOGD(TAG, "Acquired LDO channel %d with voltage %fV", this->channel_, this->voltage_); } diff --git a/esphome/components/gdk101/gdk101.cpp b/esphome/components/gdk101/gdk101.cpp index 6c218f03d9c..617e2138fb1 100644 --- a/esphome/components/gdk101/gdk101.cpp +++ b/esphome/components/gdk101/gdk101.cpp @@ -36,20 +36,20 @@ void GDK101Component::setup() { uint8_t data[2]; // first, reset the sensor if (!this->reset_sensor_(data)) { - this->status_set_error("Reset failed!"); + this->status_set_error(LOG_STR("Reset failed!")); this->mark_failed(); return; } // sensor should acknowledge success of the reset procedure if (data[0] != 1) { - this->status_set_error("Reset not acknowledged!"); + this->status_set_error(LOG_STR("Reset not acknowledged!")); this->mark_failed(); return; } delay(10); // read firmware version if (!this->read_fw_version_(data)) { - this->status_set_error("Failed to read firmware version"); + this->status_set_error(LOG_STR("Failed to read firmware version")); this->mark_failed(); return; } diff --git a/esphome/components/gt911/touchscreen/gt911_touchscreen.cpp b/esphome/components/gt911/touchscreen/gt911_touchscreen.cpp index 992a86cc21a..b11880a0421 100644 --- a/esphome/components/gt911/touchscreen/gt911_touchscreen.cpp +++ b/esphome/components/gt911/touchscreen/gt911_touchscreen.cpp @@ -79,13 +79,13 @@ void GT911Touchscreen::setup_internal_() { } } if (err != i2c::ERROR_OK) { - this->mark_failed("Calibration error"); + this->mark_failed(LOG_STR("Calibration error")); return; } } if (err != i2c::ERROR_OK) { - this->mark_failed(ESP_LOG_MSG_COMM_FAIL); + this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL)); return; } this->setup_done_ = true; diff --git a/esphome/components/http_request/update/http_request_update.cpp b/esphome/components/http_request/update/http_request_update.cpp index 9dbf8d181a3..c91b0eba730 100644 --- a/esphome/components/http_request/update/http_request_update.cpp +++ b/esphome/components/http_request/update/http_request_update.cpp @@ -29,7 +29,7 @@ void HttpRequestUpdate::setup() { this->publish_state(); } else if (state == ota::OTAState::OTA_ABORT || state == ota::OTAState::OTA_ERROR) { this->state_ = update::UPDATE_STATE_AVAILABLE; - this->status_set_error("Failed to install firmware"); + this->status_set_error(LOG_STR("Failed to install firmware")); this->publish_state(); } }); @@ -51,7 +51,7 @@ void HttpRequestUpdate::update_task(void *params) { if (container == nullptr || container->status_code != HTTP_STATUS_OK) { ESP_LOGE(TAG, "Failed to fetch manifest from %s", this_update->source_url_.c_str()); // Defer to main loop to avoid race condition on component_state_ read-modify-write - this_update->defer([this_update]() { this_update->status_set_error("Failed to fetch manifest"); }); + this_update->defer([this_update]() { this_update->status_set_error(LOG_STR("Failed to fetch manifest")); }); UPDATE_RETURN; } @@ -60,7 +60,8 @@ void HttpRequestUpdate::update_task(void *params) { if (data == nullptr) { ESP_LOGE(TAG, "Failed to allocate %zu bytes for manifest", container->content_length); // Defer to main loop to avoid race condition on component_state_ read-modify-write - this_update->defer([this_update]() { this_update->status_set_error("Failed to allocate memory for manifest"); }); + this_update->defer( + [this_update]() { this_update->status_set_error(LOG_STR("Failed to allocate memory for manifest")); }); container->end(); UPDATE_RETURN; } @@ -123,7 +124,7 @@ void HttpRequestUpdate::update_task(void *params) { if (!valid) { ESP_LOGE(TAG, "Failed to parse JSON from %s", this_update->source_url_.c_str()); // Defer to main loop to avoid race condition on component_state_ read-modify-write - this_update->defer([this_update]() { this_update->status_set_error("Failed to parse manifest JSON"); }); + this_update->defer([this_update]() { this_update->status_set_error(LOG_STR("Failed to parse manifest JSON")); }); UPDATE_RETURN; } diff --git a/esphome/components/lvgl/lvgl_esphome.cpp b/esphome/components/lvgl/lvgl_esphome.cpp index 05005b02170..fbcd68378c8 100644 --- a/esphome/components/lvgl/lvgl_esphome.cpp +++ b/esphome/components/lvgl/lvgl_esphome.cpp @@ -466,7 +466,7 @@ void LvglComponent::setup() { buffer = lv_custom_mem_alloc(buf_bytes); // NOLINT } if (buffer == nullptr) { - this->status_set_error("Memory allocation failure"); + this->status_set_error(LOG_STR("Memory allocation failure")); this->mark_failed(); return; } @@ -479,7 +479,7 @@ void LvglComponent::setup() { if (this->rotation != display::DISPLAY_ROTATION_0_DEGREES) { this->rotate_buf_ = static_cast(lv_custom_mem_alloc(buf_bytes)); // NOLINT if (this->rotate_buf_ == nullptr) { - this->status_set_error("Memory allocation failure"); + this->status_set_error(LOG_STR("Memory allocation failure")); this->mark_failed(); return; } diff --git a/esphome/components/max17043/max17043.cpp b/esphome/components/max17043/max17043.cpp index f605fb13245..e8cf4d5ab17 100644 --- a/esphome/components/max17043/max17043.cpp +++ b/esphome/components/max17043/max17043.cpp @@ -57,14 +57,14 @@ void MAX17043Component::setup() { if (config_reg != MAX17043_CONFIG_POWER_UP_DEFAULT) { ESP_LOGE(TAG, "Device does not appear to be a MAX17043"); - this->status_set_error("unrecognised"); + this->status_set_error(LOG_STR("unrecognised")); this->mark_failed(); return; } // need to write back to config register to reset the sleep bit if (!this->write_byte_16(MAX17043_CONFIG, MAX17043_CONFIG_POWER_UP_DEFAULT)) { - this->status_set_error("sleep reset failed"); + this->status_set_error(LOG_STR("sleep reset failed")); this->mark_failed(); return; } diff --git a/esphome/components/mipi_dsi/mipi_dsi.cpp b/esphome/components/mipi_dsi/mipi_dsi.cpp index 7305435e4b2..cae8647398b 100644 --- a/esphome/components/mipi_dsi/mipi_dsi.cpp +++ b/esphome/components/mipi_dsi/mipi_dsi.cpp @@ -12,8 +12,8 @@ static bool notify_refresh_ready(esp_lcd_panel_handle_t panel, esp_lcd_dpi_panel return (need_yield == pdTRUE); } -void MIPI_DSI::smark_failed(const char *message, esp_err_t err) { - ESP_LOGE(TAG, "%s: %s", message, esp_err_to_name(err)); +void MIPI_DSI::smark_failed(const LogString *message, esp_err_t err) { + ESP_LOGE(TAG, "%s: %s", LOG_STR_ARG(message), esp_err_to_name(err)); this->mark_failed(message); } @@ -37,7 +37,7 @@ void MIPI_DSI::setup() { }; auto err = esp_lcd_new_dsi_bus(&bus_config, &this->bus_handle_); if (err != ESP_OK) { - this->smark_failed("lcd_new_dsi_bus failed", err); + this->smark_failed(LOG_STR("lcd_new_dsi_bus failed"), err); return; } esp_lcd_dbi_io_config_t dbi_config = { @@ -47,7 +47,7 @@ void MIPI_DSI::setup() { }; err = esp_lcd_new_panel_io_dbi(this->bus_handle_, &dbi_config, &this->io_handle_); if (err != ESP_OK) { - this->smark_failed("new_panel_io_dbi failed", err); + this->smark_failed(LOG_STR("new_panel_io_dbi failed"), err); return; } auto pixel_format = LCD_COLOR_PIXEL_FORMAT_RGB565; @@ -75,7 +75,7 @@ void MIPI_DSI::setup() { }}; err = esp_lcd_new_panel_dpi(this->bus_handle_, &dpi_config, &this->handle_); if (err != ESP_OK) { - this->smark_failed("esp_lcd_new_panel_dpi failed", err); + this->smark_failed(LOG_STR("esp_lcd_new_panel_dpi failed"), err); return; } if (this->reset_pin_ != nullptr) { @@ -92,14 +92,14 @@ void MIPI_DSI::setup() { auto when = millis() + 120; err = esp_lcd_panel_init(this->handle_); if (err != ESP_OK) { - this->smark_failed("esp_lcd_init failed", err); + this->smark_failed(LOG_STR("esp_lcd_init failed"), err); return; } size_t index = 0; auto &vec = this->init_sequence_; while (index != vec.size()) { if (vec.size() - index < 2) { - this->mark_failed("Malformed init sequence"); + this->mark_failed(LOG_STR("Malformed init sequence")); return; } uint8_t cmd = vec[index++]; @@ -110,7 +110,7 @@ void MIPI_DSI::setup() { } else { uint8_t num_args = x & 0x7F; if (vec.size() - index < num_args) { - this->mark_failed("Malformed init sequence"); + this->mark_failed(LOG_STR("Malformed init sequence")); return; } if (cmd == SLEEP_OUT) { @@ -125,7 +125,7 @@ void MIPI_DSI::setup() { format_hex_pretty(ptr, num_args, '.', false).c_str()); err = esp_lcd_panel_io_tx_param(this->io_handle_, cmd, ptr, num_args); if (err != ESP_OK) { - this->smark_failed("lcd_panel_io_tx_param failed", err); + this->smark_failed(LOG_STR("lcd_panel_io_tx_param failed"), err); return; } index += num_args; @@ -140,7 +140,7 @@ void MIPI_DSI::setup() { err = (esp_lcd_dpi_panel_register_event_callbacks(this->handle_, &cbs, this->io_lock_)); if (err != ESP_OK) { - this->smark_failed("Failed to register callbacks", err); + this->smark_failed(LOG_STR("Failed to register callbacks"), err); return; } @@ -222,7 +222,7 @@ bool MIPI_DSI::check_buffer_() { RAMAllocator allocator; this->buffer_ = allocator.allocate(this->height_ * this->width_ * bytes_per_pixel); if (this->buffer_ == nullptr) { - this->mark_failed("Could not allocate buffer for display!"); + this->mark_failed(LOG_STR("Could not allocate buffer for display!")); return false; } return true; diff --git a/esphome/components/mipi_dsi/mipi_dsi.h b/esphome/components/mipi_dsi/mipi_dsi.h index 98ee092ed11..1cffe3b1781 100644 --- a/esphome/components/mipi_dsi/mipi_dsi.h +++ b/esphome/components/mipi_dsi/mipi_dsi.h @@ -62,7 +62,7 @@ class MIPI_DSI : public display::Display { void set_lanes(uint8_t lanes) { this->lanes_ = lanes; } void set_madctl(uint8_t madctl) { this->madctl_ = madctl; } - void smark_failed(const char *message, esp_err_t err); + void smark_failed(const LogString *message, esp_err_t err); void update() override; diff --git a/esphome/components/mipi_rgb/mipi_rgb.cpp b/esphome/components/mipi_rgb/mipi_rgb.cpp index 4c687724cf1..74eedae4f4e 100644 --- a/esphome/components/mipi_rgb/mipi_rgb.cpp +++ b/esphome/components/mipi_rgb/mipi_rgb.cpp @@ -73,7 +73,7 @@ void MipiRgbSpi::write_init_sequence_() { auto &vec = this->init_sequence_; while (index != vec.size()) { if (vec.size() - index < 2) { - this->mark_failed("Malformed init sequence"); + this->mark_failed(LOG_STR("Malformed init sequence")); return; } uint8_t cmd = vec[index++]; @@ -84,7 +84,7 @@ void MipiRgbSpi::write_init_sequence_() { } else { uint8_t num_args = x & 0x7F; if (vec.size() - index < num_args) { - this->mark_failed("Malformed init sequence"); + this->mark_failed(LOG_STR("Malformed init sequence")); return; } if (cmd == SLEEP_OUT) { @@ -165,7 +165,7 @@ void MipiRgb::common_setup_() { err = esp_lcd_panel_init(this->handle_); if (err != ESP_OK) { ESP_LOGE(TAG, "lcd setup failed: %s", esp_err_to_name(err)); - this->mark_failed("lcd setup failed"); + this->mark_failed(LOG_STR("lcd setup failed")); } ESP_LOGCONFIG(TAG, "MipiRgb setup complete"); } @@ -249,7 +249,7 @@ bool MipiRgb::check_buffer_() { RAMAllocator allocator; this->buffer_ = allocator.allocate(this->height_ * this->width_); if (this->buffer_ == nullptr) { - this->mark_failed("Could not allocate buffer for display!"); + this->mark_failed(LOG_STR("Could not allocate buffer for display!")); return false; } return true; diff --git a/esphome/components/mipi_spi/mipi_spi.h b/esphome/components/mipi_spi/mipi_spi.h index 7e597d1c610..1953aef0350 100644 --- a/esphome/components/mipi_spi/mipi_spi.h +++ b/esphome/components/mipi_spi/mipi_spi.h @@ -478,7 +478,7 @@ class MipiSpiBuffer : public MipiSpi allocator{}; this->buffer_ = allocator.allocate(BUFFER_WIDTH * BUFFER_HEIGHT / FRACTION); if (this->buffer_ == nullptr) { - this->mark_failed("Buffer allocation failed"); + this->mark_failed(LOG_STR("Buffer allocation failed")); } } diff --git a/esphome/components/mixer/speaker/mixer_speaker.cpp b/esphome/components/mixer/speaker/mixer_speaker.cpp index b0b64f57096..043b629cf18 100644 --- a/esphome/components/mixer/speaker/mixer_speaker.cpp +++ b/esphome/components/mixer/speaker/mixer_speaker.cpp @@ -78,19 +78,20 @@ void SourceSpeaker::loop() { } else { switch (err) { case ESP_ERR_NO_MEM: - this->status_set_error("Failed to start mixer: not enough memory"); + this->status_set_error(LOG_STR("Failed to start mixer: not enough memory")); break; case ESP_ERR_NOT_SUPPORTED: - this->status_set_error("Failed to start mixer: unsupported bits per sample"); + this->status_set_error(LOG_STR("Failed to start mixer: unsupported bits per sample")); break; case ESP_ERR_INVALID_ARG: - this->status_set_error("Failed to start mixer: audio stream isn't compatible with the other audio stream."); + this->status_set_error( + LOG_STR("Failed to start mixer: audio stream isn't compatible with the other audio stream.")); break; case ESP_ERR_INVALID_STATE: - this->status_set_error("Failed to start mixer: mixer task failed to start"); + this->status_set_error(LOG_STR("Failed to start mixer: mixer task failed to start")); break; default: - this->status_set_error("Failed to start mixer"); + this->status_set_error(LOG_STR("Failed to start mixer")); break; } @@ -317,7 +318,7 @@ void MixerSpeaker::loop() { xEventGroupClearBits(this->event_group_, MixerEventGroupBits::STATE_STARTING); } if (event_group_bits & MixerEventGroupBits::ERR_ESP_NO_MEM) { - this->status_set_error("Failed to allocate the mixer's internal buffer"); + this->status_set_error(LOG_STR("Failed to allocate the mixer's internal buffer")); xEventGroupClearBits(this->event_group_, MixerEventGroupBits::ERR_ESP_NO_MEM); } if (event_group_bits & MixerEventGroupBits::STATE_RUNNING) { diff --git a/esphome/components/nau7802/nau7802.cpp b/esphome/components/nau7802/nau7802.cpp index 6a31b754f73..11f63a9a336 100644 --- a/esphome/components/nau7802/nau7802.cpp +++ b/esphome/components/nau7802/nau7802.cpp @@ -278,7 +278,7 @@ void NAU7802Sensor::loop() { this->set_calibration_failure_(true); this->state_ = CalibrationState::INACTIVE; ESP_LOGE(TAG, "Failed to calibrate sensor"); - this->status_set_error("Calibration Failed"); + this->status_set_error(LOG_STR("Calibration Failed")); return; } diff --git a/esphome/components/packet_transport/packet_transport.cpp b/esphome/components/packet_transport/packet_transport.cpp index 857b40ca0ee..37e5f3d9e18 100644 --- a/esphome/components/packet_transport/packet_transport.cpp +++ b/esphome/components/packet_transport/packet_transport.cpp @@ -195,7 +195,7 @@ static void add(std::vector &vec, const char *str) { void PacketTransport::setup() { this->name_ = App.get_name().c_str(); if (strlen(this->name_) > 255) { - this->status_set_error("Device name exceeds 255 chars"); + this->status_set_error(LOG_STR("Device name exceeds 255 chars")); this->mark_failed(); return; } diff --git a/esphome/components/qmp6988/qmp6988.cpp b/esphome/components/qmp6988/qmp6988.cpp index 61fde186d72..57f54b6432f 100644 --- a/esphome/components/qmp6988/qmp6988.cpp +++ b/esphome/components/qmp6988/qmp6988.cpp @@ -310,7 +310,7 @@ void QMP6988Component::calculate_pressure_() { void QMP6988Component::setup() { if (!this->device_check_()) { - this->mark_failed(ESP_LOG_MSG_COMM_FAIL); + this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL)); return; } diff --git a/esphome/components/resampler/speaker/resampler_speaker.cpp b/esphome/components/resampler/speaker/resampler_speaker.cpp index 5e5615cbb97..ad61aca0841 100644 --- a/esphome/components/resampler/speaker/resampler_speaker.cpp +++ b/esphome/components/resampler/speaker/resampler_speaker.cpp @@ -66,17 +66,17 @@ void ResamplerSpeaker::loop() { } if (event_group_bits & ResamplingEventGroupBits::ERR_ESP_NO_MEM) { - this->status_set_error("Resampler task failed to allocate the internal buffers"); + this->status_set_error(LOG_STR("Resampler task failed to allocate the internal buffers")); xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::ERR_ESP_NO_MEM); this->state_ = speaker::STATE_STOPPING; } if (event_group_bits & ResamplingEventGroupBits::ERR_ESP_NOT_SUPPORTED) { - this->status_set_error("Cannot resample due to an unsupported audio stream"); + this->status_set_error(LOG_STR("Cannot resample due to an unsupported audio stream")); xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::ERR_ESP_NOT_SUPPORTED); this->state_ = speaker::STATE_STOPPING; } if (event_group_bits & ResamplingEventGroupBits::ERR_ESP_FAIL) { - this->status_set_error("Resampler task failed"); + this->status_set_error(LOG_STR("Resampler task failed")); xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::ERR_ESP_FAIL); this->state_ = speaker::STATE_STOPPING; } @@ -106,12 +106,12 @@ void ResamplerSpeaker::loop() { } else { switch (err) { case ESP_ERR_INVALID_STATE: - this->status_set_error("Failed to start resampler: resampler task failed to start"); + this->status_set_error(LOG_STR("Failed to start resampler: resampler task failed to start")); break; case ESP_ERR_NO_MEM: - this->status_set_error("Failed to start resampler: not enough memory for task stack"); + this->status_set_error(LOG_STR("Failed to start resampler: not enough memory for task stack")); default: - this->status_set_error("Failed to start resampler"); + this->status_set_error(LOG_STR("Failed to start resampler")); break; } diff --git a/esphome/components/sht4x/sht4x.cpp b/esphome/components/sht4x/sht4x.cpp index 62b8717ded4..617b19ef3e0 100644 --- a/esphome/components/sht4x/sht4x.cpp +++ b/esphome/components/sht4x/sht4x.cpp @@ -13,7 +13,7 @@ void SHT4XComponent::start_heater_() { ESP_LOGD(TAG, "Heater turning on"); if (this->write(cmd, 1) != i2c::ERROR_OK) { - this->status_set_error("Failed to turn on heater"); + this->status_set_error(LOG_STR("Failed to turn on heater")); } } diff --git a/esphome/components/stts22h/stts22h.cpp b/esphome/components/stts22h/stts22h.cpp index 614dc1da8b1..2b2559c8436 100644 --- a/esphome/components/stts22h/stts22h.cpp +++ b/esphome/components/stts22h/stts22h.cpp @@ -21,7 +21,7 @@ static const float SENSOR_SCALE = 0.01f; // Sensor resolution in degrees Celsiu void STTS22HComponent::setup() { // Check if device is a STTS22H if (!this->is_stts22h_sensor_()) { - this->mark_failed("Device is not a STTS22H sensor"); + this->mark_failed(LOG_STR("Device is not a STTS22H sensor")); return; } @@ -61,12 +61,12 @@ float STTS22HComponent::read_temperature_() { bool STTS22HComponent::is_stts22h_sensor_() { uint8_t whoami_value; if (this->read_register(WHOAMI_REG, &whoami_value, 1) != i2c::NO_ERROR) { - this->mark_failed(ESP_LOG_MSG_COMM_FAIL); + this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL)); return false; } if (whoami_value != WHOAMI_STTS22H_IDENTIFICATION) { - this->mark_failed("Unexpected WHOAMI identifier. Sensor is not a STTS22H"); + this->mark_failed(LOG_STR("Unexpected WHOAMI identifier. Sensor is not a STTS22H")); return false; } @@ -77,7 +77,7 @@ void STTS22HComponent::initialize_sensor_() { // Read current CTRL_REG configuration uint8_t ctrl_value; if (this->read_register(CTRL_REG, &ctrl_value, 1) != i2c::NO_ERROR) { - this->mark_failed(ESP_LOG_MSG_COMM_FAIL); + this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL)); return; } @@ -86,14 +86,14 @@ void STTS22HComponent::initialize_sensor_() { // FREERUN bit must be cleared (see sensor documentation) ctrl_value &= ~FREERUN_CTRL_ENABLE_FLAG; // Clear FREERUN bit if (this->write_register(CTRL_REG, &ctrl_value, 1) != i2c::NO_ERROR) { - this->mark_failed(ESP_LOG_MSG_COMM_FAIL); + this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL)); return; } // Enable LOW ODR mode and ADD_INC ctrl_value |= LOW_ODR_CTRL_ENABLE_FLAG | ADD_INC_ENABLE_FLAG; // Set LOW ODR bit and ADD_INC bit if (this->write_register(CTRL_REG, &ctrl_value, 1) != i2c::NO_ERROR) { - this->mark_failed(ESP_LOG_MSG_COMM_FAIL); + this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL)); return; } } diff --git a/esphome/components/udp/udp_component.cpp b/esphome/components/udp/udp_component.cpp index 7714793e1cd..9105ced21e5 100644 --- a/esphome/components/udp/udp_component.cpp +++ b/esphome/components/udp/udp_component.cpp @@ -21,7 +21,7 @@ void UDPComponent::setup() { if (this->should_broadcast_) { this->broadcast_socket_ = socket::socket(AF_INET, SOCK_DGRAM, IPPROTO_IP); if (this->broadcast_socket_ == nullptr) { - this->status_set_error("Could not create socket"); + this->status_set_error(LOG_STR("Could not create socket")); this->mark_failed(); return; } @@ -41,14 +41,14 @@ void UDPComponent::setup() { if (this->should_listen_) { this->listen_socket_ = socket::socket(AF_INET, SOCK_DGRAM, IPPROTO_IP); if (this->listen_socket_ == nullptr) { - this->status_set_error("Could not create socket"); + this->status_set_error(LOG_STR("Could not create socket")); this->mark_failed(); return; } auto err = this->listen_socket_->setblocking(false); if (err < 0) { ESP_LOGE(TAG, "Unable to set nonblocking: errno %d", errno); - this->status_set_error("Unable to set nonblocking"); + this->status_set_error(LOG_STR("Unable to set nonblocking")); this->mark_failed(); return; } @@ -73,7 +73,7 @@ void UDPComponent::setup() { err = this->listen_socket_->setsockopt(IPPROTO_IP, IP_ADD_MEMBERSHIP, &imreq, sizeof(imreq)); if (err < 0) { ESP_LOGE(TAG, "Failed to set IP_ADD_MEMBERSHIP. Error %d", errno); - this->status_set_error("Failed to set IP_ADD_MEMBERSHIP"); + this->status_set_error(LOG_STR("Failed to set IP_ADD_MEMBERSHIP")); this->mark_failed(); return; } @@ -82,7 +82,7 @@ void UDPComponent::setup() { err = this->listen_socket_->bind((struct sockaddr *) &server, sizeof(server)); if (err != 0) { ESP_LOGE(TAG, "Socket unable to bind: errno %d", errno); - this->status_set_error("Unable to bind socket"); + this->status_set_error(LOG_STR("Unable to bind socket")); this->mark_failed(); return; } diff --git a/esphome/components/usb_host/usb_host_client.cpp b/esphome/components/usb_host/usb_host_client.cpp index 4c09cf8a498..fe61353b5db 100644 --- a/esphome/components/usb_host/usb_host_client.cpp +++ b/esphome/components/usb_host/usb_host_client.cpp @@ -188,7 +188,7 @@ void USBClient::setup() { auto err = usb_host_client_register(&config, &this->handle_); if (err != ESP_OK) { ESP_LOGE(TAG, "client register failed: %s", esp_err_to_name(err)); - this->status_set_error("Client register failed"); + this->status_set_error(LOG_STR("Client register failed")); this->mark_failed(); return; } diff --git a/esphome/components/usb_host/usb_host_component.cpp b/esphome/components/usb_host/usb_host_component.cpp index fb19239c732..1e70c289df5 100644 --- a/esphome/components/usb_host/usb_host_component.cpp +++ b/esphome/components/usb_host/usb_host_component.cpp @@ -11,7 +11,7 @@ void USBHost::setup() { usb_host_config_t config{}; if (usb_host_install(&config) != ESP_OK) { - this->status_set_error("usb_host_install failed"); + this->status_set_error(LOG_STR("usb_host_install failed")); this->mark_failed(); return; } diff --git a/esphome/components/usb_uart/usb_uart.cpp b/esphome/components/usb_uart/usb_uart.cpp index c24fffb11de..6720c1e6907 100644 --- a/esphome/components/usb_uart/usb_uart.cpp +++ b/esphome/components/usb_uart/usb_uart.cpp @@ -320,7 +320,7 @@ static void fix_mps(const usb_ep_desc_t *ep) { void USBUartTypeCdcAcm::on_connected() { auto cdc_devs = this->parse_descriptors(this->device_handle_); if (cdc_devs.empty()) { - this->status_set_error("No CDC-ACM device found"); + this->status_set_error(LOG_STR("No CDC-ACM device found")); this->disconnect(); return; } @@ -341,7 +341,7 @@ void USBUartTypeCdcAcm::on_connected() { if (err != ESP_OK) { ESP_LOGE(TAG, "usb_host_interface_claim failed: %s, channel=%d, intf=%d", esp_err_to_name(err), channel->index_, channel->cdc_dev_.bulk_interface_number); - this->status_set_error("usb_host_interface_claim failed"); + this->status_set_error(LOG_STR("usb_host_interface_claim failed")); this->disconnect(); return; } diff --git a/esphome/components/voice_assistant/voice_assistant.cpp b/esphome/components/voice_assistant/voice_assistant.cpp index fd35dc7d09e..551f0370f27 100644 --- a/esphome/components/voice_assistant/voice_assistant.cpp +++ b/esphome/components/voice_assistant/voice_assistant.cpp @@ -206,7 +206,7 @@ void VoiceAssistant::loop() { case State::START_MICROPHONE: { ESP_LOGD(TAG, "Starting Microphone"); if (!this->allocate_buffers_()) { - this->status_set_error("Failed to allocate buffers"); + this->status_set_error(LOG_STR("Failed to allocate buffers")); return; } if (this->status_has_error()) { diff --git a/esphome/components/wake_on_lan/wake_on_lan.cpp b/esphome/components/wake_on_lan/wake_on_lan.cpp index 7993abd7e76..8c5bdac54b2 100644 --- a/esphome/components/wake_on_lan/wake_on_lan.cpp +++ b/esphome/components/wake_on_lan/wake_on_lan.cpp @@ -67,7 +67,7 @@ void WakeOnLanButton::setup() { #if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) this->broadcast_socket_ = socket::socket(AF_INET, SOCK_DGRAM, IPPROTO_IP); if (this->broadcast_socket_ == nullptr) { - this->status_set_error("Could not create socket"); + this->status_set_error(LOG_STR("Could not create socket")); this->mark_failed(); return; } diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index de3dd99d0c7..5e6ace8873a 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -36,6 +36,9 @@ namespace { struct ComponentErrorMessage { const Component *component; const char *message; + // Track if message is flash pointer (needs LOG_STR_ARG) or RAM pointer + // Remove before 2026.6.0 when deprecated const char* API is removed + bool is_flash_ptr; }; struct ComponentPriorityOverride { @@ -49,6 +52,25 @@ std::unique_ptr> component_error_messages; // Setup priority overrides - freed after setup completes // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) std::unique_ptr> setup_priority_overrides; + +// Helper to store error messages - reduces duplication between deprecated and new API +// Remove before 2026.6.0 when deprecated const char* API is removed +void store_component_error_message(const Component *component, const char *message, bool is_flash_ptr) { + // Lazy allocate the error messages vector if needed + if (!component_error_messages) { + component_error_messages = std::make_unique>(); + } + // Check if this component already has an error message + for (auto &entry : *component_error_messages) { + if (entry.component == component) { + entry.message = message; + entry.is_flash_ptr = is_flash_ptr; + return; + } + } + // Add new error message + component_error_messages->emplace_back(ComponentErrorMessage{component, message, is_flash_ptr}); +} } // namespace namespace setup_priority { @@ -143,16 +165,20 @@ void Component::call_dump_config() { if (this->is_failed()) { // Look up error message from global vector const char *error_msg = nullptr; + bool is_flash_ptr = false; if (component_error_messages) { for (const auto &entry : *component_error_messages) { if (entry.component == this) { error_msg = entry.message; + is_flash_ptr = entry.is_flash_ptr; break; } } } + // Log with appropriate format based on pointer type ESP_LOGE(TAG, " %s is marked FAILED: %s", LOG_STR_ARG(this->get_component_log_str()), - error_msg ? error_msg : LOG_STR_LITERAL("unspecified")); + error_msg ? (is_flash_ptr ? LOG_STR_ARG((const LogString *) error_msg) : error_msg) + : LOG_STR_LITERAL("unspecified")); } } @@ -307,6 +333,7 @@ void Component::status_set_warning(const LogString *message) { ESP_LOGW(TAG, "%s set Warning flag: %s", LOG_STR_ARG(this->get_component_log_str()), message ? LOG_STR_ARG(message) : LOG_STR_LITERAL("unspecified")); } +void Component::status_set_error() { this->status_set_error((const LogString *) nullptr); } void Component::status_set_error(const char *message) { if ((this->component_state_ & STATUS_LED_ERROR) != 0) return; @@ -315,19 +342,19 @@ void Component::status_set_error(const char *message) { ESP_LOGE(TAG, "%s set Error flag: %s", LOG_STR_ARG(this->get_component_log_str()), message ? message : LOG_STR_LITERAL("unspecified")); if (message != nullptr) { - // Lazy allocate the error messages vector if needed - if (!component_error_messages) { - component_error_messages = std::make_unique>(); - } - // Check if this component already has an error message - for (auto &entry : *component_error_messages) { - if (entry.component == this) { - entry.message = message; - return; - } - } - // Add new error message - component_error_messages->emplace_back(ComponentErrorMessage{this, message}); + store_component_error_message(this, message, false); + } +} +void Component::status_set_error(const LogString *message) { + if ((this->component_state_ & STATUS_LED_ERROR) != 0) + return; + this->component_state_ |= STATUS_LED_ERROR; + App.app_state_ |= STATUS_LED_ERROR; + ESP_LOGE(TAG, "%s set Error flag: %s", LOG_STR_ARG(this->get_component_log_str()), + message ? LOG_STR_ARG(message) : LOG_STR_LITERAL("unspecified")); + if (message != nullptr) { + // Store the LogString pointer directly (safe because LogString is always in flash/static memory) + store_component_error_message(this, LOG_STR_ARG(message), true); } } void Component::status_clear_warning() { diff --git a/esphome/core/component.h b/esphome/core/component.h index 462e0e301c7..51a9290e8bc 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -5,6 +5,7 @@ #include #include +#include "esphome/core/helpers.h" #include "esphome/core/log.h" #include "esphome/core/optional.h" @@ -157,7 +158,19 @@ class Component { */ virtual void mark_failed(); + // Remove before 2026.6.0 + ESPDEPRECATED("Use mark_failed(LOG_STR(\"static string literal\")) instead. Do NOT use .c_str() from temporary " + "strings. Will stop working in 2026.6.0", + "2025.12.0") void mark_failed(const char *message) { +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" + this->status_set_error(message); +#pragma GCC diagnostic pop + this->mark_failed(); + } + + void mark_failed(const LogString *message) { this->status_set_error(message); this->mark_failed(); } @@ -216,7 +229,13 @@ class Component { void status_set_warning(const char *message = nullptr); void status_set_warning(const LogString *message); - void status_set_error(const char *message = nullptr); + void status_set_error(); // Set error flag without message + // Remove before 2026.6.0 + ESPDEPRECATED("Use status_set_error(LOG_STR(\"static string literal\")) instead. Do NOT use .c_str() from temporary " + "strings. Will stop working in 2026.6.0", + "2025.12.0") + void status_set_error(const char *message); + void status_set_error(const LogString *message); void status_clear_warning(); From eeb373fca98681ed576b767450044d2fb5cefedf Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 25 Nov 2025 09:15:30 +1300 Subject: [PATCH 26/28] [online_image] Fix some large PNGs causing watchdog timeout (#12025) Co-authored-by: guillempages --- esphome/components/online_image/png_image.cpp | 9 +++++++++ esphome/components/online_image/png_image.h | 4 ++++ 2 files changed, 13 insertions(+) diff --git a/esphome/components/online_image/png_image.cpp b/esphome/components/online_image/png_image.cpp index 2038d09ed05..ce9d3bdc916 100644 --- a/esphome/components/online_image/png_image.cpp +++ b/esphome/components/online_image/png_image.cpp @@ -2,6 +2,7 @@ #ifdef USE_ONLINE_IMAGE_PNG_SUPPORT #include "esphome/components/display/display_buffer.h" +#include "esphome/core/application.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" @@ -38,6 +39,14 @@ static void draw_callback(pngle_t *pngle, uint32_t x, uint32_t y, uint32_t w, ui PngDecoder *decoder = (PngDecoder *) pngle_get_user_data(pngle); Color color(rgba[0], rgba[1], rgba[2], rgba[3]); decoder->draw(x, y, w, h, color); + + // Feed watchdog periodically to avoid triggering during long decode operations. + // Feed every 1024 pixels to balance efficiency and responsiveness. + uint32_t pixels = w * h; + decoder->increment_pixels_decoded(pixels); + if ((decoder->get_pixels_decoded() % 1024) < pixels) { + App.feed_wdt(); + } } PngDecoder::PngDecoder(OnlineImage *image) : ImageDecoder(image) { diff --git a/esphome/components/online_image/png_image.h b/esphome/components/online_image/png_image.h index 46519f8ef45..40e85dde33f 100644 --- a/esphome/components/online_image/png_image.h +++ b/esphome/components/online_image/png_image.h @@ -25,9 +25,13 @@ class PngDecoder : public ImageDecoder { int prepare(size_t download_size) override; int HOT decode(uint8_t *buffer, size_t size) override; + void increment_pixels_decoded(uint32_t count) { this->pixels_decoded_ += count; } + uint32_t get_pixels_decoded() const { return this->pixels_decoded_; } + protected: RAMAllocator allocator_; pngle_t *pngle_; + uint32_t pixels_decoded_{0}; }; } // namespace online_image From e09656f20e1abdd7984ee8353a49a1d108ef299d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Nov 2025 15:21:03 -0600 Subject: [PATCH 27/28] Bump bleak from 1.1.1 to 2.0.0 (#12083) 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 6ae050b35b0..df036eeccc7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -22,7 +22,7 @@ pillow==11.3.0 cairosvg==2.8.2 freetype-py==2.5.1 jinja2==3.1.6 -bleak==1.1.1 +bleak==2.0.0 # esp-idf >= 5.0 requires this pyparsing >= 3.0 From 88b898458b6a71a50ff63fed3aae2658e16a19a0 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Mon, 24 Nov 2025 15:25:49 -0600 Subject: [PATCH 28/28] [bluetooth_proxy] Fix crash due to null pointer (#12084) Co-authored-by: J. Nick Koston Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- esphome/components/bluetooth_proxy/bluetooth_proxy.h | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.h b/esphome/components/bluetooth_proxy/bluetooth_proxy.h index 4de541fac2f..4363c508ecd 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.h +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.h @@ -132,7 +132,11 @@ class BluetoothProxy final : public esp32_ble_tracker::ESPBTDeviceListener, publ void get_bluetooth_mac_address_pretty(std::span output) { const uint8_t *mac = esp_bt_dev_get_address(); - format_mac_addr_upper(mac, output.data()); + if (mac != nullptr) { + format_mac_addr_upper(mac, output.data()); + } else { + output[0] = '\0'; + } } protected: