From c863d589992079b43998471eb80ad783691fd290 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 13 May 2026 23:19:30 -0400 Subject: [PATCH 01/24] [espidf] Stop perpetual reconfigure loop on native ESP-IDF builds (#16415) --- esphome/components/esp32/__init__.py | 7 ++++++- esphome/espidf/toolchain.py | 30 ++++++++++++---------------- 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 1eb0bb2174..0c24dbf7b9 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -2509,7 +2509,12 @@ def _write_idf_component_yml(): stubs_dir = CORE.relative_build_path("component_stubs") stubs_dir.mkdir(exist_ok=True) - for component_name in components_to_stub: + # Sort so the dict insertion order (and thus the generated + # src/idf_component.yml) is deterministic across runs; otherwise + # the manifest content shuffles every build, write_file_if_changed + # always writes, and ninja keeps triggering CMake re-runs on + # otherwise-cached rebuilds. + for component_name in sorted(components_to_stub): # Create stub directory with minimal CMakeLists.txt stub_path = stubs_dir / _idf_component_stub_name(component_name) stub_path.mkdir(exist_ok=True) diff --git a/esphome/espidf/toolchain.py b/esphome/espidf/toolchain.py index 583f340996..1245c643e1 100644 --- a/esphome/espidf/toolchain.py +++ b/esphome/espidf/toolchain.py @@ -191,13 +191,19 @@ def run_reconfigure() -> int: def has_outdated_files(): """Check if the build configuration is stale. - Returns True if required build files are missing or if configuration inputs - are newer than the generated CMake/Ninja build artifacts. + Returns True if required build files are missing or if external + configuration inputs (IDF install, sdkconfig, CMake's own build/config + dir) are newer than CMakeCache.txt. We deliberately don't watch the + top-level/src ``CMakeLists.txt`` here -- those are written by + ``write_project`` via ``write_file_if_changed`` (so an mtime bump + means our content actually changed) and ninja already tracks them as + configure-time deps via ``build.ninja``. Including them in this check + causes a perpetual reconfigure loop: the two-pass write leaves + CMakeLists newer than CMakeCache.txt, and CMake doesn't restamp the + cache when only ``idf_build_set_property`` values change, so the + check would trip on every subsequent build. """ cmakecache_txt_path = CORE.relative_build_path("build/CMakeCache.txt") - - cmakelists_txt_build_path = CORE.relative_build_path("CMakeLists.txt") - cmakelists_txt_src_path = CORE.relative_src_path("CMakeLists.txt") build_config_path = CORE.relative_build_path("build/config") sdkconfig_internal_path = CORE.relative_build_path( f"sdkconfig.{CORE.name}.esphomeinternal" @@ -221,8 +227,6 @@ def has_outdated_files(): os.path.getmtime(f) > cmakecache_txt_mtime for f in [ _get_idf_path(), - cmakelists_txt_build_path, - cmakelists_txt_src_path, sdkconfig_internal_path, build_config_path, ] @@ -302,21 +306,13 @@ def run_compile(config, verbose: bool) -> int: return rc _LOGGER.info("Regenerating CMakeLists.txt with discovered components...") write_project(minimal=False) - # The post-discovery rewrite leaves CMakeLists newer than - # CMakeCache.txt. CMake won't re-touch CMakeCache.txt on a - # configure that only changes idf_build_set_property values - # (those aren't cache variables), so has_outdated_files() would - # return True on every subsequent build, perpetually retriggering - # the two-pass. Touch CMakeCache.txt now so its mtime stays past - # the rewritten CMakeLists. - cmakecache = CORE.relative_build_path("build/CMakeCache.txt") - if cmakecache.is_file(): - os.utime(cmakecache) if CORE.testing_mode: # Reconfigure again so cmake is up to date with the full # component list before the build's idf.py invocation runs -- # idf.py build would otherwise re-run cmake and regenerate # memory.ld, wiping the DRAM/IRAM patches applied below. + # Outside testing mode ninja's own configure-time dep on + # CMakeLists.txt handles the re-run as part of the build step. rc = run_reconfigure() if rc != 0: _LOGGER.error("Reconfigure with discovered components failed") From 84b5931299de172ba87dc9c5cfdf88ba7ae15773 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 14 May 2026 00:02:22 -0400 Subject: [PATCH 02/24] [espidf] Trim has_outdated_files watch list; embed IDF version in sdkconfig (#16416) --- esphome/components/esp32/__init__.py | 8 ++++- esphome/espidf/toolchain.py | 45 +++++++++++++++++----------- 2 files changed, 35 insertions(+), 18 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 0c24dbf7b9..f112549832 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -2464,8 +2464,14 @@ def _write_sdkconfig(): ) want_opts = CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS] + # Include the resolved framework version as a Kconfig comment so a + # version switch that happens to leave the option set unchanged still + # bumps this file's content -- which is what has_outdated_files() + # uses to decide whether to reconfigure. + framework_version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] contents = ( - "\n".join( + f"# ESPHOME_IDF_VERSION={framework_version}\n" + + "\n".join( f"{name}={_format_sdkconfig_val(value)}" for name, value in sorted(want_opts.items()) ) diff --git a/esphome/espidf/toolchain.py b/esphome/espidf/toolchain.py index 1245c643e1..e0bc5bb393 100644 --- a/esphome/espidf/toolchain.py +++ b/esphome/espidf/toolchain.py @@ -191,23 +191,38 @@ def run_reconfigure() -> int: def has_outdated_files(): """Check if the build configuration is stale. - Returns True if required build files are missing or if external - configuration inputs (IDF install, sdkconfig, CMake's own build/config - dir) are newer than CMakeCache.txt. We deliberately don't watch the - top-level/src ``CMakeLists.txt`` here -- those are written by - ``write_project`` via ``write_file_if_changed`` (so an mtime bump - means our content actually changed) and ninja already tracks them as - configure-time deps via ``build.ninja``. Including them in this check - causes a perpetual reconfigure loop: the two-pass write leaves - CMakeLists newer than CMakeCache.txt, and CMake doesn't restamp the - cache when only ``idf_build_set_property`` values change, so the - check would trip on every subsequent build. + Returns True if required build files are missing or if ESPHome's + resolved build inputs are newer than CMakeCache.txt: + + - ``sdkconfig..esphomeinternal`` -- the canonical "what state + did ESPHome resolve the YAML to" snapshot. Any change in build + flags, enabled components, framework version, or target ends up + rewriting it (we embed a ``# ESPHOME_IDF_VERSION=`` comment line + for the version case where the option set would otherwise be + identical). + - ``src/idf_component.yml`` -- the project manifest. Managed + component additions/removals (e.g. via ``add_idf_component``) can + happen without any sdkconfig impact, and ``_write_idf_component_yml`` + already deletes ``dependencies.lock`` on a change but that signal + gets lost as soon as the lock is missing. + + We deliberately don't watch: + - The top-level/src ``CMakeLists.txt`` -- ESPHome owns those, and + ninja already tracks them as configure-time deps. Including them + causes a perpetual reconfigure loop because CMake doesn't restamp + ``CMakeCache.txt`` when only ``idf_build_set_property`` values + change between configures. + - ``$IDF_PATH`` and CMake's ``build/config/`` -- both have mtime + semantics that fire after the wrong configure (or not at all in + common cases like in-place IDF version replacement). The sdkconfig + and manifest hashes subsume the meaningful signal. """ cmakecache_txt_path = CORE.relative_build_path("build/CMakeCache.txt") build_config_path = CORE.relative_build_path("build/config") sdkconfig_internal_path = CORE.relative_build_path( f"sdkconfig.{CORE.name}.esphomeinternal" ) + idf_component_yml_path = CORE.relative_build_path("src/idf_component.yml") dependency_lock_path = CORE.relative_build_path("dependencies.lock") build_ninja_path = CORE.relative_build_path("build/build.ninja") @@ -225,12 +240,8 @@ def has_outdated_files(): cmakecache_txt_mtime = os.path.getmtime(cmakecache_txt_path) return any( os.path.getmtime(f) > cmakecache_txt_mtime - for f in [ - _get_idf_path(), - sdkconfig_internal_path, - build_config_path, - ] - if f and os.path.exists(f) + for f in [sdkconfig_internal_path, idf_component_yml_path] + if f.exists() ) From ab273a1f8fe4419bd67d9e2a2c05ed222a0beca7 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Thu, 14 May 2026 15:07:38 -0500 Subject: [PATCH 03/24] [tinyusb] Reject `tinyusb:` configured without a USB class companion (#16413) Co-authored-by: Claude Opus 4.7 (1M context) --- esphome/components/tinyusb/__init__.py | 20 +++++++++++++++++++ .../components/tinyusb/tinyusb_component.cpp | 15 ++++++++++++++ tests/components/tinyusb/common.yaml | 5 +++++ 3 files changed, 40 insertions(+) diff --git a/esphome/components/tinyusb/__init__.py b/esphome/components/tinyusb/__init__.py index df94ad7534..724f65721b 100644 --- a/esphome/components/tinyusb/__init__.py +++ b/esphome/components/tinyusb/__init__.py @@ -1,3 +1,4 @@ +from esphome import final_validate as fv import esphome.codegen as cg from esphome.components import esp32 from esphome.components.esp32 import ( @@ -20,6 +21,13 @@ CONF_USB_PRODUCT_STR = "usb_product_str" CONF_USB_SERIAL_STR = "usb_serial_str" CONF_USB_VENDOR_ID = "usb_vendor_id" +# Components that provide a USB device class (CDC, HID, MSC, ...) on top of +# tinyusb. Configuring `tinyusb:` without any of these triggers a 5s hang in +# esp_tinyusb's driver install (descriptors_set fails with no class and no +# user-provided full_speed_config), which trips the task watchdog before +# loop() ever runs. +_USB_CLASS_COMPONENTS = ("usb_cdc_acm",) + tinyusb_ns = cg.esphome_ns.namespace("tinyusb") TinyUSB = tinyusb_ns.class_("TinyUSB", cg.Component) @@ -41,6 +49,18 @@ CONFIG_SCHEMA = cv.All( ) +def _final_validate(config): + full_config = fv.full_config.get() + if not any(name in full_config for name in _USB_CLASS_COMPONENTS): + raise cv.Invalid( + "The 'tinyusb' component requires at least one USB class component" + ) + return config + + +FINAL_VALIDATE_SCHEMA = _final_validate + + async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) diff --git a/esphome/components/tinyusb/tinyusb_component.cpp b/esphome/components/tinyusb/tinyusb_component.cpp index 2ec696c3e4..3cefc0454a 100644 --- a/esphome/components/tinyusb/tinyusb_component.cpp +++ b/esphome/components/tinyusb/tinyusb_component.cpp @@ -26,6 +26,21 @@ void TinyUSB::setup() { .string_count = SIZE, }; + // Defense-in-depth: esp_tinyusb's tinyusb_descriptors_set() fails with + // ESP_ERR_INVALID_ARG when no configuration descriptor is provided and + // no class that has a built-in default (CDC/MSC/NCM) is compiled in. In + // that case the internal task exits without notifying us, and + // tinyusb_driver_install() blocks 5s on the notify-take -- long enough + // to trip the task watchdog. Bail early so the rest of the device can + // still boot. +#if !(CFG_TUD_CDC > 0 || CFG_TUD_MSC > 0 || CFG_TUD_NCM > 0) + if (this->tusb_cfg_.descriptor.full_speed_config == nullptr) { + ESP_LOGE(TAG, "No USB class configured"); + this->mark_failed(); + return; + } +#endif + esp_err_t result = tinyusb_driver_install(&this->tusb_cfg_); if (result != ESP_OK) { ESP_LOGE(TAG, "tinyusb_driver_install failed: %s", esp_err_to_name(result)); diff --git a/tests/components/tinyusb/common.yaml b/tests/components/tinyusb/common.yaml index cb3f48836a..674e89dbe8 100644 --- a/tests/components/tinyusb/common.yaml +++ b/tests/components/tinyusb/common.yaml @@ -6,3 +6,8 @@ tinyusb: usb_product_str: ESPHomeTestProduct usb_serial_str: ESPHomeTestSerialNumber usb_vendor_id: 0x2345 + +# tinyusb requires at least one USB class companion; usb_cdc_acm satisfies that. +usb_cdc_acm: + interfaces: + - id: tinyusb_test_cdc From fb659f9ac4e35022d87317a4bbfab8af9076cba0 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Thu, 14 May 2026 15:29:56 -0500 Subject: [PATCH 04/24] [tinyusb] Reject `logger.hardware_uart: USB_CDC` (#16417) Co-authored-by: Claude Opus 4.7 (1M context) --- esphome/components/tinyusb/__init__.py | 13 ++++++++++++- tests/components/tinyusb/test.esp32-s2-idf.yaml | 5 +++++ tests/components/usb_cdc_acm/test.esp32-s2-idf.yaml | 5 +++++ 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/esphome/components/tinyusb/__init__.py b/esphome/components/tinyusb/__init__.py index 724f65721b..0e02ff8724 100644 --- a/esphome/components/tinyusb/__init__.py +++ b/esphome/components/tinyusb/__init__.py @@ -9,7 +9,7 @@ from esphome.components.esp32 import ( add_idf_sdkconfig_option, ) import esphome.config_validation as cv -from esphome.const import CONF_ID +from esphome.const import CONF_HARDWARE_UART, CONF_ID CODEOWNERS = ["@kbx81"] CONFLICTS_WITH = ["usb_host"] @@ -55,6 +55,17 @@ def _final_validate(config): raise cv.Invalid( "The 'tinyusb' component requires at least one USB class component" ) + # tinyusb owns the USB OTG peripheral. The logger's USB_CDC backend routes + # the ROM console through that same peripheral, so the two cannot coexist. + # (USB_SERIAL_JTAG is a separate peripheral and is fine alongside tinyusb.) + logger_config = full_config.get("logger") + if logger_config and logger_config.get(CONF_HARDWARE_UART) == "USB_CDC": + raise cv.Invalid( + "'tinyusb' cannot be used with 'logger.hardware_uart: USB_CDC' " + "because both share the USB OTG peripheral. Set " + "'logger.hardware_uart' to a hardware UART (e.g. UART0), or to " + "USB_SERIAL_JTAG on variants that support it (ESP32-S3, ESP32-P4)" + ) return config diff --git a/tests/components/tinyusb/test.esp32-s2-idf.yaml b/tests/components/tinyusb/test.esp32-s2-idf.yaml index dade44d145..09b98ada40 100644 --- a/tests/components/tinyusb/test.esp32-s2-idf.yaml +++ b/tests/components/tinyusb/test.esp32-s2-idf.yaml @@ -1 +1,6 @@ <<: !include common.yaml + +# S2 defaults logger to USB_CDC, which conflicts with tinyusb on the shared +# USB OTG peripheral; route the logger to UART0 so the fixture builds. +logger: + hardware_uart: UART0 diff --git a/tests/components/usb_cdc_acm/test.esp32-s2-idf.yaml b/tests/components/usb_cdc_acm/test.esp32-s2-idf.yaml index f159b38ff6..ff75731509 100644 --- a/tests/components/usb_cdc_acm/test.esp32-s2-idf.yaml +++ b/tests/components/usb_cdc_acm/test.esp32-s2-idf.yaml @@ -1,5 +1,10 @@ <<: !include tinyusb_common.yaml +# S2 defaults logger to USB_CDC, which conflicts with tinyusb on the shared +# USB OTG peripheral; route the logger to UART0 so the fixture builds. +logger: + hardware_uart: UART0 + usb_cdc_acm: interfaces: - id: usb_cdc_acm1 From dd1818661c29d03f83153e95ba3bbd5dd40c4796 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 14 May 2026 18:39:16 -0400 Subject: [PATCH 05/24] [esp32] Sweep ESP-IDF toolchain warnings + bump deprecated mark_failed (#16432) --- esphome/components/esp32/__init__.py | 4 +++- esphome/components/hdc2080/hdc2080.cpp | 2 +- esphome/components/heatpumpir/climate.py | 1 - esphome/components/pulse_meter/pulse_meter_sensor.cpp | 4 ++-- esphome/components/sim800l/sim800l.cpp | 2 +- esphome/components/tuya/tuya.cpp | 2 ++ esphome/components/tx20/tx20.cpp | 6 +++--- esphome/components/usb_uart/usb_uart.h | 2 +- esphome/components/web_server/web_server.cpp | 2 +- esphome/components/web_server_idf/web_server_idf.cpp | 2 +- esphome/components/wiegand/wiegand.cpp | 4 ++-- 11 files changed, 17 insertions(+), 14 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index f112549832..e9b0f1fd0a 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -1767,9 +1767,11 @@ async def to_code(config): else: cg.add_build_flag("-Wno-error=format") cg.add_build_flag("-Wno-error=maybe-uninitialized") - cg.add_build_flag("-Wno-error=missing-field-initializers") + cg.add_build_flag("-Wno-error=overloaded-virtual") cg.add_build_flag("-Wno-error=reorder") cg.add_build_flag("-Wno-error=volatile") + # -Wno- (not -Wno-error=): suppress entirely, too noisy on C++ aggregates + cg.add_build_flag("-Wno-missing-field-initializers") cg.set_cpp_standard("gnu++20") cg.add_build_flag("-DUSE_ESP32") diff --git a/esphome/components/hdc2080/hdc2080.cpp b/esphome/components/hdc2080/hdc2080.cpp index dcb207e099..bf3a4bc79f 100644 --- a/esphome/components/hdc2080/hdc2080.cpp +++ b/esphome/components/hdc2080/hdc2080.cpp @@ -22,7 +22,7 @@ static constexpr uint8_t MEAS_CONF_HUM = 0x04; // Bits 2:1 = 10: humidity only void HDC2080Component::setup() { const uint8_t data = 0x00; // automatic measurement mode disabled, heater off if (this->write_register(REG_RESET_DRDY_INT_CONF, &data, 1) != 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/heatpumpir/climate.py b/esphome/components/heatpumpir/climate.py index b7e0437480..aa3a08c294 100644 --- a/esphome/components/heatpumpir/climate.py +++ b/esphome/components/heatpumpir/climate.py @@ -125,7 +125,6 @@ async def to_code(config): cg.add(var.set_vertical_default(config[CONF_VERTICAL_DEFAULT])) cg.add(var.set_max_temperature(config[CONF_MAX_TEMPERATURE])) cg.add(var.set_min_temperature(config[CONF_MIN_TEMPERATURE])) - cg.add_build_flag("-Wno-error=overloaded-virtual") cg.add_library("tonia/HeatpumpIR", "1.0.41") if CORE.is_libretiny or CORE.is_esp32: diff --git a/esphome/components/pulse_meter/pulse_meter_sensor.cpp b/esphome/components/pulse_meter/pulse_meter_sensor.cpp index 3fe1c722eb..d6959d1a96 100644 --- a/esphome/components/pulse_meter/pulse_meter_sensor.cpp +++ b/esphome/components/pulse_meter/pulse_meter_sensor.cpp @@ -150,7 +150,7 @@ void IRAM_ATTR PulseMeterSensor::edge_intr(PulseMeterSensor *sensor) { edge_state.last_sent_edge_us_ = now; state.last_detected_edge_us_ = now; state.last_rising_edge_us_ = now; - state.count_++; // NOLINT(clang-diagnostic-deprecated-volatile) + state.count_ += 1; } // This ISR is bound to rising edges, so the pin is high @@ -173,7 +173,7 @@ void IRAM_ATTR PulseMeterSensor::pulse_intr(PulseMeterSensor *sensor) { } else if (length && !pulse_state.latched_ && sensor->last_pin_val_) { // Long enough high edge pulse_state.latched_ = true; state.last_detected_edge_us_ = pulse_state.last_intr_; - state.count_++; // NOLINT(clang-diagnostic-deprecated-volatile) + state.count_ += 1; } // Due to order of operations this includes diff --git a/esphome/components/sim800l/sim800l.cpp b/esphome/components/sim800l/sim800l.cpp index b8e97b1121..13b9888e05 100644 --- a/esphome/components/sim800l/sim800l.cpp +++ b/esphome/components/sim800l/sim800l.cpp @@ -126,7 +126,7 @@ void Sim800LComponent::parse_cmd_(std::string message) { break; } - // Else fall thru ... + [[fallthrough]]; } case STATE_CHECK_SMS: send_cmd_("AT+CMGL=\"ALL\""); diff --git a/esphome/components/tuya/tuya.cpp b/esphome/components/tuya/tuya.cpp index b29905f9a0..fd14844908 100644 --- a/esphome/components/tuya/tuya.cpp +++ b/esphome/components/tuya/tuya.cpp @@ -684,8 +684,10 @@ void Tuya::set_numeric_datapoint_value_(uint8_t datapoint_id, TuyaDatapointType case 4: data.push_back(value >> 24); data.push_back(value >> 16); + [[fallthrough]]; case 2: data.push_back(value >> 8); + [[fallthrough]]; case 1: data.push_back(value >> 0); break; diff --git a/esphome/components/tx20/tx20.cpp b/esphome/components/tx20/tx20.cpp index 353cb31513..3574bd7c2d 100644 --- a/esphome/components/tx20/tx20.cpp +++ b/esphome/components/tx20/tx20.cpp @@ -135,7 +135,7 @@ void Tx20Component::decode_and_publish_() { } if (tx20_se == tx20_sb) { tx20_wind_direction = tx20_se; - if (tx20_wind_direction >= 0 && tx20_wind_direction < 16) { + if (tx20_wind_direction < 16) { wind_cardinal_direction_ = DIRECTIONS[tx20_wind_direction]; } ESP_LOGV(TAG, "WindDirection %d", tx20_wind_direction); @@ -164,7 +164,7 @@ void IRAM_ATTR Tx20ComponentStore::gpio_intr(Tx20ComponentStore *arg) { } arg->buffer[arg->buffer_index] = 1; arg->start_time = now; - arg->buffer_index++; // NOLINT(clang-diagnostic-deprecated-volatile) + arg->buffer_index += 1; return; } const uint32_t delay = now - arg->start_time; @@ -195,7 +195,7 @@ void IRAM_ATTR Tx20ComponentStore::gpio_intr(Tx20ComponentStore *arg) { } arg->spent_time += delay; arg->start_time = now; - arg->buffer_index++; // NOLINT(clang-diagnostic-deprecated-volatile) + arg->buffer_index += 1; } void IRAM_ATTR Tx20ComponentStore::reset() { tx20_available = false; diff --git a/esphome/components/usb_uart/usb_uart.h b/esphome/components/usb_uart/usb_uart.h index e88c41c0cb..fb8425f6cd 100644 --- a/esphome/components/usb_uart/usb_uart.h +++ b/esphome/components/usb_uart/usb_uart.h @@ -135,7 +135,7 @@ class USBUartChannel : public uart::UARTComponent, public Parentedarg(ESPHOME_F("detail")) == "all" ? DETAIL_ALL : DETAIL_STATE; } diff --git a/esphome/components/web_server_idf/web_server_idf.cpp b/esphome/components/web_server_idf/web_server_idf.cpp index e1d3e4bf34..c32acaf03e 100644 --- a/esphome/components/web_server_idf/web_server_idf.cpp +++ b/esphome/components/web_server_idf/web_server_idf.cpp @@ -66,7 +66,7 @@ namespace { * - HTTPD_SOCK_ERR_TIMEOUT if the send buffer is full (EAGAIN/EWOULDBLOCK). * - HTTPD_SOCK_ERR_FAIL for other errors. */ -int nonblocking_send(httpd_handle_t hd, int sockfd, const char *buf, size_t buf_len, int flags) { +[[maybe_unused]] int nonblocking_send(httpd_handle_t hd, int sockfd, const char *buf, size_t buf_len, int flags) { if (buf == nullptr) { return HTTPD_SOCK_ERR_INVALID; } diff --git a/esphome/components/wiegand/wiegand.cpp b/esphome/components/wiegand/wiegand.cpp index e5c29f8b11..df64cd48aa 100644 --- a/esphome/components/wiegand/wiegand.cpp +++ b/esphome/components/wiegand/wiegand.cpp @@ -11,7 +11,7 @@ static const char *const KEYS = "0123456789*#"; void IRAM_ATTR HOT WiegandStore::d0_gpio_intr(WiegandStore *arg) { if (arg->d0.digital_read()) return; - arg->count++; // NOLINT(clang-diagnostic-deprecated-volatile) + arg->count += 1; arg->value <<= 1; arg->last_bit_time = millis(); arg->done = false; @@ -20,7 +20,7 @@ void IRAM_ATTR HOT WiegandStore::d0_gpio_intr(WiegandStore *arg) { void IRAM_ATTR HOT WiegandStore::d1_gpio_intr(WiegandStore *arg) { if (arg->d1.digital_read()) return; - arg->count++; // NOLINT(clang-diagnostic-deprecated-volatile) + arg->count += 1; arg->value = (arg->value << 1) | 1; arg->last_bit_time = millis(); arg->done = false; From d5c6efb2fe2891cde35aeae11654b776947f3d8c Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 14 May 2026 18:40:40 -0400 Subject: [PATCH 06/24] [tests] Fix -Wformat= mismatches in test YAML lambdas/logger.log (#16435) --- esphome/components/logger/__init__.py | 14 +++++----- esphome/components/lvgl/helpers.py | 28 +++++++++---------- .../components/esp32_ble_tracker/common.yaml | 4 +-- tests/components/ld2412/common.yaml | 2 +- tests/components/lvgl/lvgl-package.yaml | 6 ++-- tests/components/modbus_server/common.yaml | 2 +- tests/components/mqtt/common.yaml | 2 +- tests/components/nextion/common.yaml | 2 +- .../remote_receiver/common-actions.yaml | 2 +- tests/components/script/common.yaml | 2 +- tests/components/udp/common.yaml | 2 +- tests/components/udp/test.host.yaml | 2 +- 12 files changed, 34 insertions(+), 34 deletions(-) diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index 9d7dc8d92c..c6c440564a 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -506,13 +506,13 @@ async def _late_logger_init(config: ConfigType) -> None: def validate_printf(value): # https://stackoverflow.com/questions/30011379/how-can-i-parse-a-c-format-string-in-python cfmt = r""" - ( # start of capture group 1 - % # literal "%" - (?:[-+0 #]{0,5}) # optional flags - (?:\d+|\*)? # width - (?:\.(?:\d+|\*))? # precision - (?:h|l|ll|w|I|I32|I64)? # size - [cCdiouxXeEfgGaAnpsSZ] # type + ( # start of capture group 1 + % # literal "%" + (?:[-+0 #]{0,5}) # optional flags + (?:\d+|\*)? # width + (?:\.(?:\d+|\*))? # precision + (?:hh|h|ll|l|j|z|t|L|w|I|I32|I64)? # size + [cCdiouxXeEfgGaAnpsSZ] # type ) """ # noqa matches = re.findall(cfmt, value[CONF_FORMAT], flags=re.VERBOSE) diff --git a/esphome/components/lvgl/helpers.py b/esphome/components/lvgl/helpers.py index baa618d472..6f70a1e3bd 100644 --- a/esphome/components/lvgl/helpers.py +++ b/esphome/components/lvgl/helpers.py @@ -9,13 +9,13 @@ CONF_IF_NAN = "if_nan" # noqa f_regex = re.compile( r""" - ( # start of capture group 1 - % # literal "%" - [-+0 #]{0,5} # optional flags - (?:\d+|\*)? # width - (?:\.(?:\d+|\*))? # precision - (?:h|l|ll|w|I|I32|I64)? # size - f # type + ( # start of capture group 1 + % # literal "%" + [-+0 #]{0,5} # optional flags + (?:\d+|\*)? # width + (?:\.(?:\d+|\*))? # precision + (?:hh|h|ll|l|j|z|t|L|w|I|I32|I64)? # size + f # type ) """, flags=re.VERBOSE, @@ -23,13 +23,13 @@ f_regex = re.compile( # noqa c_regex = re.compile( r""" - ( # start of capture group 1 - % # literal "%" - [-+0 #]{0,5} # optional flags - (?:\d+|\*)? # width - (?:\.(?:\d+|\*))? # precision - (?:h|l|ll|w|I|I32|I64)? # size - [cCdiouxXeEfgGaAnpsSZ] # type + ( # start of capture group 1 + % # literal "%" + [-+0 #]{0,5} # optional flags + (?:\d+|\*)? # width + (?:\.(?:\d+|\*))? # precision + (?:hh|h|ll|l|j|z|t|L|w|I|I32|I64)? # size + [cCdiouxXeEfgGaAnpsSZ] # type ) """, flags=re.VERBOSE, diff --git a/tests/components/esp32_ble_tracker/common.yaml b/tests/components/esp32_ble_tracker/common.yaml index 018bbb42b3..564cf1f6ea 100644 --- a/tests/components/esp32_ble_tracker/common.yaml +++ b/tests/components/esp32_ble_tracker/common.yaml @@ -29,12 +29,12 @@ esp32_ble_tracker: - service_uuid: ABCD then: - lambda: !lambda |- - ESP_LOGD("main", "Length of service data is %i", x.size()); + ESP_LOGD("main", "Length of service data is %zu", x.size()); on_ble_manufacturer_data_advertise: - manufacturer_id: ABCD then: - lambda: !lambda |- - ESP_LOGD("main", "Length of manufacturer data is %i", x.size()); + ESP_LOGD("main", "Length of manufacturer data is %zu", x.size()); on_scan_end: - then: - lambda: |- diff --git a/tests/components/ld2412/common.yaml b/tests/components/ld2412/common.yaml index c5bda688dc..7a86b6fbda 100644 --- a/tests/components/ld2412/common.yaml +++ b/tests/components/ld2412/common.yaml @@ -123,7 +123,7 @@ select: - lambda: |- id(uart_bus).flush(); uint32_t new_baud_rate = stoi(x); - ESP_LOGD("change_baud_rate", "Changing baud rate from %i to %i",id(uart_bus).get_baud_rate(), new_baud_rate); + ESP_LOGD("change_baud_rate", "Changing baud rate from %" PRIu32 " to %" PRIu32, id(uart_bus).get_baud_rate(), new_baud_rate); if (id(uart_bus).get_baud_rate() != new_baud_rate) { id(uart_bus).set_baud_rate(new_baud_rate); #if defined(USE_ESP8266) || defined(USE_ESP32) diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml index 53984bb006..0f4b961297 100644 --- a/tests/components/lvgl/lvgl-package.yaml +++ b/tests/components/lvgl/lvgl-package.yaml @@ -660,13 +660,13 @@ lvgl: on_release: logger.log: format: Button released at %d/%d - args: [point.x, point.y] + args: ['(int) point.x', '(int) point.y'] on_long_press_repeat: logger.log: Button clicked on_pressing: logger.log: format: Button pressing at %d/%d - args: [point.x, point.y] + args: ['(int) point.x', '(int) point.y'] on_press_lost: logger.log: Button press lost on_single_click: @@ -944,7 +944,7 @@ lvgl: on_release: logger.log: format: Slider released at %d/%d with value %.0f - args: [point.x, point.y, x] + args: ['(int) point.x', '(int) point.y', x] - button: styles: spin_button id: spin_up diff --git a/tests/components/modbus_server/common.yaml b/tests/components/modbus_server/common.yaml index 3522c9248c..2e4a81a1aa 100644 --- a/tests/components/modbus_server/common.yaml +++ b/tests/components/modbus_server/common.yaml @@ -21,7 +21,7 @@ modbus_server: read_lambda: |- return 31; write_lambda: |- - printf("address=%d, value=%d", x); + printf("address=%d, value=%" PRId32 "\n", (int) address, x); return true; - id: modbus_server4 modbus_id: mod_bus2 diff --git a/tests/components/mqtt/common.yaml b/tests/components/mqtt/common.yaml index 8c58e9b080..6af2ce3939 100644 --- a/tests/components/mqtt/common.yaml +++ b/tests/components/mqtt/common.yaml @@ -64,7 +64,7 @@ mqtt: topic: some/topic payload: Good-bye - lambda: |- - ESP_LOGD("MQTT", "Disconnect reason %d", reason); + ESP_LOGD("MQTT", "Disconnect reason %d", (int) reason); publish_nan_as_none: false binary_sensor: diff --git a/tests/components/nextion/common.yaml b/tests/components/nextion/common.yaml index 0616b9a41a..fba6a22b97 100644 --- a/tests/components/nextion/common.yaml +++ b/tests/components/nextion/common.yaml @@ -299,7 +299,7 @@ display: - lambda: |- // key: StringRef, value: int32_t if (key == "temperature_raw") { - ESP_LOGD("nextion.custom", "%s=%d", key.c_str(), value); + ESP_LOGD("nextion.custom", "%s=%" PRId32, key.c_str(), value); } on_custom_binary_sensor: then: diff --git a/tests/components/remote_receiver/common-actions.yaml b/tests/components/remote_receiver/common-actions.yaml index 30b99eeb70..26a02d4dab 100644 --- a/tests/components/remote_receiver/common-actions.yaml +++ b/tests/components/remote_receiver/common-actions.yaml @@ -12,7 +12,7 @@ on_brennenstuhl: then: - logger.log: format: "on_brennenstuhl: %u" - args: ["x.code"] + args: ["(unsigned) x.code"] on_aeha: then: - logger.log: diff --git a/tests/components/script/common.yaml b/tests/components/script/common.yaml index c1dc68513f..f4818e2296 100644 --- a/tests/components/script/common.yaml +++ b/tests/components/script/common.yaml @@ -49,7 +49,7 @@ script: then: - lambda: |- ESP_LOGD("main", "ints=%d floats=%f bools=%d strings=%s", - ints[0], floats[0], bools[0], strings[0].c_str()); + ints[0], floats[0], (int) bools[0], strings[0].c_str()); - id: my_script_with_params parameters: prefix: string diff --git a/tests/components/udp/common.yaml b/tests/components/udp/common.yaml index 3466e8d2ee..a40ca455cb 100644 --- a/tests/components/udp/common.yaml +++ b/tests/components/udp/common.yaml @@ -11,7 +11,7 @@ udp: - "10.0.0.255" on_receive: - logger.log: - format: "Received %d bytes" + format: "Received %zu bytes" args: [data.size()] - udp.write: id: my_udp diff --git a/tests/components/udp/test.host.yaml b/tests/components/udp/test.host.yaml index 84e78894e5..825d86c19e 100644 --- a/tests/components/udp/test.host.yaml +++ b/tests/components/udp/test.host.yaml @@ -4,7 +4,7 @@ udp: addresses: ["239.0.60.53"] on_receive: - logger.log: - format: "Received %d bytes" + format: "Received %zu bytes" args: [data.size()] - udp.write: id: my_udp From da8286f5542a9a8a80bfd6e4590707a78b30cd04 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 14 May 2026 18:51:59 -0400 Subject: [PATCH 07/24] [docker] Install libusb-1.0 so ESP-IDF tools can validate openocd (#16424) Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- docker/Dockerfile | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 540d28be7f..25de9472b6 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -13,12 +13,16 @@ RUN git config --system --add safe.directory "*" \ && git config --system advice.detachedHead false # Install build tools for Python packages that require compilation -# (e.g., ruamel.yaml.clibz used by ESP-IDF's idf-component-manager) +# (e.g., ruamel.yaml.clib used by ESP-IDF's idf-component-manager). +# Also install libusb-1.0 at runtime so the ESP-IDF tools installer can +# validate openocd-esp32 (it dynamically links libusb-1.0.so.0); without +# it idf_tools.py rejects the openocd install with exit 127 and aborts +# the whole framework setup. RUN if command -v apk > /dev/null; then \ - apk add --no-cache build-base; \ + apk add --no-cache build-base libusb; \ else \ apt-get update \ - && apt-get install -y --no-install-recommends build-essential \ + && apt-get install -y --no-install-recommends build-essential libusb-1.0-0 \ && rm -rf /var/lib/apt/lists/*; \ fi From 3831aa809f3e5fd425165274d0c2165b58546d8d Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 14 May 2026 18:53:42 -0400 Subject: [PATCH 08/24] [multiple] Fix -Wformat= mismatches in component .cpp sources (#16433) --- esphome/components/bme680_bsec/bme680_bsec.cpp | 4 ++-- .../esp32_hosted/update/esp32_hosted_update.cpp | 6 +++--- esphome/components/esphome/ota/ota_esphome.cpp | 14 +++++++------- esphome/components/fastled_base/fastled_light.cpp | 2 +- esphome/components/inkplate/inkplate.cpp | 2 +- esphome/components/midea/air_conditioner.cpp | 4 ++-- esphome/components/ota/ota_partitions_esp_idf.cpp | 4 ++-- .../components/remote_receiver/remote_receiver.cpp | 8 ++++---- esphome/components/sendspin/sendspin_hub.cpp | 4 ++-- .../total_daily_energy/total_daily_energy.cpp | 2 +- 10 files changed, 25 insertions(+), 25 deletions(-) diff --git a/esphome/components/bme680_bsec/bme680_bsec.cpp b/esphome/components/bme680_bsec/bme680_bsec.cpp index b7f8c0da77..823f32c446 100644 --- a/esphome/components/bme680_bsec/bme680_bsec.cpp +++ b/esphome/components/bme680_bsec/bme680_bsec.cpp @@ -161,7 +161,7 @@ void BME680BSECComponent::dump_config() { " IAQ Mode: %s\n" " Supply Voltage: %sV\n" " Sample Rate: %s\n" - " State Save Interval: %ims", + " State Save Interval: %" PRIu32 "ms", this->temperature_offset_, this->iaq_mode_ == IAQ_MODE_STATIC ? "Static" : "Mobile", this->supply_voltage_ == SUPPLY_VOLTAGE_3V3 ? "3.3" : "1.8", BME680_BSEC_SAMPLE_RATE_LOG(this->sample_rate_), this->state_save_interval_ms_); @@ -461,7 +461,7 @@ int8_t BME680BSECComponent::write_bytes_wrapper(uint8_t devid, uint8_t a_registe } void BME680BSECComponent::delay_ms(uint32_t period) { - ESP_LOGV(TAG, "Delaying for %ums", period); + ESP_LOGV(TAG, "Delaying for %" PRIu32 "ms", period); delay(period); } diff --git a/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp b/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp index af35d32888..7f3ba77895 100644 --- a/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp +++ b/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp @@ -92,7 +92,7 @@ void Esp32HostedUpdate::setup() { if (esp_hosted_get_coprocessor_fwversion(&ver_info) == ESP_OK) { // 16 bytes: "255.255.255" (11 chars) + null + safety margin char buf[16]; - snprintf(buf, sizeof(buf), "%d.%d.%d", ver_info.major1, ver_info.minor1, ver_info.patch1); + snprintf(buf, sizeof(buf), "%" PRIu32 ".%" PRIu32 ".%" PRIu32, ver_info.major1, ver_info.minor1, ver_info.patch1); this->update_info_.current_version = buf; } else { this->update_info_.current_version = "unknown"; @@ -120,8 +120,8 @@ void Esp32HostedUpdate::setup() { this->state_ = update::UPDATE_STATE_NO_UPDATE; } } else { - ESP_LOGW(TAG, "Invalid app description magic word: 0x%08x (expected 0x%08x)", app_desc->magic_word, - ESP_APP_DESC_MAGIC_WORD); + ESP_LOGW(TAG, "Invalid app description magic word: 0x%08" PRIx32 " (expected 0x%08" PRIx32 ")", + app_desc->magic_word, ESP_APP_DESC_MAGIC_WORD); this->state_ = update::UPDATE_STATE_NO_UPDATE; } } else { diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index f1857ed664..fb0cc2e56d 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -108,8 +108,8 @@ void ESPHomeOTAComponent::dump_config() { ESP_LOGCONFIG(TAG, " Partition access allowed\n" " Running app:\n" - " Partition address: 0x%X\n" - " Used size: %zu bytes (0x%X)", + " Partition address: 0x%" PRIX32 "\n" + " Used size: %zu bytes (0x%zX)", this->running_app_offset_, this->running_app_size_, this->running_app_size_); #ifdef USE_ESP32 @@ -378,7 +378,7 @@ void ESPHomeOTAComponent::handle_data_() { } ota_size = (static_cast(buf[0]) << 24) | (static_cast(buf[1]) << 16) | (static_cast(buf[2]) << 8) | buf[3]; - ESP_LOGV(TAG, "Size is %u bytes", ota_size); + ESP_LOGV(TAG, "Size is %zu bytes", ota_size); #ifndef USE_OTA_PARTITIONS if (ota_type != ota::OTA_TYPE_UPDATE_APP) { @@ -749,7 +749,7 @@ bool ESPHomeOTAComponent::handle_auth_send_() { this->auth_buf_[0] = this->auth_type_; hasher.get_hex(buf); - ESP_LOGV(TAG, "Auth: Nonce is %.*s", hex_size, buf); + ESP_LOGV(TAG, "Auth: Nonce is %.*s", (int) hex_size, buf); } // Try to write auth_type + nonce @@ -809,13 +809,13 @@ bool ESPHomeOTAComponent::handle_auth_read_() { hasher.add(nonce, hex_size * 2); // Add both nonce and cnonce (contiguous in buffer) hasher.calculate(); - ESP_LOGV(TAG, "Auth: CNonce is %.*s", hex_size, cnonce); + ESP_LOGV(TAG, "Auth: CNonce is %.*s", (int) hex_size, cnonce); #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE char computed_hash[SHA256_HEX_SIZE + 1]; // Buffer for hex-encoded hash (max expected length + null terminator) hasher.get_hex(computed_hash); - ESP_LOGV(TAG, "Auth: Result is %.*s", hex_size, computed_hash); + ESP_LOGV(TAG, "Auth: Result is %.*s", (int) hex_size, computed_hash); #endif - ESP_LOGV(TAG, "Auth: Response is %.*s", hex_size, response); + ESP_LOGV(TAG, "Auth: Response is %.*s", (int) hex_size, response); // Compare response bool matches = hasher.equals_hex(response); diff --git a/esphome/components/fastled_base/fastled_light.cpp b/esphome/components/fastled_base/fastled_light.cpp index 8d1dd49dad..0fa69a23b4 100644 --- a/esphome/components/fastled_base/fastled_light.cpp +++ b/esphome/components/fastled_base/fastled_light.cpp @@ -19,7 +19,7 @@ void FastLEDLightOutput::dump_config() { ESP_LOGCONFIG(TAG, "FastLED light:\n" " Num LEDs: %u\n" - " Max refresh rate: %u", + " Max refresh rate: %" PRIu32, this->num_leds_, this->max_refresh_rate_.value_or(0)); } void FastLEDLightOutput::write_state(light::LightState *state) { diff --git a/esphome/components/inkplate/inkplate.cpp b/esphome/components/inkplate/inkplate.cpp index 39110ca83b..2e837fb614 100644 --- a/esphome/components/inkplate/inkplate.cpp +++ b/esphome/components/inkplate/inkplate.cpp @@ -319,7 +319,7 @@ void Inkplate::fill(Color color) { memset(this->partial_buffer_, fill, this->get_buffer_length_()); } - ESP_LOGV(TAG, "Fill finished (%ums)", millis() - start_time); + ESP_LOGV(TAG, "Fill finished (%" PRIu32 "ms)", millis() - start_time); } void Inkplate::display() { diff --git a/esphome/components/midea/air_conditioner.cpp b/esphome/components/midea/air_conditioner.cpp index 594f7fa661..7603dd5254 100644 --- a/esphome/components/midea/air_conditioner.cpp +++ b/esphome/components/midea/air_conditioner.cpp @@ -130,8 +130,8 @@ ClimateTraits AirConditioner::traits() { void AirConditioner::dump_config() { ESP_LOGCONFIG(Constants::TAG, "MideaDongle:\n" - " [x] Period: %dms\n" - " [x] Response timeout: %dms\n" + " [x] Period: %" PRIu32 "ms\n" + " [x] Response timeout: %" PRIu32 "ms\n" " [x] Request attempts: %d", this->base_.getPeriod(), this->base_.getTimeout(), this->base_.getNumAttempts()); #ifdef USE_REMOTE_TRANSMITTER diff --git a/esphome/components/ota/ota_partitions_esp_idf.cpp b/esphome/components/ota/ota_partitions_esp_idf.cpp index f91e88bde0..a7fc709313 100644 --- a/esphome/components/ota/ota_partitions_esp_idf.cpp +++ b/esphome/components/ota/ota_partitions_esp_idf.cpp @@ -210,7 +210,7 @@ OTAResponseTypes IDFOTABackend::update_partition_table() { ESP_LOGE(TAG, "Cannot resolve running app partition at address 0x%" PRIX32, running_app_offset); return OTA_RESPONSE_ERROR_PARTITION_TABLE_UPDATE; } - ESP_LOGD(TAG, "Copying running app from 0x%X to 0x%X (size: 0x%X)", running_app_part->address, + ESP_LOGD(TAG, "Copying running app from 0x%" PRIX32 " to 0x%" PRIX32 " (size: 0x%zX)", running_app_part->address, plan.copy_dest_part->address, running_app_size); err = esp_partition_copy(plan.copy_dest_part, 0, running_app_part, 0, running_app_size); if (err != ESP_OK) { @@ -261,7 +261,7 @@ OTAResponseTypes IDFOTABackend::update_partition_table() { ESP_LOGE(TAG, "Selected app partition not found after partition table update"); return OTA_RESPONSE_ERROR_PARTITION_TABLE_UPDATE; } - ESP_LOGD(TAG, "Setting next boot partition to 0x%X", new_boot_partition->address); + ESP_LOGD(TAG, "Setting next boot partition to 0x%" PRIX32, new_boot_partition->address); err = esp_ota_set_boot_partition(new_boot_partition); if (err != ESP_OK) { ESP_LOGE(TAG, "esp_ota_set_boot_partition failed (err=0x%X)", err); diff --git a/esphome/components/remote_receiver/remote_receiver.cpp b/esphome/components/remote_receiver/remote_receiver.cpp index d59ee63695..222dae8f7f 100644 --- a/esphome/components/remote_receiver/remote_receiver.cpp +++ b/esphome/components/remote_receiver/remote_receiver.cpp @@ -78,10 +78,10 @@ void RemoteReceiverComponent::setup() { void RemoteReceiverComponent::dump_config() { ESP_LOGCONFIG(TAG, "Remote Receiver:\n" - " Buffer Size: %u\n" - " Tolerance: %u%s\n" - " Filter out pulses shorter than: %u us\n" - " Signal is done after %u us of no changes", + " Buffer Size: %" PRIu32 "\n" + " Tolerance: %" PRIu32 "%s\n" + " Filter out pulses shorter than: %" PRIu32 " us\n" + " Signal is done after %" PRIu32 " us of no changes", this->buffer_size_, this->tolerance_, (this->tolerance_mode_ == remote_base::TOLERANCE_MODE_TIME) ? " us" : "%", this->filter_us_, this->idle_us_); diff --git a/esphome/components/sendspin/sendspin_hub.cpp b/esphome/components/sendspin/sendspin_hub.cpp index 04426b8b1d..57709306cd 100644 --- a/esphome/components/sendspin/sendspin_hub.cpp +++ b/esphome/components/sendspin/sendspin_hub.cpp @@ -153,7 +153,7 @@ bool SendspinHub::save_last_server_hash(uint32_t hash) { LastPlayedServerPref pref{.server_id_hash = hash}; bool ok = this->last_played_server_pref_.save(&pref); if (ok) { - ESP_LOGD(TAG, "Persisted last played server hash: 0x%08X", hash); + ESP_LOGD(TAG, "Persisted last played server hash: 0x%08" PRIX32, hash); } else { ESP_LOGW(TAG, "Failed to persist last played server hash"); } @@ -164,7 +164,7 @@ bool SendspinHub::save_last_server_hash(uint32_t hash) { std::optional SendspinHub::load_last_server_hash() { LastPlayedServerPref pref{}; if (this->last_played_server_pref_.load(&pref)) { - ESP_LOGI(TAG, "Loaded last played server hash: 0x%08X", pref.server_id_hash); + ESP_LOGI(TAG, "Loaded last played server hash: 0x%08" PRIX32, pref.server_id_hash); return pref.server_id_hash; } return std::nullopt; diff --git a/esphome/components/total_daily_energy/total_daily_energy.cpp b/esphome/components/total_daily_energy/total_daily_energy.cpp index 161c712cc1..7c9dbb604f 100644 --- a/esphome/components/total_daily_energy/total_daily_energy.cpp +++ b/esphome/components/total_daily_energy/total_daily_energy.cpp @@ -72,7 +72,7 @@ void TotalDailyEnergy::schedule_midnight_reset_() { timeout_seconds = seconds_until_midnight + 1; } - ESP_LOGD(TAG, "Scheduling midnight check in %us", timeout_seconds); + ESP_LOGD(TAG, "Scheduling midnight check in %" PRIu32 "s", timeout_seconds); this->set_timeout(TIMEOUT_ID_MIDNIGHT, timeout_seconds * MILLIS_PER_SECOND, [this]() { this->schedule_midnight_reset_(); }); } From ecac6b64ec87b1e9d22a2181e724e1cb93f2b15e Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 14 May 2026 22:41:36 -0400 Subject: [PATCH 09/24] [espidf] Gate esp_idf_size --ng on IDF version (#16441) --- esphome/build_gen/espidf.py | 10 ++++++++-- tests/unit_tests/build_gen/test_espidf.py | 7 ++++++- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/esphome/build_gen/espidf.py b/esphome/build_gen/espidf.py index 5ad2072c5b..96f84ebbd1 100644 --- a/esphome/build_gen/espidf.py +++ b/esphome/build_gen/espidf.py @@ -3,7 +3,8 @@ import json from pathlib import Path -from esphome.components.esp32 import get_esp32_variant +from esphome.components.esp32 import get_esp32_variant, idf_version +import esphome.config_validation as cv from esphome.core import CORE from esphome.helpers import mkdir_p, write_file_if_changed from esphome.writer import update_storage_json @@ -61,6 +62,11 @@ def get_project_cmakelists(minimal: bool = False) -> str: variant = get_esp32_variant() idf_target = variant.lower().replace("-", "") + # esp_idf_size 2.x (bundled with IDF >=6.0) made NG the default and + # removed the --ng flag; on 1.x (IDF 5.5) --ng is required to get + # --format=raw because the legacy mode doesn't support it. + size_ng_flag = "--ng" if idf_version() < cv.Version(6, 0, 0) else "" + # Project-wide compile options: -D defines and -W warning flags (skip # -Wl, linker flags — those go on the src component via # target_link_options below). Emitted via idf_build_set_property so the @@ -146,7 +152,7 @@ project({CORE.name}) # Emit raw JSON size data for ESPHome to read post-build. add_custom_command( TARGET ${{CMAKE_PROJECT_NAME}}.elf POST_BUILD - COMMAND ${{PYTHON}} -m esp_idf_size --ng --format=raw + COMMAND ${{PYTHON}} -m esp_idf_size {size_ng_flag} --format=raw -o ${{CMAKE_BINARY_DIR}}/esp_idf_size.json ${{CMAKE_PROJECT_NAME}}.map WORKING_DIRECTORY ${{CMAKE_BINARY_DIR}} diff --git a/tests/unit_tests/build_gen/test_espidf.py b/tests/unit_tests/build_gen/test_espidf.py index 36f0442355..540dd06731 100644 --- a/tests/unit_tests/build_gen/test_espidf.py +++ b/tests/unit_tests/build_gen/test_espidf.py @@ -11,10 +11,12 @@ import pytest from esphome.components.esp32 import ( KEY_COMPONENTS, KEY_ESP32, + KEY_IDF_VERSION, KEY_PATH, KEY_REF, KEY_REPO, ) +import esphome.config_validation as cv from esphome.const import KEY_CORE from esphome.core import CORE @@ -24,7 +26,10 @@ def _reset_core(tmp_path: Path) -> None: """Give each test its own CORE.build_path and a clean esp32 data slot.""" CORE.build_path = str(tmp_path) CORE.data.setdefault(KEY_CORE, {}) - CORE.data[KEY_ESP32] = {KEY_COMPONENTS: {}} + CORE.data[KEY_ESP32] = { + KEY_COMPONENTS: {}, + KEY_IDF_VERSION: cv.Version(5, 5, 4), + } def _write_project_description(tmp_path: Path, components: dict[str, str]) -> None: From c037058c199ff660e828474e2b284cca787b5f9b Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 14 May 2026 22:51:14 -0400 Subject: [PATCH 10/24] [esp32_hosted] Bump esp_hosted to 2.12.7 (#16440) --- esphome/components/esp32_hosted/__init__.py | 2 +- esphome/idf_component.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/esp32_hosted/__init__.py b/esphome/components/esp32_hosted/__init__.py index eca7c24b10..71d1fd3ac1 100644 --- a/esphome/components/esp32_hosted/__init__.py +++ b/esphome/components/esp32_hosted/__init__.py @@ -249,7 +249,7 @@ async def to_code(config): esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="1.5.1") esp32.add_idf_component(name="espressif/wifi_remote_over_eppp", ref="0.3.2") esp32.add_idf_component(name="espressif/eppp_link", ref="1.1.5") - esp32.add_idf_component(name="espressif/esp_hosted", ref="2.12.6") + esp32.add_idf_component(name="espressif/esp_hosted", ref="2.12.7") else: esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="0.13.0") esp32.add_idf_component(name="espressif/eppp_link", ref="0.2.0") diff --git a/esphome/idf_component.yml b/esphome/idf_component.yml index 814a4031c1..49c4cdbb2e 100644 --- a/esphome/idf_component.yml +++ b/esphome/idf_component.yml @@ -36,7 +36,7 @@ dependencies: rules: - if: "target in [esp32h2, esp32p4]" espressif/esp_hosted: - version: 2.12.6 + version: 2.12.7 rules: - if: "target in [esp32h2, esp32p4]" zorxx/multipart-parser: From 4f895425cac87e79e7b0396d41994d08181fd438 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Thu, 14 May 2026 23:31:11 -0400 Subject: [PATCH 11/24] [audio] Bump microMP3 to v0.2.1 (#16429) --- esphome/components/audio/__init__.py | 2 +- esphome/idf_component.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/audio/__init__.py b/esphome/components/audio/__init__.py index 44371e87ab..13b379ba3a 100644 --- a/esphome/components/audio/__init__.py +++ b/esphome/components/audio/__init__.py @@ -395,7 +395,7 @@ async def to_code(config): ) if data.mp3_support: cg.add_define("USE_AUDIO_MP3_SUPPORT") - add_idf_component(name="esphome/micro-mp3", ref="0.2.0") + add_idf_component(name="esphome/micro-mp3", ref="0.2.1") _emit_memory_pair( data.mp3.buffer_memory, "CONFIG_MP3_DECODER_PREFER_PSRAM", diff --git a/esphome/idf_component.yml b/esphome/idf_component.yml index 49c4cdbb2e..35c55cbb4d 100644 --- a/esphome/idf_component.yml +++ b/esphome/idf_component.yml @@ -10,7 +10,7 @@ dependencies: esphome/micro-flac: version: 0.2.0 esphome/micro-mp3: - version: 0.2.0 + version: 0.2.1 esphome/micro-opus: version: 0.4.1 esphome/micro-wav: From 25dbef83de8d4168133dbba41cf4d76cc79c6f59 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Thu, 14 May 2026 23:33:36 -0400 Subject: [PATCH 12/24] [sound_level] Use RingBufferAudioSource (#16436) --- .../components/sound_level/sound_level.cpp | 58 ++++++++++--------- esphome/components/sound_level/sound_level.h | 9 +-- 2 files changed, 35 insertions(+), 32 deletions(-) diff --git a/esphome/components/sound_level/sound_level.cpp b/esphome/components/sound_level/sound_level.cpp index fb8bfd3085..a93e396367 100644 --- a/esphome/components/sound_level/sound_level.cpp +++ b/esphome/components/sound_level/sound_level.cpp @@ -11,7 +11,7 @@ namespace esphome::sound_level { static const char *const TAG = "sound_level"; -static const uint32_t AUDIO_BUFFER_DURATION_MS = 30; +static const uint32_t MAX_FILL_DURATION_MS = 30; static const uint32_t RING_BUFFER_DURATION_MS = 120; // Square INT16_MIN since INT16_MIN^2 > INT16_MAX^2 @@ -30,8 +30,7 @@ void SoundLevelComponent::dump_config() { void SoundLevelComponent::setup() { this->microphone_source_->add_data_callback([this](const std::vector &data) { std::shared_ptr temp_ring_buffer = this->ring_buffer_.lock(); - if (this->ring_buffer_.use_count() == 2) { - // ``audio_buffer_`` and ``temp_ring_buffer`` share ownership of a ring buffer, so its safe/useful to write + if (temp_ring_buffer != nullptr) { temp_ring_buffer->write((void *) data.data(), data.size()); } }); @@ -81,10 +80,11 @@ void SoundLevelComponent::loop() { return; } - // Copy data from ring buffer into the transfer buffer - don't block to avoid slowing the main loop - this->audio_buffer_->transfer_data_from_source(0); + // Expose a chunk of the ring buffer's internal storage - don't block to avoid slowing the main loop. + // pre_shift is ignored by RingBufferAudioSource (no intermediate transfer buffer to compact). + this->audio_source_->fill(0, false); - if (this->audio_buffer_->available() == 0) { + if (this->audio_source_->available() == 0) { // No new audio available for processing return; } @@ -92,11 +92,11 @@ void SoundLevelComponent::loop() { const uint32_t samples_in_window = this->microphone_source_->get_audio_stream_info().ms_to_samples(this->measurement_duration_ms_); const uint32_t samples_available_to_process = - this->microphone_source_->get_audio_stream_info().bytes_to_samples(this->audio_buffer_->available()); + this->microphone_source_->get_audio_stream_info().bytes_to_samples(this->audio_source_->available()); const uint32_t samples_to_process = std::min(samples_in_window - this->sample_count_, samples_available_to_process); // MicrophoneSource always provides int16 samples due to Python codegen settings - const int16_t *audio_data = reinterpret_cast(this->audio_buffer_->get_buffer_start()); + const int16_t *audio_data = reinterpret_cast(this->audio_source_->data()); // Process all the new audio samples for (uint32_t i = 0; i < samples_to_process; ++i) { @@ -115,9 +115,8 @@ void SoundLevelComponent::loop() { ++this->sample_count_; } - // Remove the processed samples from ``audio_buffer_`` - this->audio_buffer_->decrease_buffer_length( - this->microphone_source_->get_audio_stream_info().samples_to_bytes(samples_to_process)); + // Remove the processed samples from ``audio_source_`` + this->audio_source_->consume(this->microphone_source_->get_audio_stream_info().samples_to_bytes(samples_to_process)); if (this->sample_count_ == samples_in_window) { // Processed enough samples for the measurement window, compute and publish the sensor values @@ -158,36 +157,39 @@ void SoundLevelComponent::stop() { } bool SoundLevelComponent::start_() { - if (this->audio_buffer_ != nullptr) { + if (this->audio_source_ != nullptr) { return true; } - // Allocate a transfer buffer - this->audio_buffer_ = audio::AudioSourceTransferBuffer::create( - this->microphone_source_->get_audio_stream_info().ms_to_bytes(AUDIO_BUFFER_DURATION_MS)); - if (this->audio_buffer_ == nullptr) { - this->status_momentary_error("transfer_buffer", 15000); + const auto &stream_info = this->microphone_source_->get_audio_stream_info(); + const size_t bytes_per_frame = stream_info.frames_to_bytes(1); + + // Allocate a ring buffer for the microphone callback to write into. Round the size down to a multiple + // of bytes_per_frame so the wrap boundary stays frame-aligned and avoids unnecessary single-frame splices. + this->ring_buffer_.reset(); // Reset pointer to any previous ring buffer allocation + const size_t ring_buffer_size = + (stream_info.ms_to_bytes(RING_BUFFER_DURATION_MS) / bytes_per_frame) * bytes_per_frame; + std::shared_ptr temp_ring_buffer = ring_buffer::RingBuffer::create(ring_buffer_size); + if (temp_ring_buffer == nullptr) { + this->status_momentary_error("ring_buffer", 15000); return false; } - // Allocates a new ring buffer, adds it as a source for the transfer buffer, and points ring_buffer_ to it - this->ring_buffer_.reset(); // Reset pointer to any previous ring buffer allocation - std::shared_ptr temp_ring_buffer = ring_buffer::RingBuffer::create( - this->microphone_source_->get_audio_stream_info().ms_to_bytes(RING_BUFFER_DURATION_MS)); - if (temp_ring_buffer.use_count() == 0) { - this->status_momentary_error("ring_buffer", 15000); - this->stop_(); + // Zero-copy source that reads directly from the ring buffer's internal storage. Frame-aligned reads + // ensure multi-channel frames are never split across the ring buffer's wrap boundary. + this->audio_source_ = audio::RingBufferAudioSource::create( + temp_ring_buffer, stream_info.ms_to_bytes(MAX_FILL_DURATION_MS), static_cast(bytes_per_frame)); + if (this->audio_source_ == nullptr) { + this->status_momentary_error("audio_source", 15000); return false; - } else { - this->ring_buffer_ = temp_ring_buffer; - this->audio_buffer_->set_source(temp_ring_buffer); } + this->ring_buffer_ = temp_ring_buffer; this->status_clear_error(); return true; } -void SoundLevelComponent::stop_() { this->audio_buffer_.reset(); } +void SoundLevelComponent::stop_() { this->audio_source_.reset(); } } // namespace esphome::sound_level diff --git a/esphome/components/sound_level/sound_level.h b/esphome/components/sound_level/sound_level.h index 4f0081a510..aabea62ca4 100644 --- a/esphome/components/sound_level/sound_level.h +++ b/esphome/components/sound_level/sound_level.h @@ -36,11 +36,12 @@ class SoundLevelComponent : public Component { void stop(); protected: - /// @brief Internal start command that, if necessary, allocates ``audio_buffer_`` and a ring buffer which - /// ``audio_buffer_`` owns and ``ring_buffer_`` points to. Returns true if allocations were successful. + /// @brief Internal start command that, if necessary, allocates a ring buffer and a zero-copy + /// ``RingBufferAudioSource`` that reads directly from it. ``ring_buffer_`` weakly references the + /// ring buffer owned by ``audio_source_``. Returns true if allocations were successful. bool start_(); - /// @brief Internal stop command the deallocates ``audio_buffer_`` (which automatically deallocates its ring buffer) + /// @brief Internal stop command that deallocates ``audio_source_`` (which releases its ring buffer) void stop_(); microphone::MicrophoneSource *microphone_source_{nullptr}; @@ -48,7 +49,7 @@ class SoundLevelComponent : public Component { sensor::Sensor *peak_sensor_{nullptr}; sensor::Sensor *rms_sensor_{nullptr}; - std::unique_ptr audio_buffer_; + std::unique_ptr audio_source_; std::weak_ptr ring_buffer_; int32_t squared_peak_{0}; From 50495c7085d618e28fa7f75531eb739673fe5d98 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 15 May 2026 10:20:15 -0700 Subject: [PATCH 13/24] [wifi] Refuse to compile when wifi_ssid is the device-builder placeholder (#16444) --- esphome/__main__.py | 8 +++ esphome/components/wifi/__init__.py | 52 +++++++++++++++- esphome/const.py | 9 +++ tests/unit_tests/components/test_wifi.py | 78 +++++++++++++++++++++++- 4 files changed, 144 insertions(+), 3 deletions(-) diff --git a/esphome/__main__.py b/esphome/__main__.py index d733534a5c..16a05ad552 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -50,6 +50,7 @@ from esphome.const import ( CONF_TOPIC, CONF_USERNAME, CONF_WEB_SERVER, + CONF_WIFI, ENV_NOGITIGNORE, KEY_CORE, KEY_TARGET_PLATFORM, @@ -733,6 +734,13 @@ def write_cpp_file() -> int: def compile_program(args: ArgsProtocol, config: ConfigType) -> int: + # Keep this gate here, NOT in config validation: device-builder needs + # `esphome config` to keep succeeding with placeholders so onboarding can run. + if CONF_WIFI in config: + from esphome.components.wifi import check_placeholder_credentials + + check_placeholder_credentials(config) + # NOTE: "Build path:" format is parsed by script/ci_memory_impact_extract.py # If you change this format, update the regex in that script as well _LOGGER.info("Compiling app... Build path: %s", CORE.build_path) diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index bad57fc481..f9cb391442 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -54,10 +54,18 @@ from esphome.const import ( CONF_TTLS_PHASE_2, CONF_USE_ADDRESS, CONF_USERNAME, + CONF_WIFI, + PLACEHOLDER_WIFI_SSID, Platform, PlatformFramework, ) -from esphome.core import CORE, CoroPriority, HexInt, coroutine_with_priority +from esphome.core import ( + CORE, + CoroPriority, + EsphomeError, + HexInt, + coroutine_with_priority, +) import esphome.final_validate as fv from esphome.types import ConfigType @@ -903,3 +911,45 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform( "wifi_component_pico_w.cpp": {PlatformFramework.RP2040_ARDUINO}, } ) + + +def _placeholder_wifi_credentials(config: ConfigType) -> list[str]: + """Return human-readable locations where the dashboard's placeholder wifi + values still appear. Empty list means no placeholders were found. + """ + placeholders: list[str] = [] + wifi_conf = config.get(CONF_WIFI) + if not wifi_conf: + return placeholders + + for idx, network in enumerate(wifi_conf.get(CONF_NETWORKS, [])): + ssid = network.get(CONF_SSID) + if isinstance(ssid, str) and ssid == PLACEHOLDER_WIFI_SSID: + placeholders.append(f"wifi.networks[{idx}].ssid") + + ap_conf = wifi_conf.get(CONF_AP) + if ap_conf: + ap_ssid = ap_conf.get(CONF_SSID) + if isinstance(ap_ssid, str) and ap_ssid == PLACEHOLDER_WIFI_SSID: + placeholders.append("wifi.ap.ssid") + + return placeholders + + +def check_placeholder_credentials(config: ConfigType) -> None: + """Raise EsphomeError if any wifi credential is the dashboard placeholder. + + Call only at compile time. NEVER from CONFIG_SCHEMA, FINAL_VALIDATE_SCHEMA, + or any path reached by `esphome config`; device-builder relies on + validation passing with the placeholders still in place. + """ + locations = _placeholder_wifi_credentials(config) + if not locations: + return + formatted = ", ".join(locations) + raise EsphomeError( + f"wifi configuration still contains the dashboard placeholder value " + f"'{PLACEHOLDER_WIFI_SSID}' at: {formatted}. " + f"Open secrets.yaml and replace 'wifi_ssid' (and 'wifi_password') " + f"with your real wifi credentials before flashing." + ) diff --git a/esphome/const.py b/esphome/const.py index 91bc52708c..1819502201 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1415,3 +1415,12 @@ ENTITY_CATEGORY_DIAGNOSTIC = "diagnostic" # The corresponding constant exists in c++ # when update_interval is set to never, it becomes SCHEDULER_DONT_RUN milliseconds SCHEDULER_DONT_RUN = 4294967295 + +# Sentinel values written by the esphome-device-builder dashboard into +# secrets.yaml on first boot so that !secret wifi_ssid / !secret wifi_password +# references resolve cleanly through validation before the user has finished +# the onboarding wizard. Compilation refuses if these reach the binary so that +# a user who dismisses onboarding can't accidentally flash a device that will +# never associate with their wifi. +PLACEHOLDER_WIFI_SSID = "REPLACE_WITH_YOUR_WIFI_NETWORK" +PLACEHOLDER_WIFI_PASSWORD = "REPLACE_WITH_YOUR_WIFI_PASSWORD" # noqa: S105 diff --git a/tests/unit_tests/components/test_wifi.py b/tests/unit_tests/components/test_wifi.py index 71a14d7817..9598c1bdd8 100644 --- a/tests/unit_tests/components/test_wifi.py +++ b/tests/unit_tests/components/test_wifi.py @@ -3,8 +3,20 @@ import pytest from esphome.components.esp32 import const -from esphome.components.wifi import has_native_wifi, variant_has_wifi -from esphome.const import Platform +from esphome.components.wifi import ( + check_placeholder_credentials, + has_native_wifi, + variant_has_wifi, +) +from esphome.const import ( + CONF_AP, + CONF_NETWORKS, + CONF_SSID, + CONF_WIFI, + PLACEHOLDER_WIFI_SSID, + Platform, +) +from esphome.core import EsphomeError, Lambda @pytest.mark.parametrize( @@ -123,3 +135,65 @@ def test_has_native_wifi_esp32_without_variant_assumes_wifi() -> None: def test_has_native_wifi_rp2040_without_board_assumes_wifi() -> None: """RP2040 without a board id falls open to True (custom-board default).""" assert has_native_wifi(platform=Platform.RP2040) is True + + +def _wifi_config( + *, + networks: list[dict] | None = None, + ap: dict | None = None, +) -> dict: + """Build a minimal config dict matching the post-validation shape.""" + wifi: dict = {} + if networks is not None: + wifi[CONF_NETWORKS] = networks + if ap is not None: + wifi[CONF_AP] = ap + return {CONF_WIFI: wifi} + + +def test_check_placeholder_credentials_passes_with_real_ssid() -> None: + """A real SSID compiles without complaint.""" + config = _wifi_config(networks=[{CONF_SSID: "home_network"}]) + assert check_placeholder_credentials(config) is None + + +def test_check_placeholder_credentials_refuses_placeholder_ssid() -> None: + """The placeholder SSID is rejected with an actionable message.""" + config = _wifi_config(networks=[{CONF_SSID: PLACEHOLDER_WIFI_SSID}]) + with pytest.raises(EsphomeError) as exc_info: + check_placeholder_credentials(config) + message = str(exc_info.value) + assert "wifi.networks[0].ssid" in message + assert "secrets.yaml" in message + + +def test_check_placeholder_credentials_refuses_placeholder_in_second_network() -> None: + """Index reporting picks the placeholder out of a mixed network list.""" + config = _wifi_config( + networks=[ + {CONF_SSID: "home_network"}, + {CONF_SSID: PLACEHOLDER_WIFI_SSID}, + ], + ) + with pytest.raises(EsphomeError) as exc_info: + check_placeholder_credentials(config) + assert "wifi.networks[1].ssid" in str(exc_info.value) + + +def test_check_placeholder_credentials_refuses_placeholder_ap_ssid() -> None: + """An AP using the placeholder broadcast name is also refused.""" + config = _wifi_config(ap={CONF_SSID: PLACEHOLDER_WIFI_SSID}) + with pytest.raises(EsphomeError) as exc_info: + check_placeholder_credentials(config) + assert "wifi.ap.ssid" in str(exc_info.value) + + +def test_check_placeholder_credentials_no_wifi_passes() -> None: + """Ethernet-only / wifi-less configs skip the check entirely.""" + assert check_placeholder_credentials({}) is None + + +def test_check_placeholder_credentials_skips_template_ssid() -> None: + """A templated (Lambda) SSID is not a string and is skipped.""" + config = _wifi_config(networks=[{CONF_SSID: Lambda('return "x";')}]) + assert check_placeholder_credentials(config) is None From 5ec0879a1049bfccc1179efc05da072327ed5de9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 15 May 2026 10:29:15 -0700 Subject: [PATCH 14/24] [core] Fix KeyError: 'esp32' on upload when validated-config cache is used (#16457) --- esphome/storage_json.py | 13 +++++- tests/unit_tests/test_compiled_config.py | 56 ++++++++++++++++++++++-- 2 files changed, 65 insertions(+), 4 deletions(-) diff --git a/esphome/storage_json.py b/esphome/storage_json.py index 7d26b22f96..e481827080 100644 --- a/esphome/storage_json.py +++ b/esphome/storage_json.py @@ -273,10 +273,21 @@ class StorageJSON: """ CORE.name = self.name CORE.build_path = self.build_path + target_platform = self.core_platform or self.target_platform.lower() CORE.data[KEY_CORE] = { - KEY_TARGET_PLATFORM: self.core_platform or self.target_platform.lower(), + KEY_TARGET_PLATFORM: target_platform, KEY_TARGET_FRAMEWORK: self.framework, } + # The compile pipeline populates CORE.data[KEY_ESP32] when esp32's + # validator runs; on the cache fast path that validator is skipped, + # so populate the variant upload_using_esptool reads via + # esp32.get_esp32_variant(). target_platform on disk is the variant + # (e.g. "ESP32S3"); core_platform is the family (e.g. "esp32"). + if target_platform == const.PLATFORM_ESP32: + from esphome.components.esp32.const import KEY_ESP32 + from esphome.const import KEY_VARIANT + + CORE.data[KEY_ESP32] = {KEY_VARIANT: self.target_platform} def __eq__(self, o) -> bool: return isinstance(o, StorageJSON) and self.as_dict() == o.as_dict() diff --git a/tests/unit_tests/test_compiled_config.py b/tests/unit_tests/test_compiled_config.py index 34e811b97b..8c9cfa8101 100644 --- a/tests/unit_tests/test_compiled_config.py +++ b/tests/unit_tests/test_compiled_config.py @@ -22,6 +22,7 @@ from esphome.const import ( KEY_CORE, KEY_TARGET_FRAMEWORK, KEY_TARGET_PLATFORM, + KEY_VARIANT, ) from esphome.core import CORE @@ -47,7 +48,12 @@ wifi: """ -def _write_storage(storage_path: Path) -> None: +def _write_storage( + storage_path: Path, + *, + esp_platform: str = "ESP32", + core_platform: str | None = "esp32", +) -> None: """Write a vanilla StorageJSON sidecar for the cache tests.""" storage_path.parent.mkdir(parents=True, exist_ok=True) data = { @@ -59,14 +65,14 @@ def _write_storage(storage_path: Path) -> None: "src_version": 1, "address": "192.168.1.42", "web_port": None, - "esp_platform": "ESP32", + "esp_platform": esp_platform, "build_path": "/build/lite_test", "firmware_bin_path": "/build/lite_test/firmware.bin", "loaded_integrations": ["api", "logger", "ota", "wifi"], "loaded_platforms": [], "no_mdns": False, "framework": "arduino", - "core_platform": "esp32", + "core_platform": core_platform, } storage_path.write_text(json.dumps(data)) @@ -123,6 +129,50 @@ def test_load_compiled_config_happy_path(fresh_cache_files: Path) -> None: assert CORE.build_path == Path("/build/lite_test") assert CORE.data[KEY_CORE][KEY_TARGET_PLATFORM] == "esp32" assert CORE.data[KEY_CORE][KEY_TARGET_FRAMEWORK] == "arduino" + # upload_using_esptool reads get_esp32_variant() off CORE.data[KEY_ESP32]. + from esphome.components.esp32.const import KEY_ESP32 + + assert CORE.data[KEY_ESP32][KEY_VARIANT] == "ESP32" + + +def test_load_compiled_config_populates_esp32_variant(tmp_path: Path) -> None: + """ESP32 variants survive the cache fast path so esptool gets the right --chip.""" + from esphome.components.esp32.const import KEY_ESP32 + + yaml_path = tmp_path / "lite_test.yaml" + yaml_path.write_text("esphome:\n name: lite_test\n") + CORE.config_path = yaml_path + + storage_dir = tmp_path / ".esphome" / "storage" + _write_storage(storage_dir / "lite_test.yaml.json", esp_platform="ESP32S3") + cache = _write_cache(storage_dir / "lite_test.yaml.validated.yaml") + _set_cache_mtime(cache, yaml_path, offset=5) + + assert load_compiled_config(yaml_path) is not None + assert CORE.data[KEY_ESP32][KEY_VARIANT] == "ESP32S3" + + +def test_load_compiled_config_skips_esp32_block_for_other_platforms( + tmp_path: Path, +) -> None: + """Non-esp32 targets shouldn't fabricate an esp32 data block.""" + from esphome.components.esp32.const import KEY_ESP32 + + yaml_path = tmp_path / "lite_test.yaml" + yaml_path.write_text("esphome:\n name: lite_test\n") + CORE.config_path = yaml_path + + storage_dir = tmp_path / ".esphome" / "storage" + _write_storage( + storage_dir / "lite_test.yaml.json", + esp_platform="ESP8266", + core_platform="esp8266", + ) + cache = _write_cache(storage_dir / "lite_test.yaml.validated.yaml") + _set_cache_mtime(cache, yaml_path, offset=5) + + assert load_compiled_config(yaml_path) is not None + assert KEY_ESP32 not in CORE.data @pytest.mark.parametrize( From c6a74222f1c198cd2032fc7f6a89f6eddbf28477 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 15 May 2026 13:32:51 -0400 Subject: [PATCH 15/24] [esp32_hosted][fingerprint_grow] Fix two remaining ESP32 toolchain warnings (#16442) --- esphome/components/esp32_hosted/update/esp32_hosted_update.cpp | 2 +- esphome/components/fingerprint_grow/fingerprint_grow.cpp | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp b/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp index 7f3ba77895..70fa41b312 100644 --- a/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp +++ b/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp @@ -121,7 +121,7 @@ void Esp32HostedUpdate::setup() { } } else { ESP_LOGW(TAG, "Invalid app description magic word: 0x%08" PRIx32 " (expected 0x%08" PRIx32 ")", - app_desc->magic_word, ESP_APP_DESC_MAGIC_WORD); + app_desc->magic_word, static_cast(ESP_APP_DESC_MAGIC_WORD)); this->state_ = update::UPDATE_STATE_NO_UPDATE; } } else { diff --git a/esphome/components/fingerprint_grow/fingerprint_grow.cpp b/esphome/components/fingerprint_grow/fingerprint_grow.cpp index 3f57789034..b38d42191b 100644 --- a/esphome/components/fingerprint_grow/fingerprint_grow.cpp +++ b/esphome/components/fingerprint_grow/fingerprint_grow.cpp @@ -206,6 +206,7 @@ uint8_t FingerprintGrowComponent::save_fingerprint_() { break; case ENROLL_MISMATCH: ESP_LOGE(TAG, "Scans do not match"); + [[fallthrough]]; default: return this->data_[0]; } From 26907f17f5833f08a8b9b7e3baf0f9900f22aafb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 15 May 2026 14:42:51 -0700 Subject: [PATCH 16/24] Bump aioesphomeapi from 45.0.0 to 45.0.1 (#16467) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 6291b5cd41..ae50c4046b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.19 esptool==5.2.0 click==8.3.3 esphome-dashboard==20260425.0 -aioesphomeapi==45.0.0 +aioesphomeapi==45.0.1 zeroconf==0.148.0 puremagic==1.30 ruamel.yaml==0.19.1 # dashboard_import From 6a8f24b951ea396e60b6ef1f2ec1acce5be2f5b6 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Sat, 16 May 2026 08:30:04 +1000 Subject: [PATCH 17/24] [ft5x06] Fix setting calibration values (#16446) --- .../ft5x06/touchscreen/ft5x06_touchscreen.cpp | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/esphome/components/ft5x06/touchscreen/ft5x06_touchscreen.cpp b/esphome/components/ft5x06/touchscreen/ft5x06_touchscreen.cpp index 835dc4aac0..24d3529fb4 100644 --- a/esphome/components/ft5x06/touchscreen/ft5x06_touchscreen.cpp +++ b/esphome/components/ft5x06/touchscreen/ft5x06_touchscreen.cpp @@ -15,6 +15,16 @@ void FT5x06Touchscreen::setup() { this->attach_interrupt_(this->interrupt_pin_, gpio::INTERRUPT_FALLING_EDGE); } + // reading the chip registers to get max x/y does not seem to work. + if (this->display_ != nullptr) { + if (this->x_raw_max_ == this->x_raw_min_) { + this->x_raw_max_ = this->display_->get_native_width(); + } + if (this->y_raw_max_ == this->y_raw_min_) { + this->y_raw_max_ = this->display_->get_native_height(); + } + } + // wait 200ms after reset. this->set_timeout(200, [this] { this->continue_setup_(); }); } @@ -39,15 +49,6 @@ void FT5x06Touchscreen::continue_setup_() { this->mark_failed(); return; } - // reading the chip registers to get max x/y does not seem to work. - if (this->display_ != nullptr) { - if (this->x_raw_max_ == this->x_raw_min_) { - this->x_raw_max_ = this->display_->get_native_width(); - } - if (this->y_raw_max_ == this->y_raw_min_) { - this->y_raw_max_ = this->display_->get_native_height(); - } - } } void FT5x06Touchscreen::update_touches() { @@ -71,7 +72,7 @@ void FT5x06Touchscreen::update_touches() { uint16_t x = encode_uint16(data[i][0] & 0x0F, data[i][1]); uint16_t y = encode_uint16(data[i][2] & 0xF, data[i][3]); - ESP_LOGD(TAG, "Read %X status, id: %d, pos %d/%d", status, id, x, y); + ESP_LOGV(TAG, "Read %X status, id: %d, pos %d/%d", status, id, x, y); if (status == 0 || status == 2) { this->add_raw_touch_position_(id, x, y); } From da237b5070cad2c0f2c430d7f54e77cf6bf8ca6b Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Sat, 16 May 2026 08:36:53 +1000 Subject: [PATCH 18/24] [lvgl] Fix image define (#16468) --- esphome/components/lvgl/__init__.py | 5 +++++ esphome/components/lvgl/lvgl_esphome.h | 10 ++++++---- esphome/components/lvgl/styles.py | 2 ++ 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/esphome/components/lvgl/__init__.py b/esphome/components/lvgl/__init__.py index 91b101cd25..4277c14dd7 100644 --- a/esphome/components/lvgl/__init__.py +++ b/esphome/components/lvgl/__init__.py @@ -55,6 +55,7 @@ from .automation import layers_to_code, lvgl_update from .defines import ( CONF_ALIGN_TO_LAMBDA_ID, LOGGER, + add_lv_use, get_focused_widgets, get_lv_images_used, get_refreshed_widgets, @@ -71,6 +72,7 @@ from .keypads import KEYPADS_CONFIG, keypads_to_code from .lv_validation import lv_bool from .lvcode import LvContext, LvglComponent, lv_event_t_ptr, lvgl_static from .schemas import ( + BASE_PROPS, DISP_BG_SCHEMA, FULL_STYLE_SCHEMA, STYLE_REMAP, @@ -100,6 +102,7 @@ from .widgets import ( get_screen_active, set_obj_properties, ) +from .widgets.img import CONF_IMAGE # Import only what we actually use directly in this file from .widgets.msgbox import MSGBOX_SCHEMA, msgboxes_to_code @@ -433,6 +436,8 @@ async def to_code(configs): # This must be done after all widgets are created styles_used = df.get_styles_used() + if any(BASE_PROPS.get(x) is lvalid.lv_image for x in styles_used): + add_lv_use(CONF_IMAGE) for use in df.get_lv_uses(): df.add_define(f"LV_USE_{use.upper()}") cg.add_define(f"USE_LVGL_{use.upper()}") diff --git a/esphome/components/lvgl/lvgl_esphome.h b/esphome/components/lvgl/lvgl_esphome.h index 218f9a60ab..3f7f1dce14 100644 --- a/esphome/components/lvgl/lvgl_esphome.h +++ b/esphome/components/lvgl/lvgl_esphome.h @@ -74,11 +74,11 @@ inline void lv_style_set_text_font(lv_style_t *style, const font::Font *font) { lv_style_set_text_font(style, font->get_lv_font()); } #endif -#if defined(USE_LVGL_IMAGE) && defined(USE_IMAGE) -#if LV_USE_IMAGE + +#ifdef USE_IMAGE +#ifdef USE_LVGL_IMAGE // Shortcut / overload, so that the source of an image widget can easily be updated from within a lambda. inline void lv_image_set_src(lv_obj_t *obj, image::Image *image) { ::lv_image_set_src(obj, image->get_lv_image_dsc()); } -#endif // LV_USE_IMAGE inline void lv_obj_set_style_bitmap_mask_src(lv_obj_t *obj, image::Image *image, lv_style_selector_t selector) { ::lv_obj_set_style_bitmap_mask_src(obj, image->get_lv_image_dsc(), selector); @@ -93,7 +93,8 @@ inline void lv_style_set_bg_image_src(lv_style_t *style, image::Image *image) { inline void lv_style_set_bitmap_mask_src(lv_style_t *style, image::Image *image) { ::lv_style_set_bitmap_mask_src(style, image->get_lv_image_dsc()); } -#endif // USE_LVGL_IMAGE +#endif + #ifdef USE_LVGL_ANIMIMG inline void lv_animimg_set_src(lv_obj_t *img, std::vector images) { auto *dsc = static_cast *>(lv_obj_get_user_data(img)); @@ -109,6 +110,7 @@ inline void lv_animimg_set_src(lv_obj_t *img, std::vector images lv_animimg_set_src(img, (const void **) dsc->data(), dsc->size()); } #endif // USE_LVGL_ANIMIMG +#endif // USE_IMAGE #ifdef USE_LVGL_METER int16_t lv_get_needle_angle_for_value(lv_obj_t *obj, int32_t value); diff --git a/esphome/components/lvgl/styles.py b/esphome/components/lvgl/styles.py index c1441526f9..5911505555 100644 --- a/esphome/components/lvgl/styles.py +++ b/esphome/components/lvgl/styles.py @@ -9,6 +9,7 @@ from .defines import ( CONF_THEME, LValidator, add_lv_use, + get_styles_used, get_theme_widget_map, literal, ) @@ -25,6 +26,7 @@ def has_style_props(config) -> bool: async def style_set(svar, style): for prop, validator in ALL_STYLES.items(): if (value := style.get(prop)) is not None: + get_styles_used().add(prop) if isinstance(validator, LValidator): value = await validator.process(value) if isinstance(value, list): From 2dbaaf1efda5625b4552dbd71036bd7f2584764c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 15 May 2026 22:06:33 -0700 Subject: [PATCH 19/24] Bump aioesphomeapi from 45.0.1 to 45.0.2 (#16469) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index ae50c4046b..7d497e2834 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.19 esptool==5.2.0 click==8.3.3 esphome-dashboard==20260425.0 -aioesphomeapi==45.0.1 +aioesphomeapi==45.0.2 zeroconf==0.148.0 puremagic==1.30 ruamel.yaml==0.19.1 # dashboard_import From f301e90fd9eea419dd82166f17f4ad3cd6477eb6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 15 May 2026 22:16:52 -0700 Subject: [PATCH 20/24] [ci] Use larger app partition for esp32-s3-idf component test grouping (#16430) --- .../build_components_base.esp32-s3-idf.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_build_components/build_components_base.esp32-s3-idf.yaml b/tests/test_build_components/build_components_base.esp32-s3-idf.yaml index ee209000e9..f3122f977e 100644 --- a/tests/test_build_components/build_components_base.esp32-s3-idf.yaml +++ b/tests/test_build_components/build_components_base.esp32-s3-idf.yaml @@ -7,6 +7,9 @@ esp32: variant: ESP32S3 framework: type: esp-idf + # Use custom partition table with larger app partition (3MB) + # Default IDF partitions only allow 1.75MB which is too small for grouped tests + partitions: ../partitions_testing.csv logger: level: VERY_VERBOSE From 20f92ad5e96cb565f898b94c79b06f2b9a699536 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 16 May 2026 18:41:09 -0700 Subject: [PATCH 21/24] Bump aioesphomeapi from 45.0.2 to 45.0.3 (#16479) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 7d497e2834..92e36297a6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.19 esptool==5.2.0 click==8.3.3 esphome-dashboard==20260425.0 -aioesphomeapi==45.0.2 +aioesphomeapi==45.0.3 zeroconf==0.148.0 puremagic==1.30 ruamel.yaml==0.19.1 # dashboard_import From 4f188bf9bb07a98ea06cdd915e962a290362a957 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 16 May 2026 19:08:19 -0700 Subject: [PATCH 22/24] Bump zeroconf from 0.148.0 to 0.149.3 (#16480) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 92e36297a6..63a25c8e36 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,7 +13,7 @@ esptool==5.2.0 click==8.3.3 esphome-dashboard==20260425.0 aioesphomeapi==45.0.3 -zeroconf==0.148.0 +zeroconf==0.149.3 puremagic==1.30 ruamel.yaml==0.19.1 # dashboard_import ruamel.yaml.clib==0.2.15 # dashboard_import From df31c72e4e4908a040a4e87fc5e6ca315bd7c3ef Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Sun, 17 May 2026 15:29:38 -0400 Subject: [PATCH 23/24] [espidf] Switch direct framework downloader to esphome-libs/esp-idf tarballs (#16484) --- esphome/espidf/framework.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/espidf/framework.py b/esphome/espidf/framework.py index 7ff373aba8..32bcf4fb3b 100644 --- a/esphome/espidf/framework.py +++ b/esphome/espidf/framework.py @@ -69,7 +69,7 @@ ESPHOME_IDF_DEFAULT_FEATURES = _str_to_lst_of_str( ESPHOME_IDF_FRAMEWORK_MIRRORS = _str_to_lst_of_str( os.environ.get( "ESPHOME_IDF_FRAMEWORK_MIRRORS", - "https://github.com/espressif/esp-idf/releases/download/v{VERSION}/esp-idf-v{VERSION}.zip;https://github.com/espressif/esp-idf/releases/download/v{MAJOR}.{MINOR}/esp-idf-v{MAJOR}.{MINOR}.zip", + "https://github.com/esphome-libs/esp-idf/releases/download/v{VERSION}/esp-idf-v{VERSION}.tar.xz;https://github.com/esphome-libs/esp-idf/releases/download/v{MAJOR}.{MINOR}/esp-idf-v{MAJOR}.{MINOR}.tar.xz", ) ) From cdf74c180e16cc9297c476953d477e2605516921 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 18 May 2026 11:11:54 +1200 Subject: [PATCH 24/24] Bump version to 2026.5.0b2 --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index a29a78ea9c..641a491828 100644 --- a/Doxyfile +++ b/Doxyfile @@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = 2026.5.0b1 +PROJECT_NUMBER = 2026.5.0b2 # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a diff --git a/esphome/const.py b/esphome/const.py index 1819502201..d6d533a702 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2026.5.0b1" +__version__ = "2026.5.0b2" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = (