From 035a510aba8e0c03aae0bbe8ae919b7e2df04444 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 2 Nov 2025 17:11:13 -0600 Subject: [PATCH] fix conflict --- esphome/core/base_automation.h | 59 +++++++++++++++++++++++++--------- 1 file changed, 43 insertions(+), 16 deletions(-) diff --git a/esphome/core/base_automation.h b/esphome/core/base_automation.h index efe9aa1c47..541911f22a 100644 --- a/esphome/core/base_automation.h +++ b/esphome/core/base_automation.h @@ -406,12 +406,24 @@ template void RepeatLoopContinuation::play(uint32_t itera } } +/** Wait until a condition is true to continue execution. + * + * Uses queue-based storage to safely handle concurrent executions. + * While concurrent execution from the same trigger is uncommon, it's possible + * (e.g., rapid button presses, high-frequency sensor updates), so we use + * queue-based storage for correctness. + */ template class WaitUntilAction : public Action, public Component { public: WaitUntilAction(Condition *condition) : condition_(condition) {} TEMPLATABLE_VALUE(uint32_t, timeout_value) + void setup() override { + // Start with loop disabled - only enable when there's work to do + this->disable_loop(); + } + void play_complex(Ts... x) override { this->num_running_++; // Check if we can continue immediately. @@ -421,16 +433,14 @@ template class WaitUntilAction : public Action, public Co } return; } - this->var_ = std::make_tuple(x...); - if (this->timeout_value_.has_value()) { - // Lambda captures only 'this' to reference stored var_ - // vs std::bind which duplicates storage of x... (already in var_) - // This eliminates ~100-200 bytes of std::bind template instantiation code - auto f = [this]() { this->play_next_tuple_(this->var_); }; - this->set_timeout("timeout", this->timeout_value_.value(x...), f); - } + // Store for later processing + auto now = millis(); + auto timeout = this->timeout_value_.optional_value(x...); + this->var_queue_.emplace_front(now, timeout, std::make_tuple(x...)); + // Enable loop now that we have work to do + this->enable_loop(); this->loop(); } @@ -438,13 +448,32 @@ template class WaitUntilAction : public Action, public Co if (this->num_running_ == 0) return; - if (!this->condition_->check_tuple(this->var_)) { - return; + auto now = millis(); + + this->var_queue_.remove_if([&](auto &queued) { + auto start = std::get(queued); + auto timeout = std::get>(queued); + auto &var = std::get>(queued); + + auto expired = timeout && (now - start) >= *timeout; + + if (!expired && !this->condition_->check_tuple(var)) { + return false; + } + + this->play_next_tuple_(var); + return true; + }); + + // If queue is now empty, disable loop until next play_complex + if (this->var_queue_.empty()) { + this->disable_loop(); } + } - this->cancel_timeout("timeout"); - - this->play_next_tuple_(this->var_); + void stop() override { + this->var_queue_.clear(); + this->disable_loop(); } float get_setup_priority() const override { return setup_priority::DATA; } @@ -452,11 +481,9 @@ template class WaitUntilAction : public Action, public Co void play(Ts... x) override { /* ignore - see play_complex */ } - void stop() override { this->cancel_timeout("timeout"); } - protected: Condition *condition_; - std::tuple var_{}; + std::forward_list, std::tuple>> var_queue_{}; }; template class UpdateComponentAction : public Action {