From 75c9430d91ffde5a1febfe6520686b14c6b6e68a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 8 Sep 2025 10:41:03 -0500 Subject: [PATCH 1/4] [core] Fix serial upload regression from DNS resolution PR #10595 (#10648) --- esphome/__main__.py | 39 +++++++++++++++------------------------ esphome/espota2.py | 13 +++++++------ 2 files changed, 22 insertions(+), 30 deletions(-) diff --git a/esphome/__main__.py b/esphome/__main__.py index 70d5cacd72..e1f683397f 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -398,28 +398,27 @@ def check_permissions(port: str): def upload_program( config: ConfigType, args: ArgsProtocol, devices: list[str] -) -> int | str: +) -> tuple[int, str | None]: host = devices[0] try: module = importlib.import_module("esphome.components." + CORE.target_platform) if getattr(module, "upload_program")(config, args, host): - return 0 + return 0, host except AttributeError: pass if get_port_type(host) == "SERIAL": check_permissions(host) + + exit_code = 1 if CORE.target_platform in (PLATFORM_ESP32, PLATFORM_ESP8266): file = getattr(args, "file", None) - return upload_using_esptool(config, host, file, args.upload_speed) + exit_code = upload_using_esptool(config, host, file, args.upload_speed) + elif CORE.target_platform == PLATFORM_RP2040 or CORE.is_libretiny: + exit_code = upload_using_platformio(config, host) + # else: Unknown target platform, exit_code remains 1 - if CORE.target_platform in (PLATFORM_RP2040): - return upload_using_platformio(config, host) - - if CORE.is_libretiny: - return upload_using_platformio(config, host) - - return 1 # Unknown target platform + return exit_code, host if exit_code == 0 else None ota_conf = {} for ota_item in config.get(CONF_OTA, []): @@ -553,7 +552,7 @@ def command_upload(args: ArgsProtocol, config: ConfigType) -> int | None: purpose="uploading", ) - exit_code = upload_program(config, args, devices) + exit_code, _ = upload_program(config, args, devices) if exit_code == 0: _LOGGER.info("Successfully uploaded program.") else: @@ -610,19 +609,11 @@ def command_run(args: ArgsProtocol, config: ConfigType) -> int | None: purpose="uploading", ) - # Try each device for upload until one succeeds - successful_device: str | None = None - for device in devices: - _LOGGER.info("Uploading to %s", device) - exit_code = upload_program(config, args, device) - if exit_code == 0: - _LOGGER.info("Successfully uploaded program.") - successful_device = device - break - if len(devices) > 1: - _LOGGER.warning("Failed to upload to %s", device) - - if successful_device is None: + exit_code, successful_device = upload_program(config, args, devices) + if exit_code == 0: + _LOGGER.info("Successfully uploaded program.") + else: + _LOGGER.warning("Failed to upload to %s", devices) return exit_code if args.no_logs: diff --git a/esphome/espota2.py b/esphome/espota2.py index d83f25a303..3d25af985b 100644 --- a/esphome/espota2.py +++ b/esphome/espota2.py @@ -310,7 +310,7 @@ def perform_ota( def run_ota_impl_( remote_host: str | list[str], remote_port: int, password: str, filename: str -) -> int: +) -> tuple[int, str | None]: # Handle both single host and list of hosts try: # Resolve all hosts at once for parallel DNS resolution @@ -344,21 +344,22 @@ def run_ota_impl_( perform_ota(sock, password, file_handle, filename) except OTAError as err: _LOGGER.error(str(err)) - return 1 + return 1, None finally: sock.close() - return 0 + # Successfully uploaded to sa[0] + return 0, sa[0] _LOGGER.error("Connection failed.") - return 1 + return 1, None def run_ota( remote_host: str | list[str], remote_port: int, password: str, filename: str -) -> int: +) -> tuple[int, str | None]: try: return run_ota_impl_(remote_host, remote_port, password, filename) except OTAError as err: _LOGGER.error(err) - return 1 + return 1, None From 703b5927938d5cb6859f8b5c1a16dca2e5e093a6 Mon Sep 17 00:00:00 2001 From: Thomas Rupprecht Date: Mon, 8 Sep 2025 20:03:41 +0200 Subject: [PATCH 2/4] Add I2S Audio Port for ESP32-C5/C6/H2 (#10414) --- esphome/components/i2s_audio/__init__.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/esphome/components/i2s_audio/__init__.py b/esphome/components/i2s_audio/__init__.py index bfa1b726f1..cff91a546f 100644 --- a/esphome/components/i2s_audio/__init__.py +++ b/esphome/components/i2s_audio/__init__.py @@ -4,6 +4,9 @@ from esphome.components.esp32 import add_idf_sdkconfig_option, get_esp32_variant from esphome.components.esp32.const import ( VARIANT_ESP32, VARIANT_ESP32C3, + VARIANT_ESP32C5, + VARIANT_ESP32C6, + VARIANT_ESP32H2, VARIANT_ESP32P4, VARIANT_ESP32S2, VARIANT_ESP32S3, @@ -62,12 +65,15 @@ I2S_ROLE_OPTIONS = { CONF_SECONDARY: i2s_role_t.I2S_ROLE_SLAVE, # NOLINT } -# https://github.com/espressif/esp-idf/blob/master/components/soc/{variant}/include/soc/soc_caps.h +# https://github.com/espressif/esp-idf/blob/master/components/soc/{variant}/include/soc/soc_caps.h (SOC_I2S_NUM) I2S_PORTS = { VARIANT_ESP32: 2, VARIANT_ESP32S2: 1, VARIANT_ESP32S3: 2, VARIANT_ESP32C3: 1, + VARIANT_ESP32C5: 1, + VARIANT_ESP32C6: 1, + VARIANT_ESP32H2: 1, VARIANT_ESP32P4: 3, } From 5cc0e21bc7d4114ead57cfed0a4fdf894efb3c70 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 8 Sep 2025 16:04:07 -0500 Subject: [PATCH 3/4] [core] Reduce unnecessary nesting in scheduler loop (#10644) --- esphome/core/scheduler.cpp | 122 ++++++++++++++++++------------------- 1 file changed, 58 insertions(+), 64 deletions(-) diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index 262349b6f9..68da0a56ca 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -436,85 +436,79 @@ void HOT Scheduler::call(uint32_t now) { this->to_remove_ = 0; } while (!this->items_.empty()) { - // use scoping to indicate visibility of `item` variable - { - // Don't copy-by value yet - auto &item = this->items_[0]; - if (item->get_next_execution() > now_64) { - // Not reached timeout yet, done for this call - break; - } - // Don't run on failed components - if (item->component != nullptr && item->component->is_failed()) { - LockGuard guard{this->lock_}; - this->pop_raw_(); - continue; - } + // Don't copy-by value yet + auto &item = this->items_[0]; + if (item->get_next_execution() > now_64) { + // Not reached timeout yet, done for this call + break; + } + // Don't run on failed components + if (item->component != nullptr && item->component->is_failed()) { + LockGuard guard{this->lock_}; + this->pop_raw_(); + continue; + } - // Check if item is marked for removal - // This handles two cases: - // 1. Item was marked for removal after cleanup_() but before we got here - // 2. Item is marked for removal but wasn't at the front of the heap during cleanup_() + // Check if item is marked for removal + // This handles two cases: + // 1. Item was marked for removal after cleanup_() but before we got here + // 2. Item is marked for removal but wasn't at the front of the heap during cleanup_() #ifdef ESPHOME_THREAD_MULTI_NO_ATOMICS - // Multi-threaded platforms without atomics: must take lock to safely read remove flag - { - LockGuard guard{this->lock_}; - if (is_item_removed_(item.get())) { - this->pop_raw_(); - this->to_remove_--; - continue; - } - } -#else - // Single-threaded or multi-threaded with atomics: can check without lock + // Multi-threaded platforms without atomics: must take lock to safely read remove flag + { + LockGuard guard{this->lock_}; if (is_item_removed_(item.get())) { - LockGuard guard{this->lock_}; this->pop_raw_(); this->to_remove_--; continue; } + } +#else + // Single-threaded or multi-threaded with atomics: can check without lock + if (is_item_removed_(item.get())) { + LockGuard guard{this->lock_}; + this->pop_raw_(); + this->to_remove_--; + continue; + } #endif #ifdef ESPHOME_DEBUG_SCHEDULER - const char *item_name = item->get_name(); - ESP_LOGV(TAG, "Running %s '%s/%s' with interval=%" PRIu32 " next_execution=%" PRIu64 " (now=%" PRIu64 ")", - item->get_type_str(), LOG_STR_ARG(item->get_source()), item_name ? item_name : "(null)", item->interval, - item->get_next_execution(), now_64); + const char *item_name = item->get_name(); + ESP_LOGV(TAG, "Running %s '%s/%s' with interval=%" PRIu32 " next_execution=%" PRIu64 " (now=%" PRIu64 ")", + item->get_type_str(), LOG_STR_ARG(item->get_source()), item_name ? item_name : "(null)", item->interval, + item->get_next_execution(), now_64); #endif /* ESPHOME_DEBUG_SCHEDULER */ - // Warning: During callback(), a lot of stuff can happen, including: - // - timeouts/intervals get added, potentially invalidating vector pointers - // - timeouts/intervals get cancelled - this->execute_item_(item.get(), now); + // Warning: During callback(), a lot of stuff can happen, including: + // - timeouts/intervals get added, potentially invalidating vector pointers + // - timeouts/intervals get cancelled + this->execute_item_(item.get(), now); + + LockGuard guard{this->lock_}; + + auto executed_item = std::move(this->items_[0]); + // Only pop after function call, this ensures we were reachable + // during the function call and know if we were cancelled. + this->pop_raw_(); + + if (executed_item->remove) { + // We were removed/cancelled in the function call, stop + this->to_remove_--; + continue; } - { - LockGuard guard{this->lock_}; - - // new scope, item from before might have been moved in the vector - auto item = std::move(this->items_[0]); - // Only pop after function call, this ensures we were reachable - // during the function call and know if we were cancelled. - this->pop_raw_(); - - if (item->remove) { - // We were removed/cancelled in the function call, stop - this->to_remove_--; - continue; - } - - if (item->type == SchedulerItem::INTERVAL) { - item->set_next_execution(now_64 + item->interval); - // Add new item directly to to_add_ - // since we have the lock held - this->to_add_.push_back(std::move(item)); - } else { - // Timeout completed - recycle it - this->recycle_item_(std::move(item)); - } - - has_added_items |= !this->to_add_.empty(); + if (executed_item->type == SchedulerItem::INTERVAL) { + executed_item->set_next_execution(now_64 + executed_item->interval); + // Add new item directly to to_add_ + // since we have the lock held + this->to_add_.push_back(std::move(executed_item)); + } else { + // Timeout completed - recycle it + this->recycle_item_(std::move(executed_item)); } + + has_added_items |= !this->to_add_.empty(); } if (has_added_items) { From f6d69231e891d84b0c56a0dae14037edd66aec98 Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Tue, 9 Sep 2025 01:10:29 +0200 Subject: [PATCH 4/4] [light] add missing header (#10590) --- esphome/components/light/light_state.h | 1 + .../components/light/test.nrf52-adafruit.yaml | 19 +++++++++++++++++++ tests/components/light/test.nrf52-mcumgr.yaml | 19 +++++++++++++++++++ 3 files changed, 39 insertions(+) create mode 100644 tests/components/light/test.nrf52-adafruit.yaml create mode 100644 tests/components/light/test.nrf52-mcumgr.yaml diff --git a/esphome/components/light/light_state.h b/esphome/components/light/light_state.h index 48323dd3c3..1427c02c35 100644 --- a/esphome/components/light/light_state.h +++ b/esphome/components/light/light_state.h @@ -12,6 +12,7 @@ #include "light_transformer.h" #include +#include namespace esphome { namespace light { diff --git a/tests/components/light/test.nrf52-adafruit.yaml b/tests/components/light/test.nrf52-adafruit.yaml new file mode 100644 index 0000000000..cb421ed4bb --- /dev/null +++ b/tests/components/light/test.nrf52-adafruit.yaml @@ -0,0 +1,19 @@ +esphome: + on_boot: + then: + - light.toggle: test_binary_light + +output: + - platform: gpio + id: test_binary + pin: 0 + +light: + - platform: binary + id: test_binary_light + name: Binary Light + output: test_binary + effects: + - strobe: + on_state: + - logger.log: Binary light state changed diff --git a/tests/components/light/test.nrf52-mcumgr.yaml b/tests/components/light/test.nrf52-mcumgr.yaml new file mode 100644 index 0000000000..cb421ed4bb --- /dev/null +++ b/tests/components/light/test.nrf52-mcumgr.yaml @@ -0,0 +1,19 @@ +esphome: + on_boot: + then: + - light.toggle: test_binary_light + +output: + - platform: gpio + id: test_binary + pin: 0 + +light: + - platform: binary + id: test_binary_light + name: Binary Light + output: test_binary + effects: + - strobe: + on_state: + - logger.log: Binary light state changed