ESPHome 2026.11.0-dev
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"
12
13namespace esphome::script {
14
16 protected:
17 void esp_logw_(int line, ProgmemStr format, const char *param) {
18 esp_log_(ESPHOME_LOG_LEVEL_WARN, line, format, param);
19 }
20 void esp_logd_(int line, ProgmemStr format, const char *param) {
21 esp_log_(ESPHOME_LOG_LEVEL_DEBUG, line, format, param);
22 }
23 void esp_log_(int level, int line, ProgmemStr format, const char *param);
24};
25
27template<typename... Ts> class Script : public ScriptLogger, public Trigger<Ts...> {
28 public:
33 virtual void execute(Ts...) = 0;
35 virtual bool is_running() { return this->is_action_running(); }
36#ifdef USE_SCRIPT_ON_STOP
38 virtual void stop() {
39 if (this->is_action_running())
40 this->stop_running_();
41 }
42
44#else
46 virtual void stop() { this->stop_action(); }
47#endif
48
49 // execute this script using a tuple that contains the arguments
50 void execute_tuple(const std::tuple<Ts...> &tuple) {
51 this->execute_tuple_(tuple, std::make_index_sequence<sizeof...(Ts)>{});
52 }
53
54 // Internal function to give scripts readable names.
55 void set_name(const LogString *name) { name_ = name; }
56
57 protected:
58 template<size_t... S> void execute_tuple_(const std::tuple<Ts...> &tuple, std::index_sequence<S...> /*unused*/) {
59 this->execute(std::get<S>(tuple)...);
60 }
61
62 // Run the action chain with this script's name published as the current source (RAII save/restore,
63 // so nesting composes), so deferred work inside the script is attributed to it in blocking
64 // warnings. Force-inlined to fold into the always-inlined trigger chain (no extra stack frame).
65 inline void run_actions_(const Ts &...x) ESPHOME_ALWAYS_INLINE {
66 ScopedSourceGuard source_guard{this->name_};
67 this->trigger(x...);
68 }
69
70 // Stop the running actions early and fire the on_stop trigger.
72 this->stop_action();
73#ifdef USE_SCRIPT_ON_STOP
74 this->stop_trigger_.trigger();
75#endif
76 }
77
78 const LogString *name_{nullptr};
79#ifdef USE_SCRIPT_ON_STOP
81#endif
82};
83
89template<typename... Ts> class SingleScript : public Script<Ts...> {
90 public:
91 void execute(Ts... x) override {
92 if (this->is_action_running()) {
93 this->esp_logw_(__LINE__, ESPHOME_LOG_FORMAT("Script '%s' is already running! (mode: single)"),
94 LOG_STR_ARG(this->name_));
95 return;
96 }
97
98 this->run_actions_(x...);
99 }
100};
101
107template<typename... Ts> class RestartScript : public Script<Ts...> {
108 public:
109 void execute(Ts... x) override {
110 if (this->is_action_running()) {
111 this->esp_logd_(__LINE__, ESPHOME_LOG_FORMAT("Script '%s' restarting (mode: restart)"), LOG_STR_ARG(this->name_));
112 this->stop_running_();
113#ifdef USE_SCRIPT_ON_STOP
114 // on_stop may have executed this script again; only the newest run may continue
115 if (this->is_action_running())
116 this->stop_action();
117#endif
118 }
119
120 this->run_actions_(x...);
121 }
122};
123
138template<typename... Ts> class QueueingScript : public Script<Ts...>, public Component {
139 public:
140 void execute(Ts... x) override {
141 if (this->is_action_running() || this->num_queued_ > 0) {
142 // num_queued_ is the number of *queued* instances (waiting, not including currently running)
143 // max_runs_ is the maximum *total* instances (running + queued)
144 // So we reject when num_queued_ + 1 >= max_runs_ (queued + running >= max)
145 if (this->num_queued_ + 1 >= this->max_runs_) {
146 this->esp_logw_(__LINE__, ESPHOME_LOG_FORMAT("Script '%s' max instances (running + queued) reached!"),
147 LOG_STR_ARG(this->name_));
148 return;
149 }
150
151 // Initialize queue on first queued item (after capacity check)
152 this->lazy_init_queue_();
153
154 this->esp_logd_(__LINE__, ESPHOME_LOG_FORMAT("Script '%s' queueing new instance (mode: queued)"),
155 LOG_STR_ARG(this->name_));
156 // Ring buffer: write to (queue_front_ + num_queued_) % queue_capacity
157 const size_t queue_capacity = static_cast<size_t>(this->max_runs_ - 1);
158 size_t write_pos = (this->queue_front_ + this->num_queued_) % queue_capacity;
159 // Use std::make_unique to replace the unique_ptr
160 this->var_queue_[write_pos] = std::make_unique<std::tuple<Ts...>>(x...);
161 this->num_queued_++;
162 // Enable loop now that there is something to dequeue - don't call loop()
163 // synchronously! Let the event loop call it to avoid reentrancy issues
164 this->enable_loop();
165 return;
166 }
167
168 this->run_actions_(x...);
169 // Check if the trigger was immediate and we can continue right away.
170 this->loop();
171 }
172
173 void stop() override {
174#ifdef USE_SCRIPT_ON_STOP
175 // Queued runs are unfinished work too, even between two runs when nothing is running
176 const bool stopping = this->is_action_running() || this->num_queued_ != 0;
177#endif
178 // Clear all queued items to free memory immediately
179 // Resetting the array automatically destroys all unique_ptrs and their contents
180 this->var_queue_.reset();
181 this->num_queued_ = 0;
182 this->queue_front_ = 0;
183#ifdef USE_SCRIPT_ON_STOP
184 if (stopping)
185 this->stop_running_();
186#else
188#endif
189 }
190
191 void loop() override {
192 if (this->num_queued_ != 0 && !this->is_action_running()) {
193 // Dequeue: decrement count, move tuple out (frees slot), advance read position
194 this->num_queued_--;
195 const size_t queue_capacity = static_cast<size_t>(this->max_runs_ - 1);
196 auto tuple_ptr = std::move(this->var_queue_[this->queue_front_]);
197 this->queue_front_ = (this->queue_front_ + 1) % queue_capacity;
198 this->trigger_tuple_(*tuple_ptr, std::make_index_sequence<sizeof...(Ts)>{});
199 }
200 if (this->num_queued_ == 0 && !this->is_idle()) {
201 // Queue is now empty - disable loop until the next execute() queues an
202 // instance. The inline is_idle() check skips the out-of-line call when
203 // the loop is already disabled (execute() calls loop() synchronously).
204 // This can run before this component's setup() (execute() from on_boot),
205 // which leaves the state machine in LOOP_DONE and skips call_setup();
206 // this class therefore must not rely on a setup() override.
207 this->disable_loop();
208 }
209 }
210
211 void set_max_runs(int max_runs) { max_runs_ = max_runs; }
212
213 protected:
214 // Lazy init queue on first use - avoids setup() ordering issues and saves memory
215 // if script is never executed during this boot cycle
216 inline void lazy_init_queue_() {
217 if (!this->var_queue_) {
218 // Allocate array of max_runs_ - 1 slots for queued items (running item is separate)
219 // unique_ptr array is zero-initialized, so all slots start as nullptr
220 this->var_queue_ = std::make_unique<std::unique_ptr<std::tuple<Ts...>>[]>(this->max_runs_ - 1);
221 }
222 }
223
224 template<size_t... S> void trigger_tuple_(const std::tuple<Ts...> &tuple, std::index_sequence<S...> /*unused*/) {
225 this->run_actions_(std::get<S>(tuple)...);
226 }
227
228 int num_queued_ = 0; // Number of queued instances (not including currently running)
229 int max_runs_ = 0; // Maximum total instances (running + queued)
230 size_t queue_front_ = 0; // Ring buffer read position (next item to execute)
231 std::unique_ptr<std::unique_ptr<std::tuple<Ts...>>[]> var_queue_; // Ring buffer of queued parameters
232};
233
239template<typename... Ts> class ParallelScript : public Script<Ts...> {
240 public:
241 void execute(Ts... x) override {
242 if (this->max_runs_ != 0 && this->automation_parent_->num_running() >= this->max_runs_) {
243 this->esp_logw_(__LINE__, ESPHOME_LOG_FORMAT("Script '%s' maximum number of parallel runs exceeded!"),
244 LOG_STR_ARG(this->name_));
245 return;
246 }
247 this->run_actions_(x...);
248 }
249 void set_max_runs(int max_runs) { max_runs_ = max_runs; }
250
251 protected:
252 int max_runs_ = 0;
253};
254
255template<class S, typename... Ts> class ScriptExecuteAction;
256
257template<class... As, typename... Ts> class ScriptExecuteAction<Script<As...>, Ts...> final : public Action<Ts...> {
258 public:
259 ScriptExecuteAction(Script<As...> *script) : script_(script) {}
260
261 using Args = std::tuple<TemplatableFn<As, Ts...>...>;
262
263 template<typename... F> void set_args(F... x) { args_ = Args{x...}; }
264
265 void play(const Ts &...x) override { this->script_->execute_tuple(this->eval_args_(x...)); }
266
267 protected:
268 // NOTE:
269 // `eval_args_impl` functions evaluates `I`th the functions in `args` member.
270 // and then recursively calls `eval_args_impl` for the `I+1`th arg.
271 // if `I` = `N` all args have been stored, and nothing is done.
272
273 template<std::size_t N>
274 void eval_args_impl_(std::tuple<As...> & /*unused*/, std::integral_constant<std::size_t, N> /*unused*/,
275 std::integral_constant<std::size_t, N> /*unused*/, Ts... /*unused*/) {}
276
277 template<std::size_t I, std::size_t N>
278 void eval_args_impl_(std::tuple<As...> &evaled_args, std::integral_constant<std::size_t, I> /*unused*/,
279 std::integral_constant<std::size_t, N> n, Ts... x) {
280 std::get<I>(evaled_args) = std::get<I>(args_).value(x...); // NOTE: evaluate `i`th arg, and store in tuple.
281 eval_args_impl_(evaled_args, std::integral_constant<std::size_t, I + 1>{}, n,
282 x...); // NOTE: recurse to next index.
283 }
284
285 std::tuple<As...> eval_args_(Ts... x) {
286 std::tuple<As...> evaled_args;
287 eval_args_impl_(evaled_args, std::integral_constant<std::size_t, 0>{}, std::tuple_size<Args>{}, x...);
288 return evaled_args;
289 }
290
292 // Empty tuple for scripts without parameters; takes no space then
293 [[no_unique_address]] Args args_;
294};
295
303template<class C, typename... Ts> class ScriptWaitAction final : public Action<Ts...>, public Component {
304 public:
305 ScriptWaitAction(C *script) : script_(script) {}
306
307 void setup() override {
308 // Start with loop disabled - only enable when there's work to do
309 // IMPORTANT: Only disable if num_running_ is 0, otherwise play_complex() was already
310 // called before our setup() (e.g., from on_boot trigger at same priority level)
311 // and we must not undo its enable_loop() call
312 if (this->num_running_ == 0) {
313 this->disable_loop();
314 }
315 }
316
317 void play_complex(const Ts &...x) override {
318 this->num_running_++;
319 // Check if we can continue immediately.
320 if (!this->script_->is_running()) {
321 this->play_next_(x...);
322 return;
323 }
324
325 // Store parameters for later execution
326 this->param_queue_.emplace_back(x...);
327 // Enable loop now that we have work to do - don't call loop() synchronously!
328 // Let the event loop call it to avoid reentrancy issues
329 this->enable_loop();
330 }
331
332 void loop() override {
333 if (this->num_running_ == 0) {
334 // Nothing left to wait for, e.g. once the last queued item has run
335 this->disable_loop();
336 return;
337 }
338
339 if (this->script_->is_running())
340 return;
341
342 // Only process ONE queued item per loop iteration
343 // Processing all items in a while loop causes infinite loops because
344 // play_next_() can trigger more items to be queued
345 if (!this->param_queue_.empty()) {
346 // Take the item out first: the chain may call stop(), which clears the queue
347 auto params = std::move(this->param_queue_.front());
348 this->param_queue_.pop_front();
349 std::apply([this](auto &...args) { this->play_next_(args...); }, params);
350 } else {
351 // Queue is now empty - disable loop until next play_complex
352 this->disable_loop();
353 }
354 }
355
356 void play(const Ts &...x) override { /* ignore - see play_complex */ }
357
358 void stop() override {
359 this->param_queue_.clear();
360 this->disable_loop();
361 }
362
363 protected:
365 std::list<std::tuple<stored_trigger_arg_t<Ts>...>> param_queue_;
366};
367
368} // namespace esphome::script
void play_next_(const Ts &...x)
Definition automation.h:613
int num_running()
Return the number of actions in the action part of this automation that are currently running.
Definition automation.h:750
void enable_loop()
Enable this component's loop.
Definition component.h:248
void disable_loop()
Disable this component's loop.
bool is_idle() const
Check if this component is idle.
Definition component.h:215
RAII guard that publishes a current source (e.g.
Function-pointer-only templatable storage (4 bytes on 32-bit).
Definition automation.h:20
Automation< Ts... > * automation_parent_
Definition automation.h:577
void trigger(const Ts &...x) ESPHOME_ALWAYS_INLINE
Definition automation.h:556
A script type that executes new instances in parallel.
Definition script.h:239
void execute(Ts... x) override
Definition script.h:241
void set_max_runs(int max_runs)
Definition script.h:249
A script type that queues new instances that are created.
Definition script.h:138
void trigger_tuple_(const std::tuple< Ts... > &tuple, std::index_sequence< S... >)
Definition script.h:224
void execute(Ts... x) override
Definition script.h:140
void set_max_runs(int max_runs)
Definition script.h:211
std::unique_ptr< std::unique_ptr< std::tuple< Ts... > >[]> var_queue_
Definition script.h:231
A script type that restarts scripts from the beginning when a new instance is started.
Definition script.h:107
void execute(Ts... x) override
Definition script.h:109
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:278
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:274
std::tuple< TemplatableFn< As, Ts... >... > Args
Definition script.h:261
The abstract base class for all script types.
Definition script.h:27
virtual bool is_running()
Check if any instance of this script is currently running.
Definition script.h:35
virtual void execute(Ts...)=0
Execute a new instance of this script.
void execute_tuple(const std::tuple< Ts... > &tuple)
Definition script.h:50
Trigger * get_stop_trigger()
Definition script.h:43
virtual void stop()
Stop all instances of this script. Fires the on_stop trigger once if any instance was running.
Definition script.h:38
void run_actions_(const Ts &...x) ESPHOME_ALWAYS_INLINE
Definition script.h:65
void set_name(const LogString *name)
Definition script.h:55
const LogString * name_
Definition script.h:78
void execute_tuple_(const std::tuple< Ts... > &tuple, std::index_sequence< S... >)
Definition script.h:58
void esp_logw_(int line, ProgmemStr format, const char *param)
Definition script.h:17
void esp_logd_(int line, ProgmemStr format, const char *param)
Definition script.h:20
void esp_log_(int level, int line, ProgmemStr format, const char *param)
Definition script.cpp:8
Wait for a script to finish before continuing.
Definition script.h:303
void play(const Ts &...x) override
Definition script.h:356
std::list< std::tuple< stored_trigger_arg_t< Ts >... > > param_queue_
Definition script.h:365
void play_complex(const Ts &...x) override
Definition script.h:317
A script type for which only a single instance at a time is allowed.
Definition script.h:89
void execute(Ts... x) override
Definition script.h:91
const char * format
const __FlashStringHelper * ProgmemStr
Definition progmem.h:31
uint16_t x
Definition tt21100.cpp:5