ESPHome 2026.8.1
Loading...
Searching...
No Matches
script.h
Go to the documentation of this file.
1#pragma once
2
3#include <list>
4#include <memory>
5#include <tuple>
10#include "esphome/core/log.h"
11
12namespace esphome::script {
13
15 protected:
16#ifdef USE_STORE_LOG_STR_IN_FLASH
17 void esp_logw_(int line, const __FlashStringHelper *format, const char *param) {
18 esp_log_(ESPHOME_LOG_LEVEL_WARN, line, format, param);
19 }
20 void esp_logd_(int line, const __FlashStringHelper *format, const char *param) {
21 esp_log_(ESPHOME_LOG_LEVEL_DEBUG, line, format, param);
22 }
23 void esp_log_(int level, int line, const __FlashStringHelper *format, const char *param);
24#else
25 void esp_logw_(int line, const char *format, const char *param) {
26 esp_log_(ESPHOME_LOG_LEVEL_WARN, line, format, param);
27 }
28 void esp_logd_(int line, const char *format, const char *param) {
29 esp_log_(ESPHOME_LOG_LEVEL_DEBUG, line, format, param);
30 }
31 void esp_log_(int level, int line, const char *format, const char *param);
32#endif
33};
34
36template<typename... Ts> class Script : public ScriptLogger, public Trigger<Ts...> {
37 public:
42 virtual void execute(Ts...) = 0;
44 virtual bool is_running() { return this->is_action_running(); }
46 virtual void stop() { this->stop_action(); }
47
48 // execute this script using a tuple that contains the arguments
49 void execute_tuple(const std::tuple<Ts...> &tuple) {
50 this->execute_tuple_(tuple, std::make_index_sequence<sizeof...(Ts)>{});
51 }
52
53 // Internal function to give scripts readable names.
54 void set_name(const LogString *name) { name_ = name; }
55
56 protected:
57 template<size_t... S> void execute_tuple_(const std::tuple<Ts...> &tuple, std::index_sequence<S...> /*unused*/) {
58 this->execute(std::get<S>(tuple)...);
59 }
60
61 // Run the action chain with this script's name published as the current source (RAII save/restore,
62 // so nesting composes), so deferred work inside the script is attributed to it in blocking
63 // warnings. Force-inlined to fold into the always-inlined trigger chain (no extra stack frame).
64 inline void run_actions_(const Ts &...x) ESPHOME_ALWAYS_INLINE {
65 ScopedSourceGuard source_guard{this->name_};
66 this->trigger(x...);
67 }
68
69 const LogString *name_{nullptr};
70};
71
77template<typename... Ts> class SingleScript : public Script<Ts...> {
78 public:
79 void execute(Ts... x) override {
80 if (this->is_action_running()) {
81 this->esp_logw_(__LINE__, ESPHOME_LOG_FORMAT("Script '%s' is already running! (mode: single)"),
82 LOG_STR_ARG(this->name_));
83 return;
84 }
85
86 this->run_actions_(x...);
87 }
88};
89
95template<typename... Ts> class RestartScript : public Script<Ts...> {
96 public:
97 void execute(Ts... x) override {
98 if (this->is_action_running()) {
99 this->esp_logd_(__LINE__, ESPHOME_LOG_FORMAT("Script '%s' restarting (mode: restart)"), LOG_STR_ARG(this->name_));
100 this->stop_action();
101 }
102
103 this->run_actions_(x...);
104 }
105};
106
121template<typename... Ts> class QueueingScript : public Script<Ts...>, public Component {
122 public:
123 void execute(Ts... x) override {
124 if (this->is_action_running() || this->num_queued_ > 0) {
125 // num_queued_ is the number of *queued* instances (waiting, not including currently running)
126 // max_runs_ is the maximum *total* instances (running + queued)
127 // So we reject when num_queued_ + 1 >= max_runs_ (queued + running >= max)
128 if (this->num_queued_ + 1 >= this->max_runs_) {
129 this->esp_logw_(__LINE__, ESPHOME_LOG_FORMAT("Script '%s' max instances (running + queued) reached!"),
130 LOG_STR_ARG(this->name_));
131 return;
132 }
133
134 // Initialize queue on first queued item (after capacity check)
135 this->lazy_init_queue_();
136
137 this->esp_logd_(__LINE__, ESPHOME_LOG_FORMAT("Script '%s' queueing new instance (mode: queued)"),
138 LOG_STR_ARG(this->name_));
139 // Ring buffer: write to (queue_front_ + num_queued_) % queue_capacity
140 const size_t queue_capacity = static_cast<size_t>(this->max_runs_ - 1);
141 size_t write_pos = (this->queue_front_ + this->num_queued_) % queue_capacity;
142 // Use std::make_unique to replace the unique_ptr
143 this->var_queue_[write_pos] = std::make_unique<std::tuple<Ts...>>(x...);
144 this->num_queued_++;
145 // Enable loop now that there is something to dequeue - don't call loop()
146 // synchronously! Let the event loop call it to avoid reentrancy issues
147 this->enable_loop();
148 return;
149 }
150
151 this->run_actions_(x...);
152 // Check if the trigger was immediate and we can continue right away.
153 this->loop();
154 }
155
156 void stop() override {
157 // Clear all queued items to free memory immediately
158 // Resetting the array automatically destroys all unique_ptrs and their contents
159 this->var_queue_.reset();
160 this->num_queued_ = 0;
161 this->queue_front_ = 0;
163 }
164
165 void loop() override {
166 if (this->num_queued_ != 0 && !this->is_action_running()) {
167 // Dequeue: decrement count, move tuple out (frees slot), advance read position
168 this->num_queued_--;
169 const size_t queue_capacity = static_cast<size_t>(this->max_runs_ - 1);
170 auto tuple_ptr = std::move(this->var_queue_[this->queue_front_]);
171 this->queue_front_ = (this->queue_front_ + 1) % queue_capacity;
172 this->trigger_tuple_(*tuple_ptr, std::make_index_sequence<sizeof...(Ts)>{});
173 }
174 if (this->num_queued_ == 0 && !this->is_idle()) {
175 // Queue is now empty - disable loop until the next execute() queues an
176 // instance. The inline is_idle() check skips the out-of-line call when
177 // the loop is already disabled (execute() calls loop() synchronously).
178 // This can run before this component's setup() (execute() from on_boot),
179 // which leaves the state machine in LOOP_DONE and skips call_setup();
180 // this class therefore must not rely on a setup() override.
181 this->disable_loop();
182 }
183 }
184
185 void set_max_runs(int max_runs) { max_runs_ = max_runs; }
186
187 protected:
188 // Lazy init queue on first use - avoids setup() ordering issues and saves memory
189 // if script is never executed during this boot cycle
190 inline void lazy_init_queue_() {
191 if (!this->var_queue_) {
192 // Allocate array of max_runs_ - 1 slots for queued items (running item is separate)
193 // unique_ptr array is zero-initialized, so all slots start as nullptr
194 this->var_queue_ = std::make_unique<std::unique_ptr<std::tuple<Ts...>>[]>(this->max_runs_ - 1);
195 }
196 }
197
198 template<size_t... S> void trigger_tuple_(const std::tuple<Ts...> &tuple, std::index_sequence<S...> /*unused*/) {
199 this->run_actions_(std::get<S>(tuple)...);
200 }
201
202 int num_queued_ = 0; // Number of queued instances (not including currently running)
203 int max_runs_ = 0; // Maximum total instances (running + queued)
204 size_t queue_front_ = 0; // Ring buffer read position (next item to execute)
205 std::unique_ptr<std::unique_ptr<std::tuple<Ts...>>[]> var_queue_; // Ring buffer of queued parameters
206};
207
213template<typename... Ts> class ParallelScript : public Script<Ts...> {
214 public:
215 void execute(Ts... x) override {
216 if (this->max_runs_ != 0 && this->automation_parent_->num_running() >= this->max_runs_) {
217 this->esp_logw_(__LINE__, ESPHOME_LOG_FORMAT("Script '%s' maximum number of parallel runs exceeded!"),
218 LOG_STR_ARG(this->name_));
219 return;
220 }
221 this->run_actions_(x...);
222 }
223 void set_max_runs(int max_runs) { max_runs_ = max_runs; }
224
225 protected:
226 int max_runs_ = 0;
227};
228
229template<class S, typename... Ts> class ScriptExecuteAction;
230
231template<class... As, typename... Ts> class ScriptExecuteAction<Script<As...>, Ts...> final : public Action<Ts...> {
232 public:
233 ScriptExecuteAction(Script<As...> *script) : script_(script) {}
234
235 using Args = std::tuple<TemplatableFn<As, Ts...>...>;
236
237 template<typename... F> void set_args(F... x) { args_ = Args{x...}; }
238
239 void play(const Ts &...x) override { this->script_->execute_tuple(this->eval_args_(x...)); }
240
241 protected:
242 // NOTE:
243 // `eval_args_impl` functions evaluates `I`th the functions in `args` member.
244 // and then recursively calls `eval_args_impl` for the `I+1`th arg.
245 // if `I` = `N` all args have been stored, and nothing is done.
246
247 template<std::size_t N>
248 void eval_args_impl_(std::tuple<As...> & /*unused*/, std::integral_constant<std::size_t, N> /*unused*/,
249 std::integral_constant<std::size_t, N> /*unused*/, Ts... /*unused*/) {}
250
251 template<std::size_t I, std::size_t N>
252 void eval_args_impl_(std::tuple<As...> &evaled_args, std::integral_constant<std::size_t, I> /*unused*/,
253 std::integral_constant<std::size_t, N> n, Ts... x) {
254 std::get<I>(evaled_args) = std::get<I>(args_).value(x...); // NOTE: evaluate `i`th arg, and store in tuple.
255 eval_args_impl_(evaled_args, std::integral_constant<std::size_t, I + 1>{}, n,
256 x...); // NOTE: recurse to next index.
257 }
258
259 std::tuple<As...> eval_args_(Ts... x) {
260 std::tuple<As...> evaled_args;
261 eval_args_impl_(evaled_args, std::integral_constant<std::size_t, 0>{}, std::tuple_size<Args>{}, x...);
262 return evaled_args;
263 }
264
267};
268
269template<class C, typename... Ts> class ScriptStopAction final : public Action<Ts...> {
270 public:
271 ScriptStopAction(C *script) : script_(script) {}
272
273 void play(const Ts &...x) override { this->script_->stop(); }
274
275 protected:
277};
278
279template<class C, typename... Ts> class IsRunningCondition final : public Condition<Ts...> {
280 public:
281 explicit IsRunningCondition(C *parent) : parent_(parent) {}
282
283 bool check(const Ts &...x) override { return this->parent_->is_running(); }
284
285 protected:
287};
288
296template<class C, typename... Ts> class ScriptWaitAction final : public Action<Ts...>, public Component {
297 public:
298 ScriptWaitAction(C *script) : script_(script) {}
299
300 void setup() override {
301 // Start with loop disabled - only enable when there's work to do
302 // IMPORTANT: Only disable if num_running_ is 0, otherwise play_complex() was already
303 // called before our setup() (e.g., from on_boot trigger at same priority level)
304 // and we must not undo its enable_loop() call
305 if (this->num_running_ == 0) {
306 this->disable_loop();
307 }
308 }
309
310 void play_complex(const Ts &...x) override {
311 this->num_running_++;
312 // Check if we can continue immediately.
313 if (!this->script_->is_running()) {
314 this->play_next_(x...);
315 return;
316 }
317
318 // Store parameters for later execution
319 this->param_queue_.emplace_back(x...);
320 // Enable loop now that we have work to do - don't call loop() synchronously!
321 // Let the event loop call it to avoid reentrancy issues
322 this->enable_loop();
323 }
324
325 void loop() override {
326 if (this->num_running_ == 0)
327 return;
328
329 if (this->script_->is_running())
330 return;
331
332 // Only process ONE queued item per loop iteration
333 // Processing all items in a while loop causes infinite loops because
334 // play_next_() can trigger more items to be queued
335 if (!this->param_queue_.empty()) {
336 auto &params = this->param_queue_.front();
337 this->play_next_tuple_(params, std::make_index_sequence<sizeof...(Ts)>{});
338 this->param_queue_.pop_front();
339 } else {
340 // Queue is now empty - disable loop until next play_complex
341 this->disable_loop();
342 }
343 }
344
345 void play(const Ts &...x) override { /* ignore - see play_complex */
346 }
347
348 void stop() override {
349 this->param_queue_.clear();
350 this->disable_loop();
351 }
352
353 protected:
354 template<size_t... S> void play_next_tuple_(const std::tuple<Ts...> &tuple, std::index_sequence<S...> /*unused*/) {
355 this->play_next_(std::get<S>(tuple)...);
356 }
357
359 std::list<std::tuple<Ts...>> param_queue_;
360};
361
362} // namespace esphome::script
void play_next_(const Ts &...x)
Definition automation.h:518
int num_running()
Return the number of actions in the action part of this automation that are currently running.
Definition automation.h:626
void enable_loop()
Enable this component's loop.
Definition component.h:246
void disable_loop()
Disable this component's loop.
bool is_idle() const
Check if this component is idle.
Definition component.h:213
Base class for all automation conditions.
Definition automation.h:438
RAII guard that publishes a current source (e.g.
Function-pointer-only templatable storage (4 bytes on 32-bit).
Definition automation.h:19
Automation< Ts... > * automation_parent_
Definition automation.h:482
void trigger(const Ts &...x) ESPHOME_ALWAYS_INLINE
Definition automation.h:461
bool check(const Ts &...x) override
Definition script.h:283
A script type that executes new instances in parallel.
Definition script.h:213
void execute(Ts... x) override
Definition script.h:215
void set_max_runs(int max_runs)
Definition script.h:223
A script type that queues new instances that are created.
Definition script.h:121
void trigger_tuple_(const std::tuple< Ts... > &tuple, std::index_sequence< S... >)
Definition script.h:198
void execute(Ts... x) override
Definition script.h:123
void set_max_runs(int max_runs)
Definition script.h:185
std::unique_ptr< std::unique_ptr< std::tuple< Ts... > >[]> var_queue_
Definition script.h:205
A script type that restarts scripts from the beginning when a new instance is started.
Definition script.h:95
void execute(Ts... x) override
Definition script.h:97
void eval_args_impl_(std::tuple< As... > &evaled_args, std::integral_constant< std::size_t, I >, std::integral_constant< std::size_t, N > n, Ts... x)
Definition script.h:252
void eval_args_impl_(std::tuple< As... > &, std::integral_constant< std::size_t, N >, std::integral_constant< std::size_t, N >, Ts...)
Definition script.h:248
std::tuple< TemplatableFn< As, Ts... >... > Args
Definition script.h:235
The abstract base class for all script types.
Definition script.h:36
virtual bool is_running()
Check if any instance of this script is currently running.
Definition script.h:44
virtual void execute(Ts...)=0
Execute a new instance of this script.
void execute_tuple(const std::tuple< Ts... > &tuple)
Definition script.h:49
virtual void stop()
Stop all instances of this script.
Definition script.h:46
void run_actions_(const Ts &...x) ESPHOME_ALWAYS_INLINE
Definition script.h:64
void set_name(const LogString *name)
Definition script.h:54
const LogString * name_
Definition script.h:69
void execute_tuple_(const std::tuple< Ts... > &tuple, std::index_sequence< S... >)
Definition script.h:57
void esp_logd_(int line, const char *format, const char *param)
Definition script.h:28
void esp_logd_(int line, const __FlashStringHelper *format, const char *param)
Definition script.h:20
void esp_logw_(int line, const char *format, const char *param)
Definition script.h:25
void esp_logw_(int line, const __FlashStringHelper *format, const char *param)
Definition script.h:17
void esp_log_(int level, int line, const __FlashStringHelper *format, const char *param)
Definition script.cpp:9
void play(const Ts &...x) override
Definition script.h:273
Wait for a script to finish before continuing.
Definition script.h:296
void play(const Ts &...x) override
Definition script.h:345
std::list< std::tuple< Ts... > > param_queue_
Definition script.h:359
void play_next_tuple_(const std::tuple< Ts... > &tuple, std::index_sequence< S... >)
Definition script.h:354
void play_complex(const Ts &...x) override
Definition script.h:310
A script type for which only a single instance at a time is allowed.
Definition script.h:77
void execute(Ts... x) override
Definition script.h:79
const char int line
Definition log.h:74
const char int const __FlashStringHelper * format
Definition log.h:74
uint16_t x
Definition tt21100.cpp:5