#pragma once #include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/hal.h" #include "esphome/core/defines.h" #include "esphome/core/preferences.h" #include "esphome/core/scheduler.h" #include "esphome/core/application.h" #include "esphome/core/helpers.h" #include #include #include namespace esphome { template class AndCondition : public Condition { public: explicit AndCondition(std::initializer_list *> conditions) { init_array_from(this->conditions_, conditions); } bool check(const Ts &...x) override { for (auto *condition : this->conditions_) { if (!condition->check(x...)) return false; } return true; } protected: std::array *, N> conditions_{}; }; template class OrCondition : public Condition { public: explicit OrCondition(std::initializer_list *> conditions) { init_array_from(this->conditions_, conditions); } bool check(const Ts &...x) override { for (auto *condition : this->conditions_) { if (condition->check(x...)) return true; } return false; } protected: std::array *, N> conditions_{}; }; template class NotCondition : public Condition { public: explicit NotCondition(Condition *condition) : condition_(condition) {} bool check(const Ts &...x) override { return !this->condition_->check(x...); } protected: Condition *condition_; }; template class XorCondition : public Condition { public: explicit XorCondition(std::initializer_list *> conditions) { init_array_from(this->conditions_, conditions); } bool check(const Ts &...x) override { size_t result = 0; for (auto *condition : this->conditions_) { result += condition->check(x...); } return result == 1; } protected: std::array *, N> conditions_{}; }; template class LambdaCondition : public Condition { public: explicit LambdaCondition(std::function &&f) : f_(std::move(f)) {} bool check(const Ts &...x) override { return this->f_(x...); } protected: std::function f_; }; /// Optimized lambda condition for stateless lambdas (no capture). /// Uses function pointer instead of std::function to reduce memory overhead. /// Memory: 4 bytes (function pointer on 32-bit) vs 32 bytes (std::function). template class StatelessLambdaCondition : public Condition { public: explicit StatelessLambdaCondition(bool (*f)(Ts...)) : f_(f) {} bool check(const Ts &...x) override { return this->f_(x...); } protected: bool (*f_)(Ts...); }; template class ForCondition : public Condition, public Component { public: explicit ForCondition(Condition<> *condition) : condition_(condition) {} TEMPLATABLE_VALUE(uint32_t, time); void loop() override { // Safe to use cached time - only called from Application::loop() this->check_internal_(App.get_loop_component_start_time()); } float get_setup_priority() const override { return setup_priority::DATA; } bool check(const Ts &...x) override { auto now = millis(); if (!this->check_internal_(now)) return false; return now - this->last_inactive_ >= this->time_.value(x...); } protected: bool check_internal_(uint32_t now) { bool cond = this->condition_->check(); if (!cond) this->last_inactive_ = now; return cond; } Condition<> *condition_; uint32_t last_inactive_{0}; }; class StartupTrigger : public Trigger<>, public Component { public: explicit StartupTrigger(float setup_priority) : setup_priority_(setup_priority) {} void setup() override { this->trigger(); } float get_setup_priority() const override { return this->setup_priority_; } protected: float setup_priority_; }; class ShutdownTrigger : public Trigger<>, public Component { public: explicit ShutdownTrigger(float setup_priority) : setup_priority_(setup_priority) {} void on_shutdown() override { this->trigger(); } float get_setup_priority() const override { return this->setup_priority_; } protected: float setup_priority_; }; class LoopTrigger : public Trigger<>, public Component { public: void loop() override { this->trigger(); } float get_setup_priority() const override { return setup_priority::DATA; } }; #ifdef ESPHOME_PROJECT_NAME class ProjectUpdateTrigger : public Trigger, public Component { public: void setup() override { uint32_t hash = fnv1_hash(ESPHOME_PROJECT_NAME); ESPPreferenceObject pref = global_preferences->make_preference(hash, true); char previous_version[30]; char current_version[30] = ESPHOME_PROJECT_VERSION_30; if (pref.load(&previous_version)) { int cmp = strcmp(previous_version, current_version); if (cmp < 0) { this->trigger(previous_version); } } pref.save(¤t_version); global_preferences->sync(); } float get_setup_priority() const override { return setup_priority::PROCESSOR; } }; #endif template class DelayAction : public Action, public Component { public: explicit DelayAction() = default; TEMPLATABLE_VALUE(uint32_t, delay) void play_complex(const Ts &...x) override { this->num_running_++; // If num_running_ > 1, we have multiple instances running in parallel // In single/restart/queued modes, only one instance runs at a time // Parallel mode uses skip_cancel=true to allow multiple delays to coexist // WARNING: This can accumulate delays if scripts are triggered faster than they complete! // Users should set max_runs on parallel scripts to limit concurrent executions. // Issue #10264: This is a workaround for parallel script delays interfering with each other. // Optimization: For no-argument delays (most common case), use direct lambda // to avoid overhead from capturing arguments by value if constexpr (sizeof...(Ts) == 0) { App.scheduler.set_timer_common_( this, Scheduler::SchedulerItem::TIMEOUT, Scheduler::NameType::NUMERIC_ID_INTERNAL, nullptr, static_cast(InternalSchedulerID::DELAY_ACTION), this->delay_.value(), [this]() { this->play_next_(); }, /* is_retry= */ false, /* skip_cancel= */ this->num_running_ > 1); } else { // For delays with arguments, capture by value to preserve argument values // Arguments must be copied because original references may be invalid after delay auto f = [this, x...]() { this->play_next_(x...); }; App.scheduler.set_timer_common_(this, Scheduler::SchedulerItem::TIMEOUT, Scheduler::NameType::NUMERIC_ID_INTERNAL, nullptr, static_cast(InternalSchedulerID::DELAY_ACTION), this->delay_.value(x...), std::move(f), /* is_retry= */ false, /* skip_cancel= */ this->num_running_ > 1); } } float get_setup_priority() const override { return setup_priority::HARDWARE; } void play(const Ts &...x) override { /* ignore - see play_complex */ } void stop() override { this->cancel_timeout(InternalSchedulerID::DELAY_ACTION); } }; template class LambdaAction : public Action { public: explicit LambdaAction(std::function &&f) : f_(std::move(f)) {} void play(const Ts &...x) override { this->f_(x...); } protected: std::function f_; }; /// Optimized lambda action for stateless lambdas (no capture). /// Uses function pointer instead of std::function to reduce memory overhead. /// Memory: 4 bytes (function pointer on 32-bit) vs 32 bytes (std::function). template class StatelessLambdaAction : public Action { public: explicit StatelessLambdaAction(void (*f)(Ts...)) : f_(f) {} void play(const Ts &...x) override { this->f_(x...); } protected: void (*f_)(Ts...); }; /// Simple continuation action that calls play_next_ on a parent action. /// Used internally by IfAction, WhileAction, RepeatAction, etc. to chain actions. /// Memory: 4-8 bytes (parent pointer) vs 40 bytes (LambdaAction with std::function). template class ContinuationAction : public Action { public: explicit ContinuationAction(Action *parent) : parent_(parent) {} void play(const Ts &...x) override { this->parent_->play_next_(x...); } protected: Action *parent_; }; // Forward declaration for WhileLoopContinuation template class WhileAction; /// Loop continuation for WhileAction that checks condition and repeats or continues. /// Memory: 4-8 bytes (parent pointer) vs 40 bytes (LambdaAction with std::function). template class WhileLoopContinuation : public Action { public: explicit WhileLoopContinuation(WhileAction *parent) : parent_(parent) {} void play(const Ts &...x) override; protected: WhileAction *parent_; }; template class IfAction : public Action { public: explicit IfAction(Condition *condition) : condition_(condition) {} void add_then(const std::initializer_list *> &actions) { this->then_.add_actions(actions); this->then_.add_action(new ContinuationAction(this)); } void add_else(const std::initializer_list *> &actions) requires(HasElse) { this->else_.add_actions(actions); this->else_.add_action(new ContinuationAction(this)); } void play_complex(const Ts &...x) override { this->num_running_++; if (this->condition_->check(x...)) { if (!this->then_.empty() && this->num_running_ > 0) { this->then_.play(x...); return; } } else if constexpr (HasElse) { if (!this->else_.empty() && this->num_running_ > 0) { this->else_.play(x...); return; } } this->play_next_(x...); } void play(const Ts &...x) override { /* ignore - see play_complex */ } void stop() override { this->then_.stop(); if constexpr (HasElse) { this->else_.stop(); } } protected: Condition *condition_; ActionList then_; struct NoElse {}; [[no_unique_address]] std::conditional_t, NoElse> else_; }; template class WhileAction : public Action { public: WhileAction(Condition *condition) : condition_(condition) {} void add_then(const std::initializer_list *> &actions) { this->then_.add_actions(actions); this->then_.add_action(new WhileLoopContinuation(this)); } friend class WhileLoopContinuation; void play_complex(const Ts &...x) override { this->num_running_++; // Initial condition check if (!this->condition_->check(x...)) { // If new condition check failed, stop loop if running this->then_.stop(); this->play_next_(x...); return; } if (this->num_running_ > 0) { this->then_.play(x...); } } void play(const Ts &...x) override { /* ignore - see play_complex */ } void stop() override { this->then_.stop(); } protected: Condition *condition_; ActionList then_; }; // Implementation of WhileLoopContinuation::play template void WhileLoopContinuation::play(const Ts &...x) { if (this->parent_->num_running_ > 0 && this->parent_->condition_->check(x...)) { // play again this->parent_->then_.play(x...); } else { // condition false, play next this->parent_->play_next_(x...); } } // Forward declaration for RepeatLoopContinuation template class RepeatAction; /// Loop continuation for RepeatAction that increments iteration and repeats or continues. /// Memory: 4-8 bytes (parent pointer) vs 40 bytes (LambdaAction with std::function). template class RepeatLoopContinuation : public Action { public: explicit RepeatLoopContinuation(RepeatAction *parent) : parent_(parent) {} void play(const uint32_t &iteration, const Ts &...x) override; protected: RepeatAction *parent_; }; template class RepeatAction : public Action { public: TEMPLATABLE_VALUE(uint32_t, count) void add_then(const std::initializer_list *> &actions) { this->then_.add_actions(actions); this->then_.add_action(new RepeatLoopContinuation(this)); } friend class RepeatLoopContinuation; void play_complex(const Ts &...x) override { this->num_running_++; if (this->count_.value(x...) > 0) { this->then_.play(0, x...); } else { this->play_next_(x...); } } void play(const Ts &...x) override { /* ignore - see play_complex */ } void stop() override { this->then_.stop(); } protected: ActionList then_; }; // Implementation of RepeatLoopContinuation::play template void RepeatLoopContinuation::play(const uint32_t &iteration, const Ts &...x) { uint32_t next_iteration = iteration + 1; if (next_iteration >= this->parent_->count_.value(x...)) { this->parent_->play_next_(x...); } else { this->parent_->then_.play(next_iteration, x...); } } /** 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 // IMPORTANT: Only disable if num_running_ is 0, otherwise play_complex() was already // called before our setup() (e.g., from on_boot trigger at same priority level) // and we must not undo its enable_loop() call if (this->num_running_ == 0) { this->disable_loop(); } } void play_complex(const Ts &...x) override { this->num_running_++; // Check if we can continue immediately. if (this->condition_->check(x...)) { if (this->num_running_ > 0) { this->play_next_(x...); } return; } // Store for later processing auto now = millis(); auto timeout = this->timeout_value_.optional_value(x...); this->var_queue_.emplace_back(now, timeout, std::make_tuple(x...)); // Do immediate check with fresh timestamp - don't call loop() synchronously! // Let the event loop call it to avoid reentrancy issues if (this->process_queue_(now)) { // Only enable loop if we still have pending items this->enable_loop(); } } void loop() override { // Safe to use cached time - only called from Application::loop() if (this->num_running_ > 0 && !this->process_queue_(App.get_loop_component_start_time())) { // If queue is now empty, disable loop until next play_complex this->disable_loop(); } } void stop() override { this->var_queue_.clear(); this->disable_loop(); } float get_setup_priority() const override { return setup_priority::DATA; } void play(const Ts &...x) override { /* ignore - see play_complex */ } protected: // Helper: Process queue, triggering completed items and removing them // Returns true if queue still has pending items bool process_queue_(uint32_t now) { // Process each queued wait_until and remove completed ones this->var_queue_.remove_if([&](auto &queued) { auto start = std::get(queued); auto timeout = std::get>(queued); auto &var = std::get>(queued); // Check if timeout has expired auto expired = timeout && (now - start) >= *timeout; // Keep waiting if not expired and condition not met if (!expired && !this->condition_->check_tuple(var)) { return false; } // Condition met or timed out - trigger next action this->play_next_tuple_(var); return true; }); return !this->var_queue_.empty(); } Condition *condition_; std::list, std::tuple>> var_queue_{}; }; template class UpdateComponentAction : public Action { public: UpdateComponentAction(PollingComponent *component) : component_(component) {} void play(const Ts &...x) override { if (!this->component_->is_ready()) return; this->component_->update(); } protected: PollingComponent *component_; }; template class SuspendComponentAction : public Action { public: SuspendComponentAction(PollingComponent *component) : component_(component) {} void play(const Ts &...x) override { if (!this->component_->is_ready()) return; this->component_->stop_poller(); } protected: PollingComponent *component_; }; template class ResumeComponentAction : public Action { public: ResumeComponentAction(PollingComponent *component) : component_(component) {} TEMPLATABLE_VALUE(uint32_t, update_interval) void play(const Ts &...x) override { if (!this->component_->is_ready()) { return; } optional update_interval = this->update_interval_.optional_value(x...); if (update_interval.has_value()) { this->component_->set_update_interval(update_interval.value()); } this->component_->start_poller(); } protected: PollingComponent *component_; }; } // namespace esphome