From d507feaf8106fe8ead595d97a84bb05c60d0df85 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 24 Apr 2026 03:37:11 -0500 Subject: [PATCH] [core] Scheduler::next_schedule_in: also handle single-threaded defer path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On ESPHOME_THREAD_SINGLE builds (ESP8266, RP2040), defer() does not route through defer_queue_ — set_timer_common_ treats it as an ordinary 0-delay set_timeout that stages in to_add_ (scheduler.cpp:197). Since next_schedule_in only inspects items_[0], any defer posted from a component's loop() sits in to_add_ invisible to the sleep calculation until the next scheduler.call() runs process_to_add(). The symmetric fix checks to_add_empty_() on single-threaded and short-circuits sleep the same way. This also costs one "skipped sleep" for any non-zero-delay timer added at runtime, but the next tick produces a correct next_schedule_in reading and the cost is a single yield — negligible vs. the stall this closes. Suggested by Copilot review on #15968. --- esphome/core/scheduler.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index 3e576ecdcc..fc891fac6f 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -423,6 +423,12 @@ optional HOT Scheduler::next_schedule_in(uint32_t now) { // iteration has work to do right now -- don't let the caller sleep. if (!this->defer_empty_()) return 0; +#else + // On single-threaded builds, defer() routes through set_timeout(..., 0) which + // stages in to_add_. process_to_add() runs at the top of every scheduler.call(), + // so anything in to_add_ becomes runnable on the next iteration; don't sleep. + if (!this->to_add_empty_()) + return 0; #endif // If no items, return empty optional