From 7ac60c15dc76a3dcd3995620b56423886d4adaa0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 18 Jul 2025 19:31:53 -1000 Subject: [PATCH 01/17] [gpio] Auto-disable interrupts for shared GPIO pins in binary sensors (#9701) --- .../components/gpio/binary_sensor/__init__.py | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/esphome/components/gpio/binary_sensor/__init__.py b/esphome/components/gpio/binary_sensor/__init__.py index 59f54520fa..8372bc7e08 100644 --- a/esphome/components/gpio/binary_sensor/__init__.py +++ b/esphome/components/gpio/binary_sensor/__init__.py @@ -4,7 +4,13 @@ from esphome import pins import esphome.codegen as cg from esphome.components import binary_sensor import esphome.config_validation as cv -from esphome.const import CONF_ID, CONF_NAME, CONF_NUMBER, CONF_PIN +from esphome.const import ( + CONF_ALLOW_OTHER_USES, + CONF_ID, + CONF_NAME, + CONF_NUMBER, + CONF_PIN, +) from esphome.core import CORE from .. import gpio_ns @@ -76,6 +82,18 @@ async def to_code(config): ) use_interrupt = False + # Check if pin is shared with other components (allow_other_uses) + # When a pin is shared, interrupts can interfere with other components + # (e.g., duty_cycle sensor) that need to monitor the pin's state changes + if use_interrupt and config[CONF_PIN].get(CONF_ALLOW_OTHER_USES, False): + _LOGGER.info( + "GPIO binary_sensor '%s': Disabling interrupts because pin %s is shared with other components. " + "The sensor will use polling mode for compatibility with other pin uses.", + config.get(CONF_NAME, config[CONF_ID]), + config[CONF_PIN][CONF_NUMBER], + ) + use_interrupt = False + cg.add(var.set_use_interrupt(use_interrupt)) if use_interrupt: cg.add(var.set_interrupt_type(config[CONF_INTERRUPT_TYPE])) From d6ff79082392f5067c2416230eb925b7e76fe767 Mon Sep 17 00:00:00 2001 From: tmpeh <41875356+tmpeh@users.noreply.github.com> Date: Sat, 19 Jul 2025 23:24:26 +0200 Subject: [PATCH 02/17] Fix format string error in ota_web_server.cpp (#9711) --- esphome/components/web_server/ota/ota_web_server.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/web_server/ota/ota_web_server.cpp b/esphome/components/web_server/ota/ota_web_server.cpp index 966c1c1024..7211f707e9 100644 --- a/esphome/components/web_server/ota/ota_web_server.cpp +++ b/esphome/components/web_server/ota/ota_web_server.cpp @@ -76,7 +76,7 @@ void OTARequestHandler::report_ota_progress_(AsyncWebServerRequest *request) { percentage = (this->ota_read_length_ * 100.0f) / request->contentLength(); ESP_LOGD(TAG, "OTA in progress: %0.1f%%", percentage); } else { - ESP_LOGD(TAG, "OTA in progress: %u bytes read", this->ota_read_length_); + ESP_LOGD(TAG, "OTA in progress: %" PRIu32 " bytes read", this->ota_read_length_); } #ifdef USE_OTA_STATE_CALLBACK // Report progress - use call_deferred since we're in web server task @@ -171,7 +171,7 @@ void OTARequestHandler::handleUpload(AsyncWebServerRequest *request, const Strin // Finalize if (final) { - ESP_LOGD(TAG, "OTA final chunk: index=%zu, len=%zu, total_read=%u, contentLength=%zu", index, len, + ESP_LOGD(TAG, "OTA final chunk: index=%zu, len=%zu, total_read=%" PRIu32 ", contentLength=%zu", index, len, this->ota_read_length_, request->contentLength()); // For Arduino framework, the Update library tracks expected size from firmware header From d92ee563f27d9ad160c3dc7a3afb1523640c133d Mon Sep 17 00:00:00 2001 From: JonasB2497 <45214989+JonasB2497@users.noreply.github.com> Date: Sun, 20 Jul 2025 00:29:02 +0200 Subject: [PATCH 03/17] [sdl][mipi_spi] Respect clipping when drawing (#9722) Co-authored-by: clydebarrow <2366188+clydebarrow@users.noreply.github.com> --- esphome/components/sdl/sdl_esphome.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/esphome/components/sdl/sdl_esphome.cpp b/esphome/components/sdl/sdl_esphome.cpp index e55bff58fe..5ad18f6311 100644 --- a/esphome/components/sdl/sdl_esphome.cpp +++ b/esphome/components/sdl/sdl_esphome.cpp @@ -48,6 +48,9 @@ void Sdl::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t * } void Sdl::draw_pixel_at(int x, int y, Color color) { + if (!this->get_clipping().inside(x, y)) + return; + SDL_Rect rect{x, y, 1, 1}; auto data = (display::ColorUtil::color_to_565(color, display::COLOR_ORDER_RGB)); SDL_UpdateTexture(this->texture_, &rect, &data, 2); From 896d7f8f7616ff705d0bdbbcd080f6aba420be28 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 21 Jul 2025 15:25:08 -0400 Subject: [PATCH 04/17] [esp32_touch] Fix setup mode in v1 driver (#9725) --- .../components/esp32_touch/esp32_touch_v1.cpp | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/esphome/components/esp32_touch/esp32_touch_v1.cpp b/esphome/components/esp32_touch/esp32_touch_v1.cpp index c3d43c6bbf..629dc8e793 100644 --- a/esphome/components/esp32_touch/esp32_touch_v1.cpp +++ b/esphome/components/esp32_touch/esp32_touch_v1.cpp @@ -16,6 +16,8 @@ namespace esp32_touch { static const char *const TAG = "esp32_touch"; +static const uint32_t SETUP_MODE_THRESHOLD = 0xFFFF; + void ESP32TouchComponent::setup() { // Create queue for touch events // Queue size calculation: children * 4 allows for burst scenarios where ISR @@ -44,7 +46,11 @@ void ESP32TouchComponent::setup() { // Configure each touch pad for (auto *child : this->children_) { - touch_pad_config(child->get_touch_pad(), child->get_threshold()); + if (this->setup_mode_) { + touch_pad_config(child->get_touch_pad(), SETUP_MODE_THRESHOLD); + } else { + touch_pad_config(child->get_touch_pad(), child->get_threshold()); + } } // Register ISR handler @@ -114,8 +120,8 @@ void ESP32TouchComponent::loop() { child->publish_state(new_state); // Original ESP32: ISR only fires when touched, release is detected by timeout // Note: ESP32 v1 uses inverted logic - touched when value < threshold - ESP_LOGV(TAG, "Touch Pad '%s' state: ON (value: %" PRIu32 " < threshold: %" PRIu32 ")", - child->get_name().c_str(), event.value, child->get_threshold()); + ESP_LOGV(TAG, "Touch Pad '%s' state: %s (value: %" PRIu32 " < threshold: %" PRIu32 ")", + child->get_name().c_str(), ONOFF(new_state), event.value, child->get_threshold()); } break; // Exit inner loop after processing matching pad } @@ -188,11 +194,6 @@ void IRAM_ATTR ESP32TouchComponent::touch_isr_handler(void *arg) { // as any pad remains touched. This allows us to detect both new touches and // continued touches, but releases must be detected by timeout in the main loop. - // IMPORTANT: ESP32 v1 touch detection logic - INVERTED compared to v2! - // ESP32 v1: Touch is detected when capacitance INCREASES, causing the measured value to DECREASE - // Therefore: touched = (value < threshold) - // This is opposite to ESP32-S2/S3 v2 where touched = (value > threshold) - // Process all configured pads to check their current state // Note: ESP32 v1 doesn't tell us which specific pad triggered the interrupt, // so we must scan all configured pads to find which ones were touched @@ -211,11 +212,16 @@ void IRAM_ATTR ESP32TouchComponent::touch_isr_handler(void *arg) { } // Skip pads that aren’t in the trigger mask - bool is_touched = (mask >> pad) & 1; - if (!is_touched) { + if (((mask >> pad) & 1) == 0) { continue; } + // IMPORTANT: ESP32 v1 touch detection logic - INVERTED compared to v2! + // ESP32 v1: Touch is detected when capacitance INCREASES, causing the measured value to DECREASE + // Therefore: touched = (value < threshold) + // This is opposite to ESP32-S2/S3 v2 where touched = (value > threshold) + bool is_touched = value < child->get_threshold(); + // Always send the current state - the main loop will filter for changes // We send both touched and untouched states because the ISR doesn't // track previous state (to keep ISR fast and simple) From 76e75f4cdc2c2bcfca6cf38d43a97c3c80e55c19 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 22 Jul 2025 08:20:25 +1200 Subject: [PATCH 05/17] [tuya] Update use of fan_schema (#9762) --- esphome/components/tuya/fan/__init__.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/esphome/components/tuya/fan/__init__.py b/esphome/components/tuya/fan/__init__.py index c732bdaf31..de95888b6b 100644 --- a/esphome/components/tuya/fan/__init__.py +++ b/esphome/components/tuya/fan/__init__.py @@ -1,7 +1,7 @@ import esphome.codegen as cg from esphome.components import fan import esphome.config_validation as cv -from esphome.const import CONF_OUTPUT_ID, CONF_SPEED_COUNT, CONF_SWITCH_DATAPOINT +from esphome.const import CONF_ID, CONF_SPEED_COUNT, CONF_SWITCH_DATAPOINT from .. import CONF_TUYA_ID, Tuya, tuya_ns @@ -14,9 +14,9 @@ CONF_DIRECTION_DATAPOINT = "direction_datapoint" TuyaFan = tuya_ns.class_("TuyaFan", cg.Component, fan.Fan) CONFIG_SCHEMA = cv.All( - fan.FAN_SCHEMA.extend( + fan.fan_schema(TuyaFan) + .extend( { - cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(TuyaFan), cv.GenerateID(CONF_TUYA_ID): cv.use_id(Tuya), cv.Optional(CONF_OSCILLATION_DATAPOINT): cv.uint8_t, cv.Optional(CONF_SPEED_DATAPOINT): cv.uint8_t, @@ -24,7 +24,8 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_DIRECTION_DATAPOINT): cv.uint8_t, cv.Optional(CONF_SPEED_COUNT, default=3): cv.int_range(min=1, max=256), } - ).extend(cv.COMPONENT_SCHEMA), + ) + .extend(cv.COMPONENT_SCHEMA), cv.has_at_least_one_key(CONF_SPEED_DATAPOINT, CONF_SWITCH_DATAPOINT), ) @@ -32,7 +33,7 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): parent = await cg.get_variable(config[CONF_TUYA_ID]) - var = cg.new_Pvariable(config[CONF_OUTPUT_ID], parent, config[CONF_SPEED_COUNT]) + var = cg.new_Pvariable(config[CONF_ID], parent, config[CONF_SPEED_COUNT]) await cg.register_component(var, config) await fan.register_fan(var, config) From f8777d3b6604d2ff8c0451c3e1925bb17a983a30 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Mon, 21 Jul 2025 18:29:05 -0500 Subject: [PATCH 06/17] [config_validation] Add support for suggesting alternate component/platform (#9757) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/config_validation.py | 84 +++++++++++++++++++++++++----------- 1 file changed, 58 insertions(+), 26 deletions(-) diff --git a/esphome/config_validation.py b/esphome/config_validation.py index 09b132a458..1c524cab6b 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -73,6 +73,7 @@ from esphome.const import ( TYPE_GIT, TYPE_LOCAL, VALID_SUBSTITUTIONS_CHARACTERS, + Framework, __version__ as ESPHOME_VERSION, ) from esphome.core import ( @@ -282,6 +283,38 @@ class FinalExternalInvalid(Invalid): """Represents an invalid value in the final validation phase where the path should not be prepended.""" +@dataclass(frozen=True, order=True) +class Version: + major: int + minor: int + patch: int + extra: str = "" + + def __str__(self): + return f"{self.major}.{self.minor}.{self.patch}" + + @classmethod + def parse(cls, value: str) -> Version: + match = re.match(r"^(\d+).(\d+).(\d+)-?(\w*)$", value) + if match is None: + raise ValueError(f"Not a valid version number {value}") + major = int(match[1]) + minor = int(match[2]) + patch = int(match[3]) + extra = match[4] or "" + return Version(major=major, minor=minor, patch=patch, extra=extra) + + @property + def is_beta(self) -> bool: + """Check if this version is a beta version.""" + return self.extra.startswith("b") + + @property + def is_dev(self) -> bool: + """Check if this version is a development version.""" + return self.extra.startswith("dev") + + def check_not_templatable(value): if isinstance(value, Lambda): raise Invalid("This option is not templatable!") @@ -619,16 +652,35 @@ def only_on(platforms): return validator_ -def only_with_framework(frameworks): +def only_with_framework( + frameworks: Framework | str | list[Framework | str], suggestions=None +): """Validate that this option can only be specified on the given frameworks.""" if not isinstance(frameworks, list): frameworks = [frameworks] + frameworks = [Framework(framework) for framework in frameworks] + + if suggestions is None: + suggestions = {} + + version = Version.parse(ESPHOME_VERSION) + if version.is_beta: + docs_format = "https://beta.esphome.io/components/{path}" + elif version.is_dev: + docs_format = "https://next.esphome.io/components/{path}" + else: + docs_format = "https://esphome.io/components/{path}" + def validator_(obj): if CORE.target_framework not in frameworks: - raise Invalid( - f"This feature is only available with frameworks {frameworks}" - ) + err_str = f"This feature is only available with framework(s) {', '.join([framework.value for framework in frameworks])}" + if suggestion := suggestions.get(CORE.target_framework, None): + (component, docs_path) = suggestion + err_str += f"\nPlease use '{component}'" + if docs_path: + err_str += f": {docs_format.format(path=docs_path)}" + raise Invalid(err_str) return obj return validator_ @@ -637,8 +689,8 @@ def only_with_framework(frameworks): only_on_esp32 = only_on(PLATFORM_ESP32) only_on_esp8266 = only_on(PLATFORM_ESP8266) only_on_rp2040 = only_on(PLATFORM_RP2040) -only_with_arduino = only_with_framework("arduino") -only_with_esp_idf = only_with_framework("esp-idf") +only_with_arduino = only_with_framework(Framework.ARDUINO) +only_with_esp_idf = only_with_framework(Framework.ESP_IDF) # Adapted from: @@ -1965,26 +2017,6 @@ def source_refresh(value: str): return positive_time_period_seconds(value) -@dataclass(frozen=True, order=True) -class Version: - major: int - minor: int - patch: int - - def __str__(self): - return f"{self.major}.{self.minor}.{self.patch}" - - @classmethod - def parse(cls, value: str) -> Version: - match = re.match(r"^(\d+).(\d+).(\d+)-?\w*$", value) - if match is None: - raise ValueError(f"Not a valid version number {value}") - major = int(match[1]) - minor = int(match[2]) - patch = int(match[3]) - return Version(major=major, minor=minor, patch=patch) - - def version_number(value): value = string_strict(value) try: From fc8c5a7438d4260772014f4c19d9925f1fa10216 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 21 Jul 2025 17:47:43 -1000 Subject: [PATCH 07/17] [core] Process pending loop enables during setup blocking phase (#9787) --- esphome/core/application.cpp | 63 ++++++++++++++++++++++-------------- esphome/core/application.h | 2 ++ 2 files changed, 41 insertions(+), 24 deletions(-) diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index e19acd3ba6..68bcc99d31 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -68,8 +68,11 @@ void Application::setup() { do { uint8_t new_app_state = STATUS_LED_WARNING; - this->scheduler.call(); - this->feed_wdt(); + uint32_t now = millis(); + + // Process pending loop enables to handle GPIO interrupts during setup + this->before_loop_tasks_(now); + for (uint32_t j = 0; j <= i; j++) { // Update loop_component_start_time_ right before calling each component this->loop_component_start_time_ = millis(); @@ -78,6 +81,8 @@ void Application::setup() { this->app_state_ |= new_app_state; this->feed_wdt(); } + + this->after_loop_tasks_(); this->app_state_ = new_app_state; yield(); } while (!component->can_proceed()); @@ -94,30 +99,10 @@ void Application::setup() { void Application::loop() { uint8_t new_app_state = 0; - this->scheduler.call(); - // Get the initial loop time at the start uint32_t last_op_end_time = millis(); - // Feed WDT with time - this->feed_wdt(last_op_end_time); - - // Process any pending enable_loop requests from ISRs - // This must be done before marking in_loop_ = true to avoid race conditions - if (this->has_pending_enable_loop_requests_) { - // Clear flag BEFORE processing to avoid race condition - // If ISR sets it during processing, we'll catch it next loop iteration - // This is safe because: - // 1. Each component has its own pending_enable_loop_ flag that we check - // 2. If we can't process a component (wrong state), enable_pending_loops_() - // will set this flag back to true - // 3. Any new ISR requests during processing will set the flag again - this->has_pending_enable_loop_requests_ = false; - this->enable_pending_loops_(); - } - - // Mark that we're in the loop for safe reentrant modifications - this->in_loop_ = true; + this->before_loop_tasks_(last_op_end_time); for (this->current_loop_index_ = 0; this->current_loop_index_ < this->looping_components_active_end_; this->current_loop_index_++) { @@ -138,7 +123,7 @@ void Application::loop() { this->feed_wdt(last_op_end_time); } - this->in_loop_ = false; + this->after_loop_tasks_(); this->app_state_ = new_app_state; // Use the last component's end time instead of calling millis() again @@ -400,6 +385,36 @@ void Application::enable_pending_loops_() { } } +void Application::before_loop_tasks_(uint32_t loop_start_time) { + // Process scheduled tasks + this->scheduler.call(); + + // Feed the watchdog timer + this->feed_wdt(loop_start_time); + + // Process any pending enable_loop requests from ISRs + // This must be done before marking in_loop_ = true to avoid race conditions + if (this->has_pending_enable_loop_requests_) { + // Clear flag BEFORE processing to avoid race condition + // If ISR sets it during processing, we'll catch it next loop iteration + // This is safe because: + // 1. Each component has its own pending_enable_loop_ flag that we check + // 2. If we can't process a component (wrong state), enable_pending_loops_() + // will set this flag back to true + // 3. Any new ISR requests during processing will set the flag again + this->has_pending_enable_loop_requests_ = false; + this->enable_pending_loops_(); + } + + // Mark that we're in the loop for safe reentrant modifications + this->in_loop_ = true; +} + +void Application::after_loop_tasks_() { + // Clear the in_loop_ flag to indicate we're done processing components + this->in_loop_ = false; +} + #ifdef USE_SOCKET_SELECT_SUPPORT bool Application::register_socket_fd(int fd) { // WARNING: This function is NOT thread-safe and must only be called from the main loop diff --git a/esphome/core/application.h b/esphome/core/application.h index f2b5cb5c89..f675881a27 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -504,6 +504,8 @@ class Application { void enable_component_loop_(Component *component); void enable_pending_loops_(); void activate_looping_component_(uint16_t index); + void before_loop_tasks_(uint32_t loop_start_time); + void after_loop_tasks_(); void feed_wdt_arch_(); From cb6acfe24bf2e73fd615ea39f70d543620519bd7 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Tue, 22 Jul 2025 06:53:33 -0500 Subject: [PATCH 08/17] [fastled_clockless, fastled_spi] Add suggested alternate when using IDF (#9784) --- esphome/components/fastled_clockless/light.py | 19 +++++++++++++++++-- esphome/components/fastled_spi/light.py | 10 ++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/esphome/components/fastled_clockless/light.py b/esphome/components/fastled_clockless/light.py index 49a6d390be..aa2172bf88 100644 --- a/esphome/components/fastled_clockless/light.py +++ b/esphome/components/fastled_clockless/light.py @@ -2,7 +2,13 @@ from esphome import pins import esphome.codegen as cg from esphome.components import fastled_base import esphome.config_validation as cv -from esphome.const import CONF_CHIPSET, CONF_NUM_LEDS, CONF_PIN, CONF_RGB_ORDER +from esphome.const import ( + CONF_CHIPSET, + CONF_NUM_LEDS, + CONF_PIN, + CONF_RGB_ORDER, + Framework, +) AUTO_LOAD = ["fastled_base"] @@ -48,13 +54,22 @@ CONFIG_SCHEMA = cv.All( cv.Required(CONF_PIN): pins.internal_gpio_output_pin_number, } ), - _validate, + cv.only_with_framework( + frameworks=Framework.ARDUINO, + suggestions={ + Framework.ESP_IDF: ( + "esp32_rmt_led_strip", + "light/esp32_rmt_led_strip", + ) + }, + ), cv.require_framework_version( esp8266_arduino=cv.Version(2, 7, 4), esp32_arduino=cv.Version(99, 0, 0), max_version=True, extra_message="Please see note on documentation for FastLED", ), + _validate, ) diff --git a/esphome/components/fastled_spi/light.py b/esphome/components/fastled_spi/light.py index ac30721eb4..81d8c3b32a 100644 --- a/esphome/components/fastled_spi/light.py +++ b/esphome/components/fastled_spi/light.py @@ -9,6 +9,7 @@ from esphome.const import ( CONF_DATA_RATE, CONF_NUM_LEDS, CONF_RGB_ORDER, + Framework, ) AUTO_LOAD = ["fastled_base"] @@ -33,6 +34,15 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_DATA_RATE): cv.frequency, } ), + cv.only_with_framework( + frameworks=Framework.ARDUINO, + suggestions={ + Framework.ESP_IDF: ( + "spi_led_strip", + "light/spi_led_strip", + ) + }, + ), cv.require_framework_version( esp8266_arduino=cv.Version(2, 7, 4), esp32_arduino=cv.Version(99, 0, 0), From ae12deff877754b3defe2fbf432646c3799938ad Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Tue, 22 Jul 2025 06:53:45 -0500 Subject: [PATCH 09/17] [neopixelbus] Add suggested alternate when using IDF (#9783) --- esphome/components/neopixelbus/light.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/esphome/components/neopixelbus/light.py b/esphome/components/neopixelbus/light.py index 3cd1bfd357..c63790e60b 100644 --- a/esphome/components/neopixelbus/light.py +++ b/esphome/components/neopixelbus/light.py @@ -15,6 +15,7 @@ from esphome.const import ( CONF_PIN, CONF_TYPE, CONF_VARIANT, + Framework, ) from esphome.core import CORE @@ -162,7 +163,15 @@ def _validate_method(value): CONFIG_SCHEMA = cv.All( - cv.only_with_arduino, + cv.only_with_framework( + frameworks=Framework.ARDUINO, + suggestions={ + Framework.ESP_IDF: ( + "esp32_rmt_led_strip", + "light/esp32_rmt_led_strip", + ) + }, + ), cv.require_framework_version( esp8266_arduino=cv.Version(2, 4, 0), esp32_arduino=cv.Version(0, 0, 0), From 86740124064ad809f2609b2e85e113a1367f911d Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Tue, 22 Jul 2025 06:54:09 -0500 Subject: [PATCH 10/17] [bme680_bsec] Add suggested alternate when using IDF (#9785) --- esphome/components/bme680_bsec/__init__.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/esphome/components/bme680_bsec/__init__.py b/esphome/components/bme680_bsec/__init__.py index 8ee463a59a..330dc4dd9c 100644 --- a/esphome/components/bme680_bsec/__init__.py +++ b/esphome/components/bme680_bsec/__init__.py @@ -1,7 +1,7 @@ import esphome.codegen as cg from esphome.components import esp32, i2c import esphome.config_validation as cv -from esphome.const import CONF_ID, CONF_SAMPLE_RATE, CONF_TEMPERATURE_OFFSET +from esphome.const import CONF_ID, CONF_SAMPLE_RATE, CONF_TEMPERATURE_OFFSET, Framework CODEOWNERS = ["@trvrnrth"] DEPENDENCIES = ["i2c"] @@ -56,7 +56,15 @@ CONFIG_SCHEMA = cv.All( ): cv.positive_time_period_minutes, } ).extend(i2c.i2c_device_schema(0x76)), - cv.only_with_arduino, + cv.only_with_framework( + frameworks=Framework.ARDUINO, + suggestions={ + Framework.ESP_IDF: ( + "bme68x_bsec2_i2c", + "sensor/bme68x_bsec2", + ) + }, + ), cv.Any( cv.only_on_esp8266, cv.All( From dc26ed9c46f8c8b09bca39df3a97caac6af802d1 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 23 Jul 2025 00:34:13 +1200 Subject: [PATCH 11/17] Bump version to 2025.7.3 --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index a601fcc8eb..ae1e519030 100644 --- a/Doxyfile +++ b/Doxyfile @@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = 2025.7.2 +PROJECT_NUMBER = 2025.7.3 # 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 93b509f72a..476059af62 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2025.7.2" +__version__ = "2025.7.3" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 3bb5a9e2f740c1067e7514bb55cdabece757d735 Mon Sep 17 00:00:00 2001 From: Guillermo Ruffino Date: Tue, 22 Jul 2025 15:52:56 -0300 Subject: [PATCH 12/17] [schema-gen] fix referenced schemas when schema in component platform (#9755) --- script/build_language_schema.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/script/build_language_schema.py b/script/build_language_schema.py index 4473ec1b5a..960c35ca0f 100755 --- a/script/build_language_schema.py +++ b/script/build_language_schema.py @@ -411,11 +411,16 @@ def add_referenced_recursive(referenced_schemas, config_var, path, eat_schema=Fa s1 = get_str_path_schema(k) p = k.split(".") - if len(p) == 3 and path[0] == f"{p[0]}.{p[1]}": - # special case for schema inside platforms - add_referenced_recursive( - referenced_schemas, s1, [path[0], "schemas", p[2]] - ) + if len(p) == 3: + if path[0] == f"{p[0]}.{p[1]}": + # special case for schema inside platforms + add_referenced_recursive( + referenced_schemas, s1, [path[0], "schemas", p[2]] + ) + else: + add_referenced_recursive( + referenced_schemas, s1, [f"{p[0]}.{p[1]}", "schemas", p[2]] + ) else: add_referenced_recursive( referenced_schemas, s1, [p[0], "schemas", p[1]] From cf403062979626731fc34e426618c77680655c2b Mon Sep 17 00:00:00 2001 From: Thomas Rupprecht Date: Tue, 22 Jul 2025 22:24:40 +0200 Subject: [PATCH 13/17] [audio] fix typo `gneneral` and `divison` (#9808) --- esphome/components/audio/audio.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/audio/audio.h b/esphome/components/audio/audio.h index 95c31872e3..e01d7eb101 100644 --- a/esphome/components/audio/audio.h +++ b/esphome/components/audio/audio.h @@ -15,7 +15,7 @@ class AudioStreamInfo { * - An audio sample represents a unit of audio for one channel. * - A frame represents a unit of audio with a sample for every channel. * - * In gneneral, converting between bytes, samples, and frames shouldn't result in rounding errors so long as frames + * In general, converting between bytes, samples, and frames shouldn't result in rounding errors so long as frames * are used as the main unit when transferring audio data. Durations may result in rounding for certain sample rates; * e.g., 44.1 KHz. The ``frames_to_milliseconds_with_remainder`` function should be used for accuracy, as it takes * into account the remainder rather than just ignoring any rounding. @@ -76,7 +76,7 @@ class AudioStreamInfo { /// @brief Computes the duration, in microseconds, the given amount of frames represents. /// @param frames Number of audio frames - /// @return Duration in microseconds `frames` respresents. May be slightly inaccurate due to integer divison rounding + /// @return Duration in microseconds `frames` represents. May be slightly inaccurate due to integer division rounding /// for certain sample rates. uint32_t frames_to_microseconds(uint32_t frames) const; From e2976162b5f9f76cf2f305f156d4d7a46b4a17c9 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 22 Jul 2025 16:54:03 -0400 Subject: [PATCH 14/17] [sgp4x] Fix build (#9794) --- esphome/components/sgp4x/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/sgp4x/sensor.py b/esphome/components/sgp4x/sensor.py index 4f29248881..7c6fe580b2 100644 --- a/esphome/components/sgp4x/sensor.py +++ b/esphome/components/sgp4x/sensor.py @@ -139,7 +139,7 @@ async def to_code(config): ) ) cg.add_library( - None, + "Sensirion Gas Index Algorithm", None, "https://github.com/Sensirion/arduino-gas-index-algorithm.git#3.2.1", ) From 1a7757e7ca5711c8c97aa0e9d225567f8ec9a57d Mon Sep 17 00:00:00 2001 From: Stas Date: Wed, 23 Jul 2025 00:39:03 +0300 Subject: [PATCH 15/17] [http_request] set correct duration_ms for failed requests (#9789) --- esphome/components/http_request/http_request_idf.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/http_request/http_request_idf.cpp b/esphome/components/http_request/http_request_idf.cpp index 68c06d28f2..89a0891b03 100644 --- a/esphome/components/http_request/http_request_idf.cpp +++ b/esphome/components/http_request/http_request_idf.cpp @@ -157,8 +157,8 @@ std::shared_ptr HttpRequestIDF::perform(std::string url, std::str container->status_code = esp_http_client_get_status_code(client); container->feed_wdt(); container->set_response_headers(user_data.response_headers); + container->duration_ms = millis() - start; if (is_success(container->status_code)) { - container->duration_ms = millis() - start; return container; } @@ -191,8 +191,8 @@ std::shared_ptr HttpRequestIDF::perform(std::string url, std::str container->feed_wdt(); container->status_code = esp_http_client_get_status_code(client); container->feed_wdt(); + container->duration_ms = millis() - start; if (is_success(container->status_code)) { - container->duration_ms = millis() - start; return container; } From 5a4e2a3eaf7c1b951e86a9ed62c3e8955682d8f9 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 23 Jul 2025 10:56:00 +1200 Subject: [PATCH 16/17] [udp] Move ``on_receive`` to const (#9811) --- esphome/components/const/__init__.py | 1 + esphome/components/udp/__init__.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/components/const/__init__.py b/esphome/components/const/__init__.py index 5b40545d89..18ef4d48b6 100644 --- a/esphome/components/const/__init__.py +++ b/esphome/components/const/__init__.py @@ -5,5 +5,6 @@ CODEOWNERS = ["@esphome/core"] CONF_BYTE_ORDER = "byte_order" CONF_COLOR_DEPTH = "color_depth" CONF_DRAW_ROUNDING = "draw_rounding" +CONF_ON_RECEIVE = "on_receive" CONF_ON_STATE_CHANGE = "on_state_change" CONF_REQUEST_HEADERS = "request_headers" diff --git a/esphome/components/udp/__init__.py b/esphome/components/udp/__init__.py index ed405d7c22..6b1e4f8ed8 100644 --- a/esphome/components/udp/__init__.py +++ b/esphome/components/udp/__init__.py @@ -1,6 +1,7 @@ from esphome import automation from esphome.automation import Trigger import esphome.codegen as cg +from esphome.components.const import CONF_ON_RECEIVE from esphome.components.packet_transport import ( CONF_BINARY_SENSORS, CONF_ENCRYPTION, @@ -27,7 +28,6 @@ trigger_args = cg.std_vector.template(cg.uint8) CONF_ADDRESSES = "addresses" CONF_LISTEN_ADDRESS = "listen_address" CONF_UDP_ID = "udp_id" -CONF_ON_RECEIVE = "on_receive" CONF_LISTEN_PORT = "listen_port" CONF_BROADCAST_PORT = "broadcast_port" From 116c91e9c5fc6d0d32191bd4e6d6e406e2bff6bf Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 22 Jul 2025 19:15:31 -0400 Subject: [PATCH 17/17] Bump ESP32 IDF version to 5.4.2 and Arduino version to 3.2.1 (#9770) --- .clang-tidy.hash | 2 +- esphome/components/esp32/__init__.py | 16 ++++++++-------- esphome/components/ethernet/esp_eth_phy_jl1101.c | 5 +++++ esphome/components/ethernet/ethernet_component.h | 4 ++++ esphome/core/defines.h | 3 +-- platformio.ini | 8 ++++---- script/clang-tidy | 2 +- 7 files changed, 24 insertions(+), 16 deletions(-) diff --git a/.clang-tidy.hash b/.clang-tidy.hash index 50a7fa9709..9efe5b2270 100644 --- a/.clang-tidy.hash +++ b/.clang-tidy.hash @@ -1 +1 @@ -0c2acbc16bfb7d63571dbe7042f94f683be25e4ca8a0f158a960a94adac4b931 +7920671c938a5ea6a11ac4594204b5ec8f38d579c962bf1f185e8d5e3ad879be diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 6ddb579733..587d75b64b 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -310,19 +310,19 @@ def _format_framework_espidf_version( # The default/recommended arduino framework version # - https://github.com/espressif/arduino-esp32/releases -RECOMMENDED_ARDUINO_FRAMEWORK_VERSION = cv.Version(3, 1, 3) +RECOMMENDED_ARDUINO_FRAMEWORK_VERSION = cv.Version(3, 2, 1) # The platform-espressif32 version to use for arduino frameworks # - https://github.com/pioarduino/platform-espressif32/releases -ARDUINO_PLATFORM_VERSION = cv.Version(53, 3, 13) +ARDUINO_PLATFORM_VERSION = cv.Version(54, 3, 21) # The default/recommended esp-idf framework version # - https://github.com/espressif/esp-idf/releases # - https://api.registry.platformio.org/v3/packages/platformio/tool/framework-espidf -RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION = cv.Version(5, 3, 2) +RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION = cv.Version(5, 4, 2) # The platformio/espressif32 version to use for esp-idf frameworks # - https://github.com/platformio/platform-espressif32/releases # - https://api.registry.platformio.org/v3/packages/platformio/platform/espressif32 -ESP_IDF_PLATFORM_VERSION = cv.Version(53, 3, 13) +ESP_IDF_PLATFORM_VERSION = cv.Version(54, 3, 21) # List based on https://registry.platformio.org/tools/platformio/framework-espidf/versions SUPPORTED_PLATFORMIO_ESP_IDF_5X = [ @@ -357,8 +357,8 @@ SUPPORTED_PIOARDUINO_ESP_IDF_5X = [ def _arduino_check_versions(value): value = value.copy() lookups = { - "dev": (cv.Version(3, 1, 3), "https://github.com/espressif/arduino-esp32.git"), - "latest": (cv.Version(3, 1, 3), None), + "dev": (cv.Version(3, 2, 1), "https://github.com/espressif/arduino-esp32.git"), + "latest": (cv.Version(3, 2, 1), None), "recommended": (RECOMMENDED_ARDUINO_FRAMEWORK_VERSION, None), } @@ -396,8 +396,8 @@ def _arduino_check_versions(value): def _esp_idf_check_versions(value): value = value.copy() lookups = { - "dev": (cv.Version(5, 3, 2), "https://github.com/espressif/esp-idf.git"), - "latest": (cv.Version(5, 3, 2), None), + "dev": (cv.Version(5, 4, 2), "https://github.com/espressif/esp-idf.git"), + "latest": (cv.Version(5, 2, 2), None), "recommended": (RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION, None), } diff --git a/esphome/components/ethernet/esp_eth_phy_jl1101.c b/esphome/components/ethernet/esp_eth_phy_jl1101.c index 4f31e0a9fd..5e73e99101 100644 --- a/esphome/components/ethernet/esp_eth_phy_jl1101.c +++ b/esphome/components/ethernet/esp_eth_phy_jl1101.c @@ -25,6 +25,9 @@ #include "driver/gpio.h" #include "esp_rom_gpio.h" #include "esp_rom_sys.h" +#include "esp_idf_version.h" + +#if defined(USE_ARDUINO) || ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 4, 2) static const char *TAG = "jl1101"; #define PHY_CHECK(a, str, goto_tag, ...) \ @@ -336,4 +339,6 @@ esp_eth_phy_t *esp_eth_phy_new_jl1101(const eth_phy_config_t *config) { err: return NULL; } + +#endif /* USE_ARDUINO */ #endif /* USE_ESP32 */ diff --git a/esphome/components/ethernet/ethernet_component.h b/esphome/components/ethernet/ethernet_component.h index 1b347946f5..bdcda6afb4 100644 --- a/esphome/components/ethernet/ethernet_component.h +++ b/esphome/components/ethernet/ethernet_component.h @@ -11,6 +11,7 @@ #include "esp_eth_mac.h" #include "esp_netif.h" #include "esp_mac.h" +#include "esp_idf_version.h" namespace esphome { namespace ethernet { @@ -153,7 +154,10 @@ class EthernetComponent : public Component { // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) extern EthernetComponent *global_eth_component; + +#if defined(USE_ARDUINO) || ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 4, 2) extern "C" esp_eth_phy_t *esp_eth_phy_new_jl1101(const eth_phy_config_t *config); +#endif } // namespace ethernet } // namespace esphome diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 19d380bd29..9355d56084 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -166,12 +166,11 @@ #define USE_WIFI_11KV_SUPPORT #ifdef USE_ARDUINO -#define USE_ARDUINO_VERSION_CODE VERSION_CODE(3, 1, 3) +#define USE_ARDUINO_VERSION_CODE VERSION_CODE(3, 2, 1) #define USE_ETHERNET #endif #ifdef USE_ESP_IDF -#define USE_ESP_IDF_VERSION_CODE VERSION_CODE(5, 3, 2) #define USE_MICRO_WAKE_WORD #define USE_MICRO_WAKE_WORD_VAD #if defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32H2) diff --git a/platformio.ini b/platformio.ini index 7fb301c08b..350f220853 100644 --- a/platformio.ini +++ b/platformio.ini @@ -125,9 +125,9 @@ extra_scripts = post:esphome/components/esp8266/post_build.py.script ; This are common settings for the ESP32 (all variants) using Arduino. [common:esp32-arduino] extends = common:arduino -platform = https://github.com/pioarduino/platform-espressif32/releases/download/53.03.13/platform-espressif32.zip +platform = https://github.com/pioarduino/platform-espressif32/releases/download/54.03.21/platform-espressif32.zip platform_packages = - pioarduino/framework-arduinoespressif32@https://github.com/espressif/arduino-esp32/releases/download/3.1.3/esp32-3.1.3.zip + pioarduino/framework-arduinoespressif32@https://github.com/espressif/arduino-esp32/releases/download/3.2.1/esp32-3.2.1.zip framework = arduino lib_deps = @@ -161,9 +161,9 @@ extra_scripts = post:esphome/components/esp32/post_build.py.script ; This are common settings for the ESP32 (all variants) using IDF. [common:esp32-idf] extends = common:idf -platform = https://github.com/pioarduino/platform-espressif32/releases/download/53.03.13/platform-espressif32.zip +platform = https://github.com/pioarduino/platform-espressif32/releases/download/54.03.21/platform-espressif32.zip platform_packages = - pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.3.2/esp-idf-v5.3.2.zip + pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.4.2/esp-idf-v5.4.2.zip framework = espidf lib_deps = diff --git a/script/clang-tidy b/script/clang-tidy index b5905e0e4e..187acd02ad 100755 --- a/script/clang-tidy +++ b/script/clang-tidy @@ -105,7 +105,7 @@ def clang_options(idedata): # idedata contains include directories for all toolchains of this platform, only use those from the one in use toolchain_dir = os.path.normpath(f"{idedata['cxx_path']}/../../") for directory in idedata["includes"]["toolchain"]: - if directory.startswith(toolchain_dir): + if directory.startswith(toolchain_dir) and "picolibc" not in directory: cmd.extend(["-isystem", directory]) # add library include directories using -isystem to suppress their errors