From ef9fc87351a288293c0ad3e9ab59c7788af8db09 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 27 Feb 2026 11:17:04 -0500 Subject: [PATCH 1/8] [zigbee] Fix codegen ordering for basic/identify attribute lists (#14343) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- esphome/components/zigbee/__init__.py | 3 ++- esphome/components/zigbee/zigbee_zephyr.py | 8 +++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/esphome/components/zigbee/__init__.py b/esphome/components/zigbee/__init__.py index 7e917a9d704..a327cc29886 100644 --- a/esphome/components/zigbee/__init__.py +++ b/esphome/components/zigbee/__init__.py @@ -8,7 +8,7 @@ from esphome.components.zephyr import zephyr_add_pm_static, zephyr_data from esphome.components.zephyr.const import KEY_BOOTLOADER import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_INTERNAL, CONF_NAME -from esphome.core import CORE +from esphome.core import CORE, CoroPriority, coroutine_with_priority from esphome.types import ConfigType from .const_zephyr import ( @@ -96,6 +96,7 @@ FINAL_VALIDATE_SCHEMA = cv.All( ) +@coroutine_with_priority(CoroPriority.CORE) async def to_code(config: ConfigType) -> None: cg.add_define("USE_ZIGBEE") if CORE.using_zephyr: diff --git a/esphome/components/zigbee/zigbee_zephyr.py b/esphome/components/zigbee/zigbee_zephyr.py index 0b6daa9476a..a1e6ad3097e 100644 --- a/esphome/components/zigbee/zigbee_zephyr.py +++ b/esphome/components/zigbee/zigbee_zephyr.py @@ -179,6 +179,13 @@ async def zephyr_to_code(config: ConfigType) -> None: "USE_ZIGBEE_WIPE_ON_BOOT_MAGIC", random.randint(0x000001, 0xFFFFFF) ) cg.add_define("USE_ZIGBEE_WIPE_ON_BOOT") + + # Generate attribute lists before any await that could yield (e.g., build_automation + # waiting for variables from other components). If the hub's priority decays while + # yielding, deferred entity jobs may add cluster list globals that reference these + # attribute lists before they're declared. + await _attr_to_code(config) + var = cg.new_Pvariable(config[CONF_ID]) if on_join_config := config.get(CONF_ON_JOIN): @@ -186,7 +193,6 @@ async def zephyr_to_code(config: ConfigType) -> None: await cg.register_component(var, config) - await _attr_to_code(config) CORE.add_job(_ctx_to_code, config) From 017d1b2872b9ad85bf5b8a2fb563d14ff517975c Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Fri, 27 Feb 2026 11:12:50 -0600 Subject: [PATCH 2/8] [audio] Bump microOpus to v0.3.4 (#14346) --- 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 d8d426ec63a..d95fcf66d7b 100644 --- a/esphome/components/audio/__init__.py +++ b/esphome/components/audio/__init__.py @@ -214,4 +214,4 @@ async def to_code(config): cg.add_define("USE_AUDIO_MP3_SUPPORT") if data.opus_support: cg.add_define("USE_AUDIO_OPUS_SUPPORT") - add_idf_component(name="esphome/micro-opus", ref="0.3.3") + add_idf_component(name="esphome/micro-opus", ref="0.3.4") diff --git a/esphome/idf_component.yml b/esphome/idf_component.yml index 83b2d9d95c8..37bda65afdd 100644 --- a/esphome/idf_component.yml +++ b/esphome/idf_component.yml @@ -4,7 +4,7 @@ dependencies: esphome/esp-audio-libs: version: 2.0.3 esphome/micro-opus: - version: 0.3.3 + version: 0.3.4 espressif/esp-tflite-micro: version: 1.3.3~1 espressif/esp32-camera: From 20314b4d63ee07d97840c821cf1ca0003393a339 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Feb 2026 10:20:08 -0700 Subject: [PATCH 3/8] [mdns] Update espressif/mdns to v1.10.0 (#14338) --- esphome/components/mdns/__init__.py | 2 +- esphome/idf_component.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/mdns/__init__.py b/esphome/components/mdns/__init__.py index 420e6a60e30..0d535d69709 100644 --- a/esphome/components/mdns/__init__.py +++ b/esphome/components/mdns/__init__.py @@ -163,7 +163,7 @@ async def to_code(config): cg.add_library("LEAmDNS", None) if CORE.is_esp32: - add_idf_component(name="espressif/mdns", ref="1.9.1") + add_idf_component(name="espressif/mdns", ref="1.10.0") cg.add_define("USE_MDNS") diff --git a/esphome/idf_component.yml b/esphome/idf_component.yml index 37bda65afdd..550e7b9af79 100644 --- a/esphome/idf_component.yml +++ b/esphome/idf_component.yml @@ -10,7 +10,7 @@ dependencies: espressif/esp32-camera: version: 2.1.1 espressif/mdns: - version: 1.9.1 + version: 1.10.0 espressif/esp_wifi_remote: version: 1.3.2 rules: From 72ca514cc2650e641faa774a7d43a81dcdf6fb98 Mon Sep 17 00:00:00 2001 From: deirdreobyrne Date: Fri, 27 Feb 2026 17:25:53 +0000 Subject: [PATCH 4/8] [esp32_hosted] Add configurable SDIO clock frequency (#14319) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: Deirdre Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- esphome/components/esp32_hosted/__init__.py | 8 ++++++++ .../test-sdio-speed.esp32-p4-idf.yaml | 16 ++++++++++++++++ 2 files changed, 24 insertions(+) create mode 100644 tests/components/esp32_hosted/test-sdio-speed.esp32-p4-idf.yaml diff --git a/esphome/components/esp32_hosted/__init__.py b/esphome/components/esp32_hosted/__init__.py index 287c780769d..720d81acc4a 100644 --- a/esphome/components/esp32_hosted/__init__.py +++ b/esphome/components/esp32_hosted/__init__.py @@ -23,6 +23,7 @@ CONF_D1_PIN = "d1_pin" CONF_D2_PIN = "d2_pin" CONF_D3_PIN = "d3_pin" CONF_SLOT = "slot" +CONF_SDIO_FREQUENCY = "sdio_frequency" CONFIG_SCHEMA = cv.All( cv.Schema( @@ -37,6 +38,9 @@ CONFIG_SCHEMA = cv.All( cv.Required(CONF_D3_PIN): pins.internal_gpio_output_pin_number, cv.Required(CONF_RESET_PIN): pins.internal_gpio_output_pin_number, cv.Optional(CONF_SLOT, default=1): cv.int_range(min=0, max=1), + cv.Optional(CONF_SDIO_FREQUENCY, default="40MHz"): cv.All( + cv.frequency, cv.Range(min=400e3, max=50e6) + ), } ), ) @@ -91,6 +95,10 @@ async def to_code(config): config[CONF_D3_PIN], ) esp32.add_idf_sdkconfig_option("CONFIG_ESP_HOSTED_CUSTOM_SDIO_PINS", True) + esp32.add_idf_sdkconfig_option( + "CONFIG_ESP_HOSTED_SDIO_CLOCK_FREQ_KHZ", + int(config[CONF_SDIO_FREQUENCY] // 1000), + ) framework_ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] os.environ["ESP_IDF_VERSION"] = f"{framework_ver.major}.{framework_ver.minor}" diff --git a/tests/components/esp32_hosted/test-sdio-speed.esp32-p4-idf.yaml b/tests/components/esp32_hosted/test-sdio-speed.esp32-p4-idf.yaml new file mode 100644 index 00000000000..9268c9ae415 --- /dev/null +++ b/tests/components/esp32_hosted/test-sdio-speed.esp32-p4-idf.yaml @@ -0,0 +1,16 @@ +esp32_hosted: + variant: ESP32C6 + slot: 1 + active_high: true + reset_pin: GPIO15 + cmd_pin: GPIO13 + clk_pin: GPIO12 + d0_pin: GPIO11 + d1_pin: GPIO10 + d2_pin: GPIO9 + d3_pin: GPIO8 + sdio_frequency: 8MHz + +wifi: + ssid: MySSID + password: password1 From 1c7f769ec7f54775c3d31f426dcfbb14f66a911f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Feb 2026 10:48:21 -0700 Subject: [PATCH 5/8] [core] Add millis_64() HAL function with native ESP32 implementation (#14339) --- esphome/components/esp32/core.cpp | 1 + esphome/components/esp8266/core.cpp | 2 ++ esphome/components/host/core.cpp | 2 ++ esphome/components/libretiny/core.cpp | 2 ++ esphome/components/rp2040/core.cpp | 2 ++ .../uptime/sensor/uptime_seconds_sensor.cpp | 4 +-- .../uptime/text_sensor/uptime_text_sensor.cpp | 4 +-- esphome/components/web_server/web_server.cpp | 4 +-- esphome/components/zephyr/core.cpp | 2 ++ esphome/core/hal.h | 1 + esphome/core/scheduler.cpp | 34 ++++++++++-------- esphome/core/scheduler.h | 35 +++++++++++++++---- 12 files changed, 66 insertions(+), 27 deletions(-) diff --git a/esphome/components/esp32/core.cpp b/esphome/components/esp32/core.cpp index 202d929ab93..b9ae871abf9 100644 --- a/esphome/components/esp32/core.cpp +++ b/esphome/components/esp32/core.cpp @@ -23,6 +23,7 @@ namespace esphome { void HOT yield() { vPortYield(); } uint32_t IRAM_ATTR HOT millis() { return (uint32_t) (esp_timer_get_time() / 1000ULL); } +uint64_t HOT millis_64() { return static_cast(esp_timer_get_time()) / 1000ULL; } void HOT delay(uint32_t ms) { vTaskDelay(ms / portTICK_PERIOD_MS); } uint32_t IRAM_ATTR HOT micros() { return (uint32_t) esp_timer_get_time(); } void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); } diff --git a/esphome/components/esp8266/core.cpp b/esphome/components/esp8266/core.cpp index 236d3022be3..497e99b61f1 100644 --- a/esphome/components/esp8266/core.cpp +++ b/esphome/components/esp8266/core.cpp @@ -3,6 +3,7 @@ #include "core.h" #include "esphome/core/defines.h" #include "esphome/core/hal.h" +#include "esphome/core/application.h" #include "esphome/core/helpers.h" #include "preferences.h" #include @@ -16,6 +17,7 @@ namespace esphome { void HOT yield() { ::yield(); } uint32_t IRAM_ATTR HOT millis() { return ::millis(); } +uint64_t millis_64() { return App.scheduler.millis_64_impl_(::millis()); } void HOT delay(uint32_t ms) { ::delay(ms); } uint32_t IRAM_ATTR HOT micros() { return ::micros(); } void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); } diff --git a/esphome/components/host/core.cpp b/esphome/components/host/core.cpp index c20a33fa378..5c08717ac99 100644 --- a/esphome/components/host/core.cpp +++ b/esphome/components/host/core.cpp @@ -1,5 +1,6 @@ #ifdef USE_HOST +#include "esphome/core/application.h" #include "esphome/core/hal.h" #include "esphome/core/helpers.h" #include "preferences.h" @@ -19,6 +20,7 @@ uint32_t IRAM_ATTR HOT millis() { uint32_t ms = round(spec.tv_nsec / 1e6); return ((uint32_t) seconds) * 1000U + ms; } +uint64_t millis_64() { return App.scheduler.millis_64_impl_(millis()); } void HOT delay(uint32_t ms) { struct timespec ts; ts.tv_sec = ms / 1000; diff --git a/esphome/components/libretiny/core.cpp b/esphome/components/libretiny/core.cpp index 4dda7c38563..6cbc81938d9 100644 --- a/esphome/components/libretiny/core.cpp +++ b/esphome/components/libretiny/core.cpp @@ -3,6 +3,7 @@ #include "core.h" #include "esphome/core/defines.h" #include "esphome/core/hal.h" +#include "esphome/core/application.h" #include "esphome/core/helpers.h" #include "preferences.h" @@ -13,6 +14,7 @@ namespace esphome { void HOT yield() { ::yield(); } uint32_t IRAM_ATTR HOT millis() { return ::millis(); } +uint64_t millis_64() { return App.scheduler.millis_64_impl_(::millis()); } uint32_t IRAM_ATTR HOT micros() { return ::micros(); } void HOT delay(uint32_t ms) { ::delay(ms); } void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { ::delayMicroseconds(us); } diff --git a/esphome/components/rp2040/core.cpp b/esphome/components/rp2040/core.cpp index 37378d88bb1..52c6f1185c4 100644 --- a/esphome/components/rp2040/core.cpp +++ b/esphome/components/rp2040/core.cpp @@ -3,6 +3,7 @@ #include "core.h" #include "esphome/core/defines.h" #include "esphome/core/hal.h" +#include "esphome/core/application.h" #include "esphome/core/helpers.h" #include "hardware/watchdog.h" @@ -11,6 +12,7 @@ namespace esphome { void HOT yield() { ::yield(); } uint32_t IRAM_ATTR HOT millis() { return ::millis(); } +uint64_t millis_64() { return App.scheduler.millis_64_impl_(::millis()); } void HOT delay(uint32_t ms) { ::delay(ms); } uint32_t IRAM_ATTR HOT micros() { return ::micros(); } void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); } diff --git a/esphome/components/uptime/sensor/uptime_seconds_sensor.cpp b/esphome/components/uptime/sensor/uptime_seconds_sensor.cpp index 20e8ed8fdae..552e4e5da42 100644 --- a/esphome/components/uptime/sensor/uptime_seconds_sensor.cpp +++ b/esphome/components/uptime/sensor/uptime_seconds_sensor.cpp @@ -1,6 +1,6 @@ #include "uptime_seconds_sensor.h" -#include "esphome/core/application.h" +#include "esphome/core/hal.h" #include "esphome/core/log.h" namespace esphome::uptime { @@ -8,7 +8,7 @@ namespace esphome::uptime { static const char *const TAG = "uptime.sensor"; void UptimeSecondsSensor::update() { - const uint64_t uptime = App.scheduler.millis_64(); + const uint64_t uptime = millis_64(); const uint64_t seconds_int = uptime / 1000ULL; const float seconds = float(seconds_int) + (uptime % 1000ULL) / 1000.0f; this->publish_state(seconds); diff --git a/esphome/components/uptime/text_sensor/uptime_text_sensor.cpp b/esphome/components/uptime/text_sensor/uptime_text_sensor.cpp index 88ae53fbfcd..c89d23672e9 100644 --- a/esphome/components/uptime/text_sensor/uptime_text_sensor.cpp +++ b/esphome/components/uptime/text_sensor/uptime_text_sensor.cpp @@ -1,6 +1,6 @@ #include "uptime_text_sensor.h" -#include "esphome/core/application.h" +#include "esphome/core/hal.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" @@ -19,7 +19,7 @@ static void append_unit(char *buf, size_t buf_size, size_t &pos, const char *sep void UptimeTextSensor::setup() { this->update(); } void UptimeTextSensor::update() { - uint32_t uptime = static_cast(App.scheduler.millis_64() / 1000); + uint32_t uptime = static_cast(millis_64() / 1000); unsigned interval = this->get_update_interval() / 1000; // Calculate all time units diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index e2f9c213314..16674321c9d 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -385,7 +385,7 @@ json::SerializationBuffer<> WebServer::get_config_json() { #endif root[ESPHOME_F("log")] = this->expose_log_; root[ESPHOME_F("lang")] = "en"; - root[ESPHOME_F("uptime")] = static_cast(App.scheduler.millis_64() / 1000); + root[ESPHOME_F("uptime")] = static_cast(millis_64() / 1000); return builder.serialize(); } @@ -414,7 +414,7 @@ void WebServer::setup() { // getting a lot of events this->set_interval(10000, [this]() { char buf[32]; - auto uptime = static_cast(App.scheduler.millis_64() / 1000); + auto uptime = static_cast(millis_64() / 1000); buf_append_printf(buf, sizeof(buf), 0, "{\"uptime\":%u}", uptime); this->events_.try_send_nodefer(buf, "ping", millis(), 30000); }); diff --git a/esphome/components/zephyr/core.cpp b/esphome/components/zephyr/core.cpp index d7027b33f57..7f5d6d44faa 100644 --- a/esphome/components/zephyr/core.cpp +++ b/esphome/components/zephyr/core.cpp @@ -4,6 +4,7 @@ #include #include #include +#include "esphome/core/application.h" #include "esphome/core/hal.h" #include "esphome/core/helpers.h" #include "esphome/core/defines.h" @@ -17,6 +18,7 @@ static const device *const WDT = DEVICE_DT_GET(DT_ALIAS(watchdog0)); void yield() { ::k_yield(); } uint32_t millis() { return k_ticks_to_ms_floor32(k_uptime_ticks()); } +uint64_t millis_64() { return App.scheduler.millis_64_impl_(millis()); } uint32_t micros() { return k_ticks_to_us_floor32(k_uptime_ticks()); } void delayMicroseconds(uint32_t us) { ::k_usleep(us); } void delay(uint32_t ms) { ::k_msleep(ms); } diff --git a/esphome/core/hal.h b/esphome/core/hal.h index 1a4230e4215..fa5ca646f2e 100644 --- a/esphome/core/hal.h +++ b/esphome/core/hal.h @@ -32,6 +32,7 @@ namespace esphome { void yield(); uint32_t millis(); +uint64_t millis_64(); uint32_t micros(); void delay(uint32_t ms); void delayMicroseconds(uint32_t us); // NOLINT(readability-identifier-naming) diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index d9c66b2000b..06c0bb8b1bf 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -28,8 +28,10 @@ static constexpr size_t MAX_POOL_SIZE = 5; // Set to 5 to match the pool size - when we have as many cancelled items as our // pool can hold, it's time to clean up and recycle them. static constexpr uint32_t MAX_LOGICALLY_DELETED_ITEMS = 5; +#ifndef USE_ESP32 // Half the 32-bit range - used to detect rollovers vs normal time progression static constexpr uint32_t HALF_MAX_UINT32 = std::numeric_limits::max() / 2; +#endif // max delay to start an interval sequence static constexpr uint32_t MAX_INTERVAL_DELAY = 5000; @@ -150,8 +152,8 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type return; } - // Get fresh timestamp BEFORE taking lock - millis_64_ may need to acquire lock itself - const uint64_t now = this->millis_64_(millis()); + // Get fresh 64-bit timestamp BEFORE taking lock + const uint64_t now_64 = millis_64(); // Take lock early to protect scheduler_item_pool_ access LockGuard guard{this->lock_}; @@ -184,7 +186,7 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type item->interval = delay; // first execution happens immediately after a random smallish offset uint32_t offset = this->calculate_interval_offset_(delay); - item->set_next_execution(now + offset); + item->set_next_execution(now_64 + offset); #ifdef ESPHOME_LOG_HAS_VERBOSE SchedulerNameLog name_log; ESP_LOGV(TAG, "Scheduler interval for %s is %" PRIu32 "ms, offset %" PRIu32 "ms", @@ -192,11 +194,11 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type #endif } else { item->interval = 0; - item->set_next_execution(now + delay); + item->set_next_execution(now_64 + delay); } #ifdef ESPHOME_DEBUG_SCHEDULER - this->debug_log_timer_(item.get(), name_type, static_name, hash_or_id, type, delay, now); + this->debug_log_timer_(item.get(), name_type, static_name, hash_or_id, type, delay, now_64); #endif /* ESPHOME_DEBUG_SCHEDULER */ // For retries, check if there's a cancelled timeout first @@ -399,8 +401,7 @@ optional HOT Scheduler::next_schedule_in(uint32_t now) { return {}; auto &item = this->items_[0]; - // Convert the fresh timestamp from caller (usually Application::loop()) to 64-bit - const auto now_64 = this->millis_64_(now); // 'now' from parameter - fresh from caller + const auto now_64 = this->millis_64_from_(now); const uint64_t next_exec = item->get_next_execution(); if (next_exec < now_64) return 0; @@ -461,8 +462,8 @@ void HOT Scheduler::call(uint32_t now) { this->process_defer_queue_(now); #endif /* not ESPHOME_THREAD_SINGLE */ - // Convert the fresh timestamp from main loop to 64-bit for scheduler operations - const auto now_64 = this->millis_64_(now); // 'now' from parameter - fresh from Application::loop() + // Extend the caller's 32-bit timestamp to 64-bit for scheduler operations + const auto now_64 = this->millis_64_from_(now); this->process_to_add(); // Track if any items were added to to_add_ during this call (intervals or from callbacks) @@ -474,15 +475,18 @@ void HOT Scheduler::call(uint32_t now) { if (now_64 - last_print > 2000) { last_print = now_64; std::vector old_items; -#ifdef ESPHOME_THREAD_MULTI_ATOMICS +#if !defined(USE_ESP32) && defined(ESPHOME_THREAD_MULTI_ATOMICS) const auto last_dbg = this->last_millis_.load(std::memory_order_relaxed); const auto major_dbg = this->millis_major_.load(std::memory_order_relaxed); ESP_LOGD(TAG, "Items: count=%zu, pool=%zu, now=%" PRIu64 " (%" PRIu16 ", %" PRIu32 ")", this->items_.size(), this->scheduler_item_pool_.size(), now_64, major_dbg, last_dbg); -#else /* not ESPHOME_THREAD_MULTI_ATOMICS */ +#elif !defined(USE_ESP32) ESP_LOGD(TAG, "Items: count=%zu, pool=%zu, now=%" PRIu64 " (%" PRIu16 ", %" PRIu32 ")", this->items_.size(), this->scheduler_item_pool_.size(), now_64, this->millis_major_, this->last_millis_); -#endif /* else ESPHOME_THREAD_MULTI_ATOMICS */ +#else + ESP_LOGD(TAG, "Items: count=%zu, pool=%zu, now=%" PRIu64, this->items_.size(), this->scheduler_item_pool_.size(), + now_64); +#endif // Cleanup before debug output this->cleanup_(); while (!this->items_.empty()) { @@ -710,9 +714,8 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, NameType name_type return total_cancelled > 0; } -uint64_t Scheduler::millis_64() { return this->millis_64_(millis()); } - -uint64_t Scheduler::millis_64_(uint32_t now) { +#ifndef USE_ESP32 +uint64_t Scheduler::millis_64_impl_(uint32_t now) { // THREAD SAFETY NOTE: // This function has three implementations, based on the precompiler flags // - ESPHOME_THREAD_SINGLE - Runs on single-threaded platforms (ESP8266, RP2040, etc.) @@ -869,6 +872,7 @@ uint64_t Scheduler::millis_64_(uint32_t now) { "No platform threading model defined. One of ESPHOME_THREAD_SINGLE, ESPHOME_THREAD_MULTI_NO_ATOMICS, or ESPHOME_THREAD_MULTI_ATOMICS must be defined." #endif } +#endif // not USE_ESP32 bool HOT Scheduler::SchedulerItem::cmp(const SchedulerItemPtr &a, const SchedulerItemPtr &b) { // High bits are almost always equal (change only on 32-bit rollover ~49 days) diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index 840ee7159a7..c6053258103 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -10,6 +10,7 @@ #endif #include "esphome/core/component.h" +#include "esphome/core/hal.h" #include "esphome/core/helpers.h" namespace esphome { @@ -117,12 +118,12 @@ class Scheduler { bool cancel_retry(Component *component, uint32_t id); /// Get 64-bit millisecond timestamp (handles 32-bit millis() rollover) - uint64_t millis_64(); + uint64_t millis_64() { return esphome::millis_64(); } - // Calculate when the next scheduled item should run - // @param now Fresh timestamp from millis() - must not be stale/cached - // Returns the time in milliseconds until the next scheduled item, or nullopt if no items - // This method performs cleanup of removed items before checking the schedule + // Calculate when the next scheduled item should run. + // @param now On ESP32, unused for 64-bit extension (native); on other platforms, extended to 64-bit via rollover. + // Returns the time in milliseconds until the next scheduled item, or nullopt if no items. + // This method performs cleanup of removed items before checking the schedule. // IMPORTANT: This method should only be called from the main thread (loop task). optional next_schedule_in(uint32_t now); @@ -282,7 +283,25 @@ class Scheduler { // Common implementation for cancel_retry bool cancel_retry_(Component *component, NameType name_type, const char *static_name, uint32_t hash_or_id); - uint64_t millis_64_(uint32_t now); + // Extend a 32-bit millis() value to 64-bit. Use when the caller already has a fresh now. + // On ESP32, ignores now and uses esp_timer_get_time() directly (native 64-bit). + // On non-ESP32, extends now to 64-bit using rollover tracking. + uint64_t millis_64_from_(uint32_t now) { +#ifdef USE_ESP32 + (void) now; + return millis_64(); +#else + return this->millis_64_impl_(now); +#endif + } + +#ifndef USE_ESP32 + // On non-ESP32 platforms, millis_64() HAL function delegates to this method + // which tracks 32-bit millis() rollover using millis_major_ and last_millis_. + // On ESP32, millis_64() uses esp_timer_get_time() directly. + friend uint64_t millis_64(); + uint64_t millis_64_impl_(uint32_t now); +#endif // Cleanup logically deleted items from the scheduler // Returns the number of items remaining after cleanup // IMPORTANT: This method should only be called from the main thread (loop task). @@ -549,6 +568,9 @@ class Scheduler { // to synchronize between tasks (see https://github.com/esphome/backlog/issues/52) std::vector scheduler_item_pool_; +#ifndef USE_ESP32 + // On ESP32, millis_64() uses esp_timer_get_time() directly; no rollover tracking needed. + // On other platforms, these fields track 32-bit millis() rollover for millis_64_impl_(). #ifdef ESPHOME_THREAD_MULTI_ATOMICS /* * Multi-threaded platforms with atomic support: last_millis_ needs atomic for lock-free updates @@ -577,6 +599,7 @@ class Scheduler { #else /* not ESPHOME_THREAD_MULTI_ATOMICS */ uint16_t millis_major_{0}; #endif /* else ESPHOME_THREAD_MULTI_ATOMICS */ +#endif /* not USE_ESP32 */ }; } // namespace esphome From 4fe173b64487b90d17fbca26727e0ad19bdd0d20 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Feb 2026 10:56:57 -0700 Subject: [PATCH 6/8] [wifi] Remove stale TODO comment for ESP8266 callback deferral (#14347) --- esphome/components/wifi/wifi_component_esp8266.cpp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index cbf7d7d80f8..8911bf15e07 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -470,10 +470,6 @@ const LogString *get_disconnect_reason_str(uint8_t reason) { return LOG_STR("Unspecified"); } -// TODO: This callback runs in ESP8266 system context with limited stack (~2KB). -// All listener notifications should be deferred to wifi_loop_() via pending_ flags -// to avoid stack overflow. Currently only connect_state is deferred; disconnect, -// IP, and scan listeners still run in this context and should be migrated. void WiFiComponent::wifi_event_callback(System_Event_t *event) { switch (event->event) { case EVENT_STAMODE_CONNECTED: { From c3a0eeceec21b6f7e97a2044078c611c843679e2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Feb 2026 11:17:17 -0700 Subject: [PATCH 7/8] [wifi] Use direct SDK APIs for LibreTiny SSID retrieval (#14349) --- .../wifi/wifi_component_libretiny.cpp | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/esphome/components/wifi/wifi_component_libretiny.cpp b/esphome/components/wifi/wifi_component_libretiny.cpp index 2cc05928afd..1c5490a3ac3 100644 --- a/esphome/components/wifi/wifi_component_libretiny.cpp +++ b/esphome/components/wifi/wifi_component_libretiny.cpp @@ -13,6 +13,19 @@ #include #include +#ifdef USE_BK72XX +extern "C" { +#include +} +#endif + +#ifdef USE_RTL87XX +extern "C" { +#include +#include +} +#endif + #include "esphome/core/application.h" #include "esphome/core/hal.h" #include "esphome/core/helpers.h" @@ -760,10 +773,22 @@ bssid_t WiFiComponent::wifi_bssid() { } std::string WiFiComponent::wifi_ssid() { return WiFi.SSID().c_str(); } const char *WiFiComponent::wifi_ssid_to(std::span buffer) { - // TODO: Find direct LibreTiny API to avoid Arduino String allocation +#ifdef USE_BK72XX + LinkStatusTypeDef link_status{}; + bk_wlan_get_link_status(&link_status); + size_t len = strnlen(reinterpret_cast(link_status.ssid), SSID_BUFFER_SIZE - 1); + memcpy(buffer.data(), link_status.ssid, len); +#elif defined(USE_RTL87XX) + rtw_wifi_setting_t setting{}; + wifi_get_setting("wlan0", &setting); + size_t len = strnlen(reinterpret_cast(setting.ssid), SSID_BUFFER_SIZE - 1); + memcpy(buffer.data(), setting.ssid, len); +#else + // LN882X: wifi_get_sta_conn_info() provides direct pointer access String ssid = WiFi.SSID(); size_t len = std::min(static_cast(ssid.length()), SSID_BUFFER_SIZE - 1); memcpy(buffer.data(), ssid.c_str(), len); +#endif buffer[len] = '\0'; return buffer.data(); } From 4ae7633418716d4f606e189ad1eb532d7ec00cb5 Mon Sep 17 00:00:00 2001 From: Michael Cassaniti Date: Sat, 28 Feb 2026 05:23:02 +1100 Subject: [PATCH 8/8] [safe_mode] Add feature to explicitly mark a boot as successful (#14306) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- esphome/components/safe_mode/__init__.py | 17 ++++++++++++++++ esphome/components/safe_mode/automation.h | 14 +++++++------ esphome/components/safe_mode/safe_mode.cpp | 20 +++++++++++-------- esphome/components/safe_mode/safe_mode.h | 2 ++ .../components/safe_mode/common-enabled.yaml | 4 ++++ 5 files changed, 43 insertions(+), 14 deletions(-) diff --git a/esphome/components/safe_mode/__init__.py b/esphome/components/safe_mode/__init__.py index d1754aaad72..f54151b7460 100644 --- a/esphome/components/safe_mode/__init__.py +++ b/esphome/components/safe_mode/__init__.py @@ -21,6 +21,7 @@ CONF_ON_SAFE_MODE = "on_safe_mode" safe_mode_ns = cg.esphome_ns.namespace("safe_mode") SafeModeComponent = safe_mode_ns.class_("SafeModeComponent", cg.Component) SafeModeTrigger = safe_mode_ns.class_("SafeModeTrigger", automation.Trigger.template()) +MarkSuccessfulAction = safe_mode_ns.class_("MarkSuccessfulAction", automation.Action) def _remove_id_if_disabled(value): @@ -53,6 +54,22 @@ CONFIG_SCHEMA = cv.All( ) +@automation.register_action( + "safe_mode.mark_successful", + MarkSuccessfulAction, + cv.Schema( + { + cv.GenerateID(): cv.use_id(SafeModeComponent), + } + ), +) +async def safe_mode_mark_successful_to_code(config, action_id, template_arg, args): + parent = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg) + cg.add(var.set_parent(parent)) + return var + + @coroutine_with_priority(CoroPriority.APPLICATION) async def to_code(config): if not config[CONF_DISABLED]: diff --git a/esphome/components/safe_mode/automation.h b/esphome/components/safe_mode/automation.h index 1d82ac45f14..dee02c64a07 100644 --- a/esphome/components/safe_mode/automation.h +++ b/esphome/components/safe_mode/automation.h @@ -1,20 +1,22 @@ #pragma once #include "esphome/core/defines.h" - -#ifdef USE_SAFE_MODE_CALLBACK -#include "safe_mode.h" - #include "esphome/core/automation.h" +#include "safe_mode.h" namespace esphome::safe_mode { +#ifdef USE_SAFE_MODE_CALLBACK class SafeModeTrigger final : public Trigger<> { public: explicit SafeModeTrigger(SafeModeComponent *parent) { parent->add_on_safe_mode_callback([this]() { trigger(); }); } }; +#endif // USE_SAFE_MODE_CALLBACK + +template class MarkSuccessfulAction : public Action, public Parented { + public: + void play(const Ts &...x) override { this->parent_->mark_successful(); } +}; } // namespace esphome::safe_mode - -#endif // USE_SAFE_MODE_CALLBACK diff --git a/esphome/components/safe_mode/safe_mode.cpp b/esphome/components/safe_mode/safe_mode.cpp index aa4fdb8291c..fe2acd96122 100644 --- a/esphome/components/safe_mode/safe_mode.cpp +++ b/esphome/components/safe_mode/safe_mode.cpp @@ -63,18 +63,22 @@ void SafeModeComponent::dump_config() { float SafeModeComponent::get_setup_priority() const { return setup_priority::AFTER_WIFI; } +void SafeModeComponent::mark_successful() { + this->clean_rtc(); + this->boot_successful_ = true; +#if defined(USE_ESP32) && defined(USE_OTA_ROLLBACK) + // Mark OTA partition as valid to prevent rollback + esp_ota_mark_app_valid_cancel_rollback(); +#endif + // Disable loop since we no longer need to check + this->disable_loop(); +} + void SafeModeComponent::loop() { if (!this->boot_successful_ && (millis() - this->safe_mode_start_time_) > this->safe_mode_boot_is_good_after_) { // successful boot, reset counter ESP_LOGI(TAG, "Boot seems successful; resetting boot loop counter"); - this->clean_rtc(); - this->boot_successful_ = true; -#if defined(USE_ESP32) && defined(USE_OTA_ROLLBACK) - // Mark OTA partition as valid to prevent rollback - esp_ota_mark_app_valid_cancel_rollback(); -#endif - // Disable loop since we no longer need to check - this->disable_loop(); + this->mark_successful(); } } diff --git a/esphome/components/safe_mode/safe_mode.h b/esphome/components/safe_mode/safe_mode.h index 902b8c415de..1b28ea28f20 100644 --- a/esphome/components/safe_mode/safe_mode.h +++ b/esphome/components/safe_mode/safe_mode.h @@ -31,6 +31,8 @@ class SafeModeComponent final : public Component { void on_safe_shutdown() override; + void mark_successful(); + #ifdef USE_SAFE_MODE_CALLBACK void add_on_safe_mode_callback(std::function &&callback) { this->safe_mode_callback_.add(std::move(callback)); diff --git a/tests/components/safe_mode/common-enabled.yaml b/tests/components/safe_mode/common-enabled.yaml index c24f49e6b69..43025c60dbd 100644 --- a/tests/components/safe_mode/common-enabled.yaml +++ b/tests/components/safe_mode/common-enabled.yaml @@ -16,3 +16,7 @@ button: switch: - platform: safe_mode name: Safe Mode Switch + +esphome: + on_boot: + - safe_mode.mark_successful