mirror of
https://github.com/esphome/esphome.git
synced 2026-11-15 01:25:57 +00:00
Compare commits
17
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
14f809e70d | ||
|
|
2b7c8e5523 | ||
|
|
8634800ea6 | ||
|
|
b8504cf93f | ||
|
|
5045d72732 | ||
|
|
65a3fff28d | ||
|
|
c98eb5b250 | ||
|
|
7464db1db8 | ||
|
|
b3053c90ab | ||
|
|
3d2b769f91 | ||
|
|
2709de3355 | ||
|
|
2b24bef810 | ||
|
|
b647b65e6e | ||
|
|
a6d561ce19 | ||
|
|
ee9dcc2958 | ||
|
|
19fc21db23 | ||
|
|
61528b9d4e |
No files matched your search
@@ -123,17 +123,10 @@ Both live in `esphome/core/helpers.h` and accept `std::function`, lambdas and fo
|
||||
their templatized `add()` method. `LazyCallbackManager` is a drop-in replacement for `CallbackManager` that defers allocation until the first
|
||||
callback is added. Prefer it for entity-level callbacks where most instances have no subscribers.
|
||||
|
||||
A filter that keeps state between calls (timing, edges) still needs no Trigger class: give each automation a
|
||||
small object with constant initializers, created from its `CONF_TRIGGER_ID` before the automations are built,
|
||||
and let `when` ask it, e.g. `ApplyCall("{}->check(state, {}, {})", ((CONF_TRIGGER_ID, ...), ...))`. An `ID`
|
||||
value in an `ApplyCall` is written as `::id`, so a callback parameter cannot hide it. `on_click` in
|
||||
`esphome/components/binary_sensor/__init__.py` is the in-tree example; once a second component needs this,
|
||||
move the object creation into `CallbackAutomation`.
|
||||
|
||||
## Triggers: Trigger class method
|
||||
|
||||
Use `build_automation()` with a `Trigger<Ts...>` subclass only when the state must be set up from the
|
||||
parent (in the constructor or in `setup()`), or the trigger needs its own timers.
|
||||
Use `build_automation()` with a `Trigger<Ts...>` subclass only when the forwarder needs **mutable state
|
||||
beyond a single `Automation*` pointer** (e.g. edge detection tracking previous state, timing logic).
|
||||
|
||||
**Python:**
|
||||
```python
|
||||
|
||||
+64
-21
@@ -36,6 +36,7 @@ from esphome.cpp_generator import (
|
||||
LambdaExpression,
|
||||
MockObj,
|
||||
MockObjClass,
|
||||
Statement,
|
||||
TemplateArgsType,
|
||||
call_lambda,
|
||||
)
|
||||
@@ -704,11 +705,16 @@ def apply_static_function(
|
||||
statements: list[str],
|
||||
share: bool = False,
|
||||
suffix: str = "fn",
|
||||
add_global_: Callable[[Statement], object] | None = None,
|
||||
) -> MockObj:
|
||||
"""Emit the static function for ``id_``; ``suffix`` names a second one, ``share`` reuses an identical one."""
|
||||
return cg.shared_static_function(
|
||||
_apply_function_name(id_, suffix), return_type, params, statements, share=share
|
||||
)
|
||||
"""Emit the static function for ``id_``; ``suffix`` names a second one, ``share`` reuses an identical one.
|
||||
|
||||
``add_global_`` places the declaration (e.g. at a ``LatePvariable``); such a function is never shared.
|
||||
"""
|
||||
name = _apply_function_name(id_, suffix)
|
||||
if add_global_ is not None:
|
||||
return cg.static_function(name, return_type, params, statements, add_global_)
|
||||
return cg.shared_static_function(name, return_type, params, statements, share=share)
|
||||
|
||||
|
||||
def _apply_function_name(id_: ID, suffix: str = "fn") -> str:
|
||||
@@ -1903,19 +1909,31 @@ def _emit_inline_run(
|
||||
"""
|
||||
if len(run) == 1 and run[0].emit is not None:
|
||||
return run[0].emit()
|
||||
statements = _run_statements(run)
|
||||
id_ = run[0].id_.copy()
|
||||
id_.type = ApplyRunAction
|
||||
fn = apply_static_function(
|
||||
id_,
|
||||
cg.void,
|
||||
[
|
||||
(cg.RawExpression("const int &"), _RUNNING_PARAM),
|
||||
*_apply_params(args),
|
||||
],
|
||||
statements,
|
||||
return cg.new_Pvariable(
|
||||
id_, cg.TemplateArguments(_run_function(id_, run, args), *template_arg)
|
||||
)
|
||||
|
||||
|
||||
def _run_function(
|
||||
id_: ID,
|
||||
run: list[_InlineAction],
|
||||
args: TemplateArgsType,
|
||||
stoppable: bool = True,
|
||||
add_global_: Callable[[Statement], object] | None = None,
|
||||
) -> MockObj:
|
||||
"""Emit the function that runs ``run``; a stoppable one takes the run count first and
|
||||
returns once a stop clears it. ``add_global_`` is passed to ``cg.static_function``."""
|
||||
params = _apply_params(args)
|
||||
statements = _run_statements(run)
|
||||
if stoppable:
|
||||
params = [(cg.RawExpression("const int &"), _RUNNING_PARAM), *params]
|
||||
else:
|
||||
statements = [line for line in statements if line != _STOP_CHECK]
|
||||
return apply_static_function(
|
||||
id_, cg.void, params, statements, add_global_=add_global_
|
||||
)
|
||||
return cg.new_Pvariable(id_, cg.TemplateArguments(fn, *template_arg))
|
||||
|
||||
|
||||
async def _build_action_plan(
|
||||
@@ -2098,10 +2116,10 @@ def _automation_from_plan(
|
||||
|
||||
|
||||
def _is_direct(plan: list[MockObj | list[_InlineAction]], config: ConfigType) -> bool:
|
||||
"""Whether a callback automation is all inline actions, so it needs no objects.
|
||||
"""Whether an automation is all inline actions without a manual id, so it needs no objects.
|
||||
|
||||
Without a Trigger or a manual id nothing can reach such an automation to stop it, so
|
||||
no stop checks are needed either. Only for callers that create no Trigger.
|
||||
Without a Trigger or a manual id nothing can reach such an automation to stop it, so a
|
||||
caller that creates no Trigger needs no stop checks either.
|
||||
"""
|
||||
return (
|
||||
not config[CONF_AUTOMATION_ID].is_manual
|
||||
@@ -2114,13 +2132,15 @@ def _direct_function(
|
||||
plan: list[MockObj | list[_InlineAction]],
|
||||
args: TemplateArgsType,
|
||||
config: ConfigType,
|
||||
id_: ID | None = None,
|
||||
stoppable: bool = False,
|
||||
add_global_: Callable[[Statement], object] | None = None,
|
||||
) -> MockObj | None:
|
||||
"""The function that runs ``plan`` if it is direct, named after ``id_`` (default: its
|
||||
first action); see ``_run_function``."""
|
||||
if not _is_direct(plan, config):
|
||||
return None
|
||||
statements = [line for line in _run_statements(plan[0]) if line != _STOP_CHECK]
|
||||
return apply_static_function(
|
||||
plan[0][0].id_, cg.void, _apply_params(args), statements
|
||||
)
|
||||
return _run_function(id_ or plan[0][0].id_, plan[0], args, stoppable, add_global_)
|
||||
|
||||
|
||||
def _when_call(
|
||||
@@ -2140,6 +2160,29 @@ async def build_automation(
|
||||
return await new_automation(args, config, trigger)
|
||||
|
||||
|
||||
async def build_automation_or_run_function(
|
||||
trigger: MockObj,
|
||||
args: TemplateArgsType,
|
||||
config: ConfigType,
|
||||
add_global_: Callable[[Statement], object] | None = None,
|
||||
) -> MockObj | None:
|
||||
"""Build ``config`` on ``trigger`` like ``build_automation`` and return None; if every
|
||||
action is inline, return the stoppable function ``esphome__<trigger id>__fn`` instead."""
|
||||
plan = await _build_automation_plan(args, config)
|
||||
if (
|
||||
fn := _direct_function(
|
||||
plan,
|
||||
args,
|
||||
config,
|
||||
id_=trigger.base,
|
||||
stoppable=True,
|
||||
add_global_=add_global_,
|
||||
)
|
||||
) is None:
|
||||
_automation_from_plan(plan, args, config, trigger)
|
||||
return fn
|
||||
|
||||
|
||||
async def build_trigger_callback(
|
||||
args: TemplateArgsType,
|
||||
config: ConfigType,
|
||||
|
||||
@@ -12,6 +12,7 @@ from esphome.cpp_generator import ( # noqa: F401
|
||||
ArrayInitializer,
|
||||
Expression,
|
||||
FlashStringLiteral,
|
||||
LatePvariable,
|
||||
LineComment,
|
||||
LogStringLiteral,
|
||||
MockObj,
|
||||
|
||||
@@ -134,8 +134,10 @@ BinarySensorInitiallyOff = binary_sensor_ns.class_(
|
||||
BinarySensorPtr = BinarySensor.operator("ptr")
|
||||
|
||||
# Triggers
|
||||
ClickDetector = binary_sensor_ns.class_("ClickDetector")
|
||||
DoubleClickDetector = binary_sensor_ns.class_("DoubleClickDetector")
|
||||
ClickTrigger = binary_sensor_ns.class_("ClickTrigger", automation.Trigger.template())
|
||||
DoubleClickTrigger = binary_sensor_ns.class_(
|
||||
"DoubleClickTrigger", automation.Trigger.template()
|
||||
)
|
||||
MultiClickTrigger = binary_sensor_ns.class_(
|
||||
"MultiClickTrigger", automation.Trigger.template(), cg.Component
|
||||
)
|
||||
@@ -448,23 +450,6 @@ def validate_click_timing(value):
|
||||
return value
|
||||
|
||||
|
||||
def _click_automation_schema(detector: MockObjClass) -> cv.All:
|
||||
return cv.All(
|
||||
automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(detector),
|
||||
cv.Optional(
|
||||
CONF_MIN_LENGTH, default="50ms"
|
||||
): cv.positive_time_period_milliseconds,
|
||||
cv.Optional(
|
||||
CONF_MAX_LENGTH, default="350ms"
|
||||
): cv.positive_time_period_milliseconds,
|
||||
}
|
||||
),
|
||||
validate_click_timing,
|
||||
)
|
||||
|
||||
|
||||
def validate_publish_initial_state(value):
|
||||
value = cv.boolean(value)
|
||||
_LOGGER.warning(
|
||||
@@ -497,9 +482,35 @@ _BINARY_SENSOR_SCHEMA = (
|
||||
): validate_filters,
|
||||
cv.Optional(CONF_ON_PRESS): automation.validate_automation({}),
|
||||
cv.Optional(CONF_ON_RELEASE): automation.validate_automation({}),
|
||||
cv.Optional(CONF_ON_CLICK): _click_automation_schema(ClickDetector),
|
||||
cv.Optional(CONF_ON_DOUBLE_CLICK): _click_automation_schema(
|
||||
DoubleClickDetector
|
||||
cv.Optional(CONF_ON_CLICK): cv.All(
|
||||
automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ClickTrigger),
|
||||
cv.Optional(
|
||||
CONF_MIN_LENGTH, default="50ms"
|
||||
): cv.positive_time_period_milliseconds,
|
||||
cv.Optional(
|
||||
CONF_MAX_LENGTH, default="350ms"
|
||||
): cv.positive_time_period_milliseconds,
|
||||
}
|
||||
),
|
||||
validate_click_timing,
|
||||
),
|
||||
cv.Optional(CONF_ON_DOUBLE_CLICK): cv.All(
|
||||
automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
DoubleClickTrigger
|
||||
),
|
||||
cv.Optional(
|
||||
CONF_MIN_LENGTH, default="50ms"
|
||||
): cv.positive_time_period_milliseconds,
|
||||
cv.Optional(
|
||||
CONF_MAX_LENGTH, default="350ms"
|
||||
): cv.positive_time_period_milliseconds,
|
||||
}
|
||||
),
|
||||
validate_click_timing,
|
||||
),
|
||||
cv.Optional(CONF_ON_MULTI_CLICK): automation.validate_automation(
|
||||
{
|
||||
@@ -553,11 +564,6 @@ def binary_sensor_schema(
|
||||
return _BINARY_SENSOR_SCHEMA.extend(schema)
|
||||
|
||||
|
||||
_CLICK_DETECTORS = (
|
||||
(CONF_ON_CLICK, ClickDetector),
|
||||
(CONF_ON_DOUBLE_CLICK, DoubleClickDetector),
|
||||
)
|
||||
|
||||
_CALLBACK_AUTOMATIONS = (
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_PRESS,
|
||||
@@ -580,40 +586,32 @@ _CALLBACK_AUTOMATIONS = (
|
||||
"add_full_state_callback",
|
||||
[(cg.optional.template(bool), "x_previous"), (cg.optional.template(bool), "x")],
|
||||
),
|
||||
*(
|
||||
automation.CallbackAutomation(
|
||||
conf_key,
|
||||
"add_on_state_callback",
|
||||
params=[(bool, "state")],
|
||||
forward=[],
|
||||
when=automation.ApplyCall(
|
||||
"{}->check(state, {}, {})",
|
||||
(
|
||||
(CONF_TRIGGER_ID, detector.operator("ptr")),
|
||||
(CONF_MIN_LENGTH, cg.uint32),
|
||||
(CONF_MAX_LENGTH, cg.uint32),
|
||||
),
|
||||
),
|
||||
)
|
||||
for conf_key, detector in _CLICK_DETECTORS
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.AUTOMATION)
|
||||
async def _build_binary_sensor_automations(var, config):
|
||||
# The detectors must exist before the automations that name them are built
|
||||
if clicks := [c for key, _ in _CLICK_DETECTORS for c in config.get(key, ())]:
|
||||
cg.add_define("USE_BINARY_SENSOR_CLICK_TRIGGER")
|
||||
for conf in clicks:
|
||||
cg.new_Pvariable(conf[CONF_TRIGGER_ID])
|
||||
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
|
||||
|
||||
if config.get(CONF_ON_CLICK) or config.get(CONF_ON_DOUBLE_CLICK):
|
||||
cg.add_define("USE_BINARY_SENSOR_CLICK_TRIGGER")
|
||||
if config.get(CONF_ON_MULTI_CLICK):
|
||||
cg.add_define("USE_BINARY_SENSOR_MULTI_CLICK_TRIGGER")
|
||||
if config.get(CONF_ON_RAW_VALUE):
|
||||
cg.add_define("USE_BINARY_SENSOR_RAW_CALLBACK")
|
||||
|
||||
for conf in config.get(CONF_ON_CLICK, []):
|
||||
trigger = cg.new_Pvariable(
|
||||
conf[CONF_TRIGGER_ID], var, conf[CONF_MIN_LENGTH], conf[CONF_MAX_LENGTH]
|
||||
)
|
||||
await automation.build_automation(trigger, [], conf)
|
||||
|
||||
for conf in config.get(CONF_ON_DOUBLE_CLICK, []):
|
||||
trigger = cg.new_Pvariable(
|
||||
conf[CONF_TRIGGER_ID], var, conf[CONF_MIN_LENGTH], conf[CONF_MAX_LENGTH]
|
||||
)
|
||||
await automation.build_automation(trigger, [], conf)
|
||||
|
||||
for conf in config.get(CONF_ON_MULTI_CLICK, []):
|
||||
timings = [
|
||||
cg.StructInitializer(
|
||||
|
||||
@@ -136,28 +136,6 @@ bool match_interval(uint32_t min_length, uint32_t max_length, uint32_t length) {
|
||||
return length >= min_length && length <= max_length;
|
||||
}
|
||||
}
|
||||
|
||||
bool ClickDetector::check(bool state, uint32_t min_length, uint32_t max_length) {
|
||||
if (state) {
|
||||
this->start_time_ = millis();
|
||||
return false;
|
||||
}
|
||||
return match_interval(min_length, max_length, millis() - this->start_time_);
|
||||
}
|
||||
|
||||
bool DoubleClickDetector::check(bool state, uint32_t min_length, uint32_t max_length) {
|
||||
const uint32_t now = millis();
|
||||
if (state && this->start_time_ != 0 && this->end_time_ != 0 &&
|
||||
match_interval(min_length, max_length, this->end_time_ - this->start_time_) &&
|
||||
match_interval(min_length, max_length, now - this->end_time_)) {
|
||||
this->start_time_ = 0;
|
||||
this->end_time_ = 0;
|
||||
return true;
|
||||
}
|
||||
this->start_time_ = this->end_time_;
|
||||
this->end_time_ = now;
|
||||
return false;
|
||||
}
|
||||
#endif // USE_BINARY_SENSOR_CLICK_TRIGGER
|
||||
|
||||
} // namespace esphome::binary_sensor
|
||||
|
||||
@@ -24,25 +24,54 @@ static_assert(std::is_same_v<decltype(MultiClickTriggerEvent::state), uint32_t>
|
||||
|
||||
bool match_interval(uint32_t min_length, uint32_t max_length, uint32_t length);
|
||||
|
||||
/// Times the presses of one on_click automation.
|
||||
class ClickDetector {
|
||||
class ClickTrigger final : public Trigger<> {
|
||||
public:
|
||||
/// Whether `state` ends a press of min_length to max_length ms; a max_length of 0 means no maximum.
|
||||
bool check(bool state, uint32_t min_length, uint32_t max_length);
|
||||
explicit ClickTrigger(BinarySensor *parent, uint32_t min_length, uint32_t max_length)
|
||||
: min_length_(min_length), max_length_(max_length) {
|
||||
parent->add_on_state_callback([this](bool state) {
|
||||
if (state) {
|
||||
this->start_time_ = millis();
|
||||
} else {
|
||||
const uint32_t length = millis() - this->start_time_;
|
||||
if (match_interval(this->min_length_, this->max_length_, length))
|
||||
this->trigger();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected:
|
||||
uint32_t start_time_{0}; ///< The millis() time when the press started.
|
||||
uint32_t start_time_{0}; /// The millis() time when the click started.
|
||||
uint32_t min_length_; /// Minimum length of click. 0 means no minimum.
|
||||
uint32_t max_length_; /// Maximum length of click. 0 means no maximum.
|
||||
};
|
||||
|
||||
/// Times the presses of one on_double_click automation.
|
||||
class DoubleClickDetector {
|
||||
class DoubleClickTrigger final : public Trigger<> {
|
||||
public:
|
||||
/// Whether `state` is the second press, with the first press and the gap both in the window.
|
||||
bool check(bool state, uint32_t min_length, uint32_t max_length);
|
||||
explicit DoubleClickTrigger(BinarySensor *parent, uint32_t min_length, uint32_t max_length)
|
||||
: min_length_(min_length), max_length_(max_length) {
|
||||
parent->add_on_state_callback([this](bool state) {
|
||||
const uint32_t now = millis();
|
||||
|
||||
if (state && this->start_time_ != 0 && this->end_time_ != 0) {
|
||||
if (match_interval(this->min_length_, this->max_length_, this->end_time_ - this->start_time_) &&
|
||||
match_interval(this->min_length_, this->max_length_, now - this->end_time_)) {
|
||||
this->trigger();
|
||||
this->start_time_ = 0;
|
||||
this->end_time_ = 0;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this->start_time_ = this->end_time_;
|
||||
this->end_time_ = now;
|
||||
});
|
||||
}
|
||||
|
||||
protected:
|
||||
uint32_t start_time_{0}; ///< The millis() time of the edge before the last one.
|
||||
uint32_t end_time_{0}; ///< The millis() time of the last edge.
|
||||
uint32_t start_time_{0};
|
||||
uint32_t end_time_{0};
|
||||
uint32_t min_length_; /// Minimum length of click. 0 means no minimum.
|
||||
uint32_t max_length_; /// Maximum length of click. 0 means no maximum.
|
||||
};
|
||||
|
||||
class MultiClickTrigger final : public Trigger<>, public Component {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
|
||||
from esphome import automation
|
||||
@@ -12,12 +14,14 @@ from esphome.types import ConfigType
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
DOMAIN = "script"
|
||||
script_ns = cg.esphome_ns.namespace("script")
|
||||
Script = script_ns.class_("Script", automation.Trigger.template())
|
||||
Script = script_ns.class_("Script")
|
||||
AutomationScript = script_ns.class_(
|
||||
"AutomationScript", Script, automation.Trigger.template()
|
||||
)
|
||||
FunctionBody = script_ns.class_("FunctionBody", Script)
|
||||
FunctionScript = script_ns.class_("FunctionScript", Script)
|
||||
FunctionPointerScript = script_ns.class_("FunctionPointerScript", Script)
|
||||
ScriptWaitAction = script_ns.class_("ScriptWaitAction", automation.Action, cg.Component)
|
||||
SingleScript = script_ns.class_("SingleScript", Script)
|
||||
RestartScript = script_ns.class_("RestartScript", Script)
|
||||
QueueingScript = script_ns.class_("QueueingScript", Script, cg.Component)
|
||||
ParallelScript = script_ns.class_("ParallelScript", Script)
|
||||
|
||||
CONF_SINGLE = "single"
|
||||
CONF_QUEUED = "queued"
|
||||
@@ -25,12 +29,38 @@ CONF_PARALLEL = "parallel"
|
||||
CONF_MAX_RUNS = "max_runs"
|
||||
CONF_SCRIPT_STOP = "script.stop"
|
||||
|
||||
SCRIPT_MODES = {
|
||||
CONF_SINGLE: SingleScript,
|
||||
CONF_RESTART: RestartScript,
|
||||
CONF_QUEUED: QueueingScript,
|
||||
CONF_PARALLEL: ParallelScript,
|
||||
|
||||
def _mode(
|
||||
name: str, *parents: cg.MockObjClass
|
||||
) -> tuple[cg.MockObjClass, cg.MockObjClass]:
|
||||
"""The class of a mode with an Automation, and its template on the class that runs."""
|
||||
return (
|
||||
script_ns.class_(f"{name}Script", AutomationScript, *parents),
|
||||
script_ns.class_(f"{name}Mode", Script, *parents),
|
||||
)
|
||||
|
||||
|
||||
MODES = {
|
||||
CONF_SINGLE: _mode("Single"),
|
||||
CONF_RESTART: _mode("Restart"),
|
||||
CONF_QUEUED: _mode("Queueing", cg.Component),
|
||||
CONF_PARALLEL: _mode("Parallel"),
|
||||
}
|
||||
SCRIPT_MODES = {mode: classes[0] for mode, classes in MODES.items()}
|
||||
|
||||
|
||||
@dataclass
|
||||
class ScriptData:
|
||||
# By script id: the class script.wait is templated on, or the script.wait actions to
|
||||
# create once the script's class is known
|
||||
waits: dict[str, cg.MockObj | list[Callable[[cg.MockObj], None]]] = field(
|
||||
default_factory=dict
|
||||
)
|
||||
|
||||
|
||||
def _data() -> ScriptData:
|
||||
return CORE.data.setdefault(DOMAIN, ScriptData())
|
||||
|
||||
|
||||
PARAMETER_TYPE_TRANSLATIONS = {
|
||||
"string": "std::string",
|
||||
@@ -103,6 +133,20 @@ def validate_parameter_name(value):
|
||||
raise cv.Invalid(f"Script's parameter name cannot be {CONF_ID}")
|
||||
|
||||
|
||||
def validate_parameter_names(value: Any) -> Any:
|
||||
"""Reject reserved parameter names here: as dict keys of a schema they would only be
|
||||
reported as "extra keys not allowed"."""
|
||||
if isinstance(value, dict):
|
||||
for name in value:
|
||||
# Generated C++ code names its own variables like this
|
||||
if str(name).startswith("esphome__"):
|
||||
raise cv.Invalid(
|
||||
"Names starting with 'esphome__' are reserved for generated code",
|
||||
path=[name],
|
||||
)
|
||||
return value
|
||||
|
||||
|
||||
ALLOWED_PARAM_TYPE_CHARSET = set("abcdefghijklmnopqrstuvwxyz0123456789_:*&[]<>")
|
||||
|
||||
|
||||
@@ -170,10 +214,9 @@ CONFIG_SCHEMA = cv.All(
|
||||
*SCRIPT_MODES, lower=True
|
||||
),
|
||||
cv.Optional(CONF_MAX_RUNS): cv.int_range(min=0, max=100),
|
||||
cv.Optional(CONF_PARAMETERS, default={}): cv.Schema(
|
||||
{
|
||||
validate_parameter_name: validate_parameter_type,
|
||||
}
|
||||
cv.Optional(CONF_PARAMETERS, default={}): cv.All(
|
||||
validate_parameter_names,
|
||||
cv.Schema({validate_parameter_name: validate_parameter_type}),
|
||||
),
|
||||
cv.Optional(CONF_ON_STOP): automation.validate_automation(single=True),
|
||||
},
|
||||
@@ -183,28 +226,56 @@ CONFIG_SCHEMA = cv.All(
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
# Register all variables first, so that scripts can use other scripts
|
||||
triggers = []
|
||||
# Register all variables first, so that scripts can use other scripts. Each is created
|
||||
# once its actions are built, which decides its class, in the place it was declared.
|
||||
waits = _data().waits
|
||||
scripts = []
|
||||
for conf in config:
|
||||
template, func_args = parameters_to_template(conf[CONF_PARAMETERS])
|
||||
trigger = cg.new_Pvariable(conf[CONF_ID], template)
|
||||
id_ = conf[CONF_ID].copy()
|
||||
id_.type = id_.type.template(template)
|
||||
late = cg.LatePvariable(id_)
|
||||
waits[id_.id] = []
|
||||
script = late.var
|
||||
# Add a human-readable name to the script
|
||||
cg.add(trigger.set_name(cg.LogStringLiteral(conf[CONF_ID].id)))
|
||||
cg.add(script.set_name(cg.LogStringLiteral(id_.id)))
|
||||
|
||||
if CONF_MAX_RUNS in conf:
|
||||
cg.add(trigger.set_max_runs(conf[CONF_MAX_RUNS]))
|
||||
cg.add(script.set_max_runs(conf[CONF_MAX_RUNS]))
|
||||
|
||||
if conf[CONF_MODE] == CONF_QUEUED:
|
||||
await cg.register_component(trigger, conf)
|
||||
if queued := conf[CONF_MODE] == CONF_QUEUED:
|
||||
await cg.register_component(script, conf)
|
||||
|
||||
triggers.append((trigger, func_args, conf))
|
||||
scripts.append((late, template, func_args, conf, queued))
|
||||
|
||||
if any(CONF_ON_STOP in conf for conf in config):
|
||||
cg.add_define("USE_SCRIPT_ON_STOP")
|
||||
for trigger, func_args, conf in triggers:
|
||||
await automation.build_automation(trigger, func_args, conf)
|
||||
for late, template, func_args, conf, queued in scripts:
|
||||
# The class or its constructor names the function, so it is declared in the script's place
|
||||
fn = await automation.build_automation_or_run_function(
|
||||
late.var, func_args, conf, late.global_slot.add
|
||||
)
|
||||
script_class = wait_class = late.id.type
|
||||
ctor_args = []
|
||||
if fn is not None:
|
||||
wait_class = MODES[conf[CONF_MODE]][1].template(
|
||||
FunctionBody.template(template), *template
|
||||
)
|
||||
if queued:
|
||||
# One class for all of them: a per-script class would also copy the
|
||||
# Component vtable, which costs more flash than the pointer's RAM
|
||||
script_class = FunctionPointerScript.template(wait_class, *template)
|
||||
ctor_args = [fn]
|
||||
else:
|
||||
script_class = FunctionScript.template(wait_class, fn, *template)
|
||||
late.define(script_class, *ctor_args)
|
||||
for create_wait in waits[late.id.id]:
|
||||
create_wait(wait_class)
|
||||
waits[late.id.id] = wait_class
|
||||
if (stop_conf := conf.get(CONF_ON_STOP)) is not None:
|
||||
await automation.build_automation(trigger.get_stop_trigger(), [], stop_conf)
|
||||
await automation.build_automation(
|
||||
late.var.get_stop_trigger(), [], stop_conf
|
||||
)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
@@ -291,8 +362,25 @@ automation.register_apply_action(
|
||||
)
|
||||
async def script_wait_action_to_code(config, action_id, template_arg, args):
|
||||
full_id, paren = await cg.get_variable_with_full_id(config[CONF_ID])
|
||||
template_arg = cg.TemplateArguments(full_id.type, *template_arg)
|
||||
var = cg.new_Pvariable(action_id, template_arg, paren)
|
||||
script_waits = _data().waits[full_id.id]
|
||||
pending = isinstance(script_waits, list)
|
||||
# Created where it is built, once the class of the script it waits for is known
|
||||
id_ = action_id.copy()
|
||||
id_.type = id_.type.template(
|
||||
full_id.type if pending else script_waits, *template_arg
|
||||
)
|
||||
late = cg.LatePvariable(id_)
|
||||
|
||||
def create(script_class: cg.MockObj) -> None:
|
||||
late.define(
|
||||
action_id.type, cg.TemplateArguments(script_class, *template_arg), paren
|
||||
)
|
||||
|
||||
if pending:
|
||||
script_waits.append(create)
|
||||
else:
|
||||
create(script_waits)
|
||||
var = late.var
|
||||
await cg.register_component(var, {})
|
||||
return var
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ class ScriptLogger {
|
||||
};
|
||||
|
||||
/// The abstract base class for all script types.
|
||||
template<typename... Ts> class Script : public ScriptLogger, public Trigger<Ts...> {
|
||||
template<typename... Ts> class Script : public ScriptLogger {
|
||||
public:
|
||||
/** Execute a new instance of this script.
|
||||
*
|
||||
@@ -32,55 +32,94 @@ template<typename... Ts> class Script : public ScriptLogger, public Trigger<Ts..
|
||||
*/
|
||||
virtual void execute(Ts...) = 0;
|
||||
/// Check if any instance of this script is currently running.
|
||||
virtual bool is_running() { return this->is_action_running(); }
|
||||
#ifdef USE_SCRIPT_ON_STOP
|
||||
virtual bool is_running() = 0;
|
||||
/// Stop all instances of this script. Fires the on_stop trigger once if any instance was running.
|
||||
virtual void stop() {
|
||||
if (this->is_action_running())
|
||||
this->stop_running_();
|
||||
}
|
||||
virtual void stop() = 0;
|
||||
|
||||
#ifdef USE_SCRIPT_ON_STOP
|
||||
Trigger<> *get_stop_trigger() { return &this->stop_trigger_; }
|
||||
#else
|
||||
/// Stop all instances of this script.
|
||||
virtual void stop() { this->stop_action(); }
|
||||
#endif
|
||||
|
||||
// Internal function to give scripts readable names.
|
||||
void set_name(const LogString *name) { name_ = name; }
|
||||
|
||||
protected:
|
||||
// Run the action chain with this script's name published as the current source (RAII save/restore,
|
||||
// so nesting composes), so deferred work inside the script is attributed to it in blocking
|
||||
// warnings. Force-inlined to fold into the always-inlined trigger chain (no extra stack frame).
|
||||
inline void run_actions_(const Ts &...x) ESPHOME_ALWAYS_INLINE {
|
||||
ScopedSourceGuard source_guard{this->name_};
|
||||
this->trigger(x...);
|
||||
}
|
||||
|
||||
// Stop the running actions early and fire the on_stop trigger.
|
||||
void stop_running_() {
|
||||
this->stop_action();
|
||||
#ifdef USE_SCRIPT_ON_STOP
|
||||
this->stop_trigger_.trigger();
|
||||
#endif
|
||||
}
|
||||
|
||||
const LogString *name_{nullptr};
|
||||
#ifdef USE_SCRIPT_ON_STOP
|
||||
Trigger<> stop_trigger_;
|
||||
#endif
|
||||
};
|
||||
|
||||
/// Runs the script's actions through an Automation, for actions that can wait (delay, wait_until, ...).
|
||||
template<typename... Ts> class AutomationScript : public Script<Ts...>, public Trigger<Ts...> {
|
||||
protected:
|
||||
bool body_running_() { return this->is_action_running(); }
|
||||
int body_num_running_() { return this->automation_parent_->num_running(); }
|
||||
inline void run_body_(const Ts &...x) ESPHOME_ALWAYS_INLINE { this->trigger(x...); }
|
||||
void stop_body_() { this->stop_action(); }
|
||||
};
|
||||
|
||||
/** Runs the script's actions as one generated function, when they are all inline.
|
||||
*
|
||||
* Like ApplyRunAction, the function gets the run count by reference and returns early once a
|
||||
* stop clears it. The mode code built on this is shared; FunctionScript or FunctionPointerScript
|
||||
* supplies the function.
|
||||
*/
|
||||
template<typename... Ts> class FunctionBody : public Script<Ts...> {
|
||||
protected:
|
||||
bool body_running_() { return this->num_running_ > 0; }
|
||||
int body_num_running_() { return this->num_running_; }
|
||||
inline void run_body_(const Ts &...x) ESPHOME_ALWAYS_INLINE {
|
||||
this->num_running_++;
|
||||
this->run_function(this->num_running_, x...);
|
||||
if (this->num_running_ > 0)
|
||||
this->num_running_--;
|
||||
}
|
||||
void stop_body_() { this->num_running_ = 0; }
|
||||
virtual void run_function(const int &running, const Ts &...x) = 0;
|
||||
|
||||
int num_running_{0};
|
||||
};
|
||||
|
||||
/// What all script modes share, on top of the class that runs the actions.
|
||||
template<class Body, typename... Ts> class ScriptBase : public Body {
|
||||
public:
|
||||
bool is_running() override { return this->body_running_(); }
|
||||
#ifdef USE_SCRIPT_ON_STOP
|
||||
void stop() override {
|
||||
if (this->body_running_())
|
||||
this->stop_running_();
|
||||
}
|
||||
#else
|
||||
void stop() override { this->stop_body_(); }
|
||||
#endif
|
||||
|
||||
protected:
|
||||
// Publish this script's name as the current source while the actions run (RAII, so nesting
|
||||
// composes), so deferred work is attributed to it in blocking warnings.
|
||||
inline void run_actions_(const Ts &...x) ESPHOME_ALWAYS_INLINE {
|
||||
ScopedSourceGuard source_guard{this->name_};
|
||||
this->run_body_(x...);
|
||||
}
|
||||
|
||||
// Stop the running actions early and fire the on_stop trigger.
|
||||
void stop_running_() {
|
||||
this->stop_body_();
|
||||
#ifdef USE_SCRIPT_ON_STOP
|
||||
this->stop_trigger_.trigger();
|
||||
#endif
|
||||
}
|
||||
};
|
||||
|
||||
/** A script type for which only a single instance at a time is allowed.
|
||||
*
|
||||
* If a new instance is executed while the previous one hasn't finished yet,
|
||||
* a warning is printed and the new instance is discarded.
|
||||
*/
|
||||
template<typename... Ts> class SingleScript : public Script<Ts...> {
|
||||
template<class Body, typename... Ts> class SingleMode : public ScriptBase<Body, Ts...> {
|
||||
public:
|
||||
void execute(Ts... x) override {
|
||||
if (this->is_action_running()) {
|
||||
if (this->body_running_()) {
|
||||
this->esp_logw_(__LINE__, ESPHOME_LOG_FORMAT("Script '%s' is already running! (mode: single)"),
|
||||
LOG_STR_ARG(this->name_));
|
||||
return;
|
||||
@@ -95,16 +134,16 @@ template<typename... Ts> class SingleScript : public Script<Ts...> {
|
||||
* If a new instance is started but another one is already running, the existing
|
||||
* script is stopped and the new instance starts from the beginning.
|
||||
*/
|
||||
template<typename... Ts> class RestartScript : public Script<Ts...> {
|
||||
template<class Body, typename... Ts> class RestartMode : public ScriptBase<Body, Ts...> {
|
||||
public:
|
||||
void execute(Ts... x) override {
|
||||
if (this->is_action_running()) {
|
||||
if (this->body_running_()) {
|
||||
this->esp_logd_(__LINE__, ESPHOME_LOG_FORMAT("Script '%s' restarting (mode: restart)"), LOG_STR_ARG(this->name_));
|
||||
this->stop_running_();
|
||||
#ifdef USE_SCRIPT_ON_STOP
|
||||
// on_stop may have executed this script again; only the newest run may continue
|
||||
if (this->is_action_running())
|
||||
this->stop_action();
|
||||
if (this->body_running_())
|
||||
this->stop_body_();
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -126,10 +165,10 @@ template<typename... Ts> class RestartScript : public Script<Ts...> {
|
||||
* - Subsequent executes while running are queued starting at position 0
|
||||
* - Maximum total instances = max_runs_ (includes 1 running + (max_runs_ - 1) queued)
|
||||
*/
|
||||
template<typename... Ts> class QueueingScript : public Script<Ts...>, public Component {
|
||||
template<class Body, typename... Ts> class QueueingMode : public ScriptBase<Body, Ts...>, public Component {
|
||||
public:
|
||||
void execute(Ts... x) override {
|
||||
if (this->is_action_running() || this->num_queued_ > 0) {
|
||||
if (this->body_running_() || this->num_queued_ > 0) {
|
||||
// num_queued_ is the number of *queued* instances (waiting, not including currently running)
|
||||
// max_runs_ is the maximum *total* instances (running + queued)
|
||||
// So we reject when num_queued_ + 1 >= max_runs_ (queued + running >= max)
|
||||
@@ -164,7 +203,7 @@ template<typename... Ts> class QueueingScript : public Script<Ts...>, public Com
|
||||
void stop() override {
|
||||
#ifdef USE_SCRIPT_ON_STOP
|
||||
// Queued runs are unfinished work too, even between two runs when nothing is running
|
||||
const bool stopping = this->is_action_running() || this->num_queued_ != 0;
|
||||
const bool stopping = this->body_running_() || this->num_queued_ != 0;
|
||||
#endif
|
||||
// Clear all queued items to free memory immediately
|
||||
// Resetting the array automatically destroys all unique_ptrs and their contents
|
||||
@@ -175,12 +214,12 @@ template<typename... Ts> class QueueingScript : public Script<Ts...>, public Com
|
||||
if (stopping)
|
||||
this->stop_running_();
|
||||
#else
|
||||
Script<Ts...>::stop();
|
||||
ScriptBase<Body, Ts...>::stop();
|
||||
#endif
|
||||
}
|
||||
|
||||
void loop() override {
|
||||
if (this->num_queued_ != 0 && !this->is_action_running()) {
|
||||
if (this->num_queued_ != 0 && !this->body_running_()) {
|
||||
// Dequeue: decrement count, move tuple out (frees slot), advance read position
|
||||
this->num_queued_--;
|
||||
const size_t queue_capacity = static_cast<size_t>(this->max_runs_ - 1);
|
||||
@@ -227,10 +266,10 @@ template<typename... Ts> class QueueingScript : public Script<Ts...>, public Com
|
||||
* If a new instance is started while previous ones haven't finished yet,
|
||||
* the new one is executed in parallel to the other instances.
|
||||
*/
|
||||
template<typename... Ts> class ParallelScript : public Script<Ts...> {
|
||||
template<class Body, typename... Ts> class ParallelMode : public ScriptBase<Body, Ts...> {
|
||||
public:
|
||||
void execute(Ts... x) override {
|
||||
if (this->max_runs_ != 0 && this->automation_parent_->num_running() >= this->max_runs_) {
|
||||
if (this->max_runs_ != 0 && this->body_num_running_() >= this->max_runs_) {
|
||||
this->esp_logw_(__LINE__, ESPHOME_LOG_FORMAT("Script '%s' maximum number of parallel runs exceeded!"),
|
||||
LOG_STR_ARG(this->name_));
|
||||
return;
|
||||
@@ -243,6 +282,32 @@ template<typename... Ts> class ParallelScript : public Script<Ts...> {
|
||||
int max_runs_ = 0;
|
||||
};
|
||||
|
||||
template<typename... Ts> using SingleScript = SingleMode<AutomationScript<Ts...>, Ts...>;
|
||||
template<typename... Ts> using RestartScript = RestartMode<AutomationScript<Ts...>, Ts...>;
|
||||
template<typename... Ts> using QueueingScript = QueueingMode<AutomationScript<Ts...>, Ts...>;
|
||||
template<typename... Ts> using ParallelScript = ParallelMode<AutomationScript<Ts...>, Ts...>;
|
||||
|
||||
/// A script of mode `Mode` (built on FunctionBody) whose actions are the generated function Fn.
|
||||
template<class Mode, auto Fn, typename... Ts> class FunctionScript final : public Mode {
|
||||
protected:
|
||||
void run_function(const int &running, const Ts &...x) override { Fn(running, x...); }
|
||||
};
|
||||
|
||||
/** Like FunctionScript, but one class for all scripts of `Mode`, holding the function.
|
||||
*
|
||||
* For queued scripts, whose per-script class would copy the Component vtable too.
|
||||
*/
|
||||
template<class Mode, typename... Ts> class FunctionPointerScript final : public Mode {
|
||||
public:
|
||||
using fn_t = void (*)(const int &, const Ts &...);
|
||||
explicit FunctionPointerScript(fn_t fn) : fn_(fn) {}
|
||||
|
||||
protected:
|
||||
void run_function(const int &running, const Ts &...x) override { this->fn_(running, x...); }
|
||||
|
||||
const fn_t fn_;
|
||||
};
|
||||
|
||||
/** Wait for a script to finish before continuing.
|
||||
*
|
||||
* Uses queue-based storage to safely handle concurrent executions.
|
||||
|
||||
+65
-14
@@ -803,6 +803,18 @@ def Pvariable(id_: ID, rhs: SafeExpType, type_: "MockObj" = None) -> "MockObj":
|
||||
if type_ is not None:
|
||||
id_.type = type_
|
||||
|
||||
_emit_pvariable(id_, rhs, CORE.add_global, CORE.add)
|
||||
CORE.register_variable(id_, obj)
|
||||
return obj
|
||||
|
||||
|
||||
def _emit_pvariable(
|
||||
id_: ID,
|
||||
rhs: SafeExpType,
|
||||
add_global_: Callable[[Expression | Statement], object],
|
||||
add_: Callable[[Expression | Statement], object],
|
||||
) -> None:
|
||||
"""Emit the storage, declaration and construction of the pointer variable ``id_``."""
|
||||
if isinstance(rhs, MockObj) and rhs.is_new_expr:
|
||||
# For 'new' allocations, use placement new into static storage
|
||||
# to avoid heap fragmentation on embedded devices.
|
||||
@@ -825,14 +837,14 @@ def Pvariable(id_: ID, rhs: SafeExpType, type_: "MockObj" = None) -> "MockObj":
|
||||
storage_name = _pstorage_name(actual_type, id_.id)
|
||||
|
||||
# Declare aligned byte array for the object storage
|
||||
CORE.add_global(
|
||||
add_global_(
|
||||
RawStatement(
|
||||
f"alignas({actual_type}) static unsigned char {storage_name}[sizeof({actual_type})];"
|
||||
)
|
||||
)
|
||||
# Pointer declaration uses id_.type to preserve the declared base-class
|
||||
# pointer type for downstream callers (polymorphism through base ptr).
|
||||
CORE.add_global(
|
||||
add_global_(
|
||||
AssignmentExpression(
|
||||
f"static {pointer_type}",
|
||||
"*const ",
|
||||
@@ -841,15 +853,48 @@ def Pvariable(id_: ID, rhs: SafeExpType, type_: "MockObj" = None) -> "MockObj":
|
||||
)
|
||||
)
|
||||
placement_new = CallExpression(f"new({id_.id}) {actual_type}", *call_expr.args)
|
||||
CORE.add(ExpressionStatement(placement_new))
|
||||
add_(ExpressionStatement(placement_new))
|
||||
CORE.data.setdefault(_STATIC_POINTER_IDS, set()).add(id_.id)
|
||||
else:
|
||||
decl = VariableDeclarationExpression(id_.type, "*", id_, static=True)
|
||||
CORE.add_global(decl)
|
||||
CORE.add(AssignmentExpression(None, None, id_, rhs))
|
||||
add_global_(decl)
|
||||
add_(AssignmentExpression(None, None, id_, rhs))
|
||||
|
||||
CORE.register_variable(id_, obj)
|
||||
return obj
|
||||
|
||||
class _StatementSlot(Statement):
|
||||
"""A place for statements that are added later."""
|
||||
|
||||
__slots__ = ("statements",)
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.statements: list[Statement] = []
|
||||
|
||||
def add(self, expression: Expression | Statement) -> None:
|
||||
self.statements.append(statement(expression))
|
||||
|
||||
def __str__(self) -> str:
|
||||
return "\n".join(str(item).rstrip() for item in self.statements)
|
||||
|
||||
|
||||
class LatePvariable:
|
||||
"""A pointer variable registered now, so other code can refer to ``var``, and created by
|
||||
``define()`` in the place it was declared, once its class is known."""
|
||||
|
||||
def __init__(self, id_: ID) -> None:
|
||||
self.id = id_
|
||||
self.var = MockObj(id_, "->")
|
||||
# Global declarations that must come before the variable can be added here
|
||||
self.global_slot = _StatementSlot()
|
||||
self._main_slot = _StatementSlot()
|
||||
CORE.add_global(self.global_slot)
|
||||
CORE.add(self._main_slot)
|
||||
CORE.register_variable(id_, self.var)
|
||||
|
||||
def define(self, type_: "MockObjClass", *args: SafeExpType) -> None:
|
||||
"""Create the variable as ``type_``, like ``new_Pvariable`` with ``args``."""
|
||||
self.id.type = type_
|
||||
rhs = _new_expression(self.id, args)
|
||||
_emit_pvariable(self.id, rhs, self.global_slot.add, self._main_slot.add)
|
||||
|
||||
|
||||
_STATIC_POINTER_IDS = "static_pointer_ids"
|
||||
@@ -871,16 +916,21 @@ def new_Pvariable(id_: ID, *args: SafeExpType) -> "MockObj":
|
||||
with the given arguments.
|
||||
|
||||
:param id_: The ID used to declare the variable (also specifies the type).
|
||||
:param args: The values to pass to the constructor.
|
||||
:param args: The values to pass to the constructor, after optional template arguments.
|
||||
|
||||
:return: The new variable as a MockObj.
|
||||
"""
|
||||
if args and isinstance(args[0], TemplateArguments):
|
||||
id_ = id_.copy()
|
||||
return Pvariable(id_, _new_expression(id_, args))
|
||||
|
||||
|
||||
def _new_expression(id_: ID, args: tuple[SafeExpType, ...]) -> "MockObj":
|
||||
"""``new`` of ``id_.type``, first templated on ``args[0]`` if that is TemplateArguments."""
|
||||
if args and isinstance(args[0], TemplateArguments):
|
||||
id_.type = id_.type.template(args[0])
|
||||
args = args[1:]
|
||||
rhs = id_.type.new(*args)
|
||||
return Pvariable(id_, rhs)
|
||||
return id_.type.new(*args)
|
||||
|
||||
|
||||
def static_function(
|
||||
@@ -888,18 +938,19 @@ def static_function(
|
||||
return_type: SafeExpType,
|
||||
parameters: TemplateArgsType,
|
||||
body: list[str | Statement],
|
||||
add_global_: Callable[[Statement], object] | None = None,
|
||||
) -> RawExpression:
|
||||
"""Emit ``static <return_type> <name>(parameters) { body }`` at global scope and return an
|
||||
expression naming it, for use as a template argument or a function pointer.
|
||||
|
||||
The declaration goes with the other globals, so a template argument can name the function
|
||||
at once; the body goes after the user includes, where every id and everything those
|
||||
headers declare is in scope.
|
||||
The declaration goes with the other globals (or to ``add_global_``, e.g. a
|
||||
``LatePvariable``'s place), so a template argument can name the function at once; the body
|
||||
goes after the user includes, where every id and everything those headers declare is in scope.
|
||||
"""
|
||||
signature = (
|
||||
f"static {safe_exp(return_type)} {name}({ParameterListExpression(*parameters)})"
|
||||
)
|
||||
add_global(RawStatement(f"{signature};"))
|
||||
(add_global_ or add_global)(RawStatement(f"{signature};"))
|
||||
add_global_after_includes(
|
||||
RawStatement(f"{signature} {{\n{indent(chr(10).join(map(str, body)))}\n}}")
|
||||
)
|
||||
|
||||
@@ -8,24 +8,18 @@ from esphome.core import CORE
|
||||
|
||||
|
||||
def test_click_triggers(generate_main: Callable[[str | Path], str]) -> None:
|
||||
"""Each trigger gets a detector and a callback with its timing in milliseconds, defaults 50 and 350."""
|
||||
"""Each trigger gets its timing in milliseconds, defaults 50 and 350, and its own automation."""
|
||||
main_cpp = generate_main("tests/component_tests/binary_sensor/test_click.yaml")
|
||||
|
||||
kinds = dict(
|
||||
re.findall(
|
||||
r"new\((\w+)\) binary_sensor::(Click|DoubleClick)Detector\(\);", main_cpp
|
||||
)
|
||||
triggers = re.findall(
|
||||
r"new\((\w+)\) binary_sensor::(Click|DoubleClick)Trigger\(button_a, (\d+), (\d+)\);",
|
||||
main_cpp,
|
||||
)
|
||||
calls = re.findall(r"if \(!\(::(\w+)->check\(state, (\d+), (\d+)\)\)\)", main_cpp)
|
||||
assert [(kinds[detector_id], *timing) for detector_id, *timing in calls] == [
|
||||
assert [trigger[1:] for trigger in triggers] == [
|
||||
("Click", "50", "350"),
|
||||
("Click", "1000", "3000"),
|
||||
("DoubleClick", "20", "400"),
|
||||
]
|
||||
assert main_cpp.count("button_a->add_on_state_callback([](") == 3
|
||||
# Only the automation with a delay keeps an Automation object
|
||||
assert re.findall(r"new\((\w+)\) Automation<>\(\);", main_cpp) == [
|
||||
"automation_id_2"
|
||||
]
|
||||
assert "::automation_id_2->trigger();" in main_cpp
|
||||
for trigger_id, *_ in triggers:
|
||||
assert f"Automation<>({trigger_id});" in main_cpp
|
||||
assert "USE_BINARY_SENSOR_CLICK_TRIGGER" in {define.name for define in CORE.defines}
|
||||
@@ -21,9 +21,7 @@ binary_sensor:
|
||||
- delay: 1s
|
||||
- logger.log: a long click done
|
||||
on_double_click:
|
||||
# Named like the callback parameter, which must not hide it
|
||||
- trigger_id: state
|
||||
min_length: 20ms
|
||||
- min_length: 20ms
|
||||
max_length: 400ms
|
||||
then:
|
||||
- logger.log: a double click
|
||||
@@ -14,7 +14,8 @@ def test_nec_field_lambdas_get_the_script_args(
|
||||
main_cpp = generate_main(component_config_path("nec_lambda_args.yaml"))
|
||||
(body,) = protocol_call_bodies(main_cpp)
|
||||
assert re.search(
|
||||
r"static void esphome__\w+__fn\(const std::remove_cvref_t<int> & x\) \{",
|
||||
r"static void esphome__\w+__fn\(const int & esphome__running, "
|
||||
r"const std::remove_cvref_t<int> & x\) \{",
|
||||
main_cpp,
|
||||
)
|
||||
assert "apply_call.data.address = [](int x) -> uint16_t {" in body
|
||||
|
||||
@@ -51,9 +51,10 @@ def test_code_storage_id_warns(
|
||||
def test_no_action_objects(main_cpp: str) -> None:
|
||||
assert "RawAction" not in main_cpp
|
||||
assert "RCSwitch" not in main_cpp.replace("RCSwitchBase", "")
|
||||
# The script's two transmits merge into one action; the buttons call functions directly
|
||||
assert main_cpp.count("new(applyaction_id") == 1
|
||||
assert main_cpp.count("new(automation_id") == 1
|
||||
# The script runs its transmits from one generated function; the buttons call functions directly
|
||||
assert "new(applyaction_id" not in main_cpp
|
||||
assert "new(automation_id" not in main_cpp
|
||||
assert "FunctionScript<" in main_cpp
|
||||
|
||||
|
||||
def test_raw_static_code_in_flash(main_cpp: str, bodies: list[str]) -> None:
|
||||
|
||||
@@ -33,24 +33,50 @@ def _actions_of(main_cpp: str, automation: str) -> list[str]:
|
||||
return re.findall(rf"\b{automation}->add_action\((\w+)\);", main_cpp)
|
||||
|
||||
|
||||
def _single_action(main_cpp: str, script: str) -> str:
|
||||
(action,) = _actions_of(main_cpp, _automation(main_cpp, script))
|
||||
return action
|
||||
def _template_tail(types: tuple[str, ...]) -> str:
|
||||
return "".join(f", {type_}" for type_ in types)
|
||||
|
||||
|
||||
def _run_function(main_cpp: str, script: str) -> str:
|
||||
"""Return the function of the one merged action the script's Automation runs."""
|
||||
constructed = _constructed(main_cpp, _single_action(main_cpp, script))
|
||||
match = re.match(r"ApplyRunAction<(\w+)[,>]", constructed)
|
||||
assert match is not None, constructed
|
||||
return match.group(1)
|
||||
def _mode_class(mode: str, *types: str) -> str:
|
||||
"""The mode class shared by scripts that run a function, with the same parameters."""
|
||||
return f"script::{mode}<script::FunctionBody<{', '.join(types)}>{_template_tail(types)}>"
|
||||
|
||||
|
||||
def test_inline_actions_merge_into_one_action(generate_main) -> None:
|
||||
"""A script of inline actions runs them as one merged action of an Automation."""
|
||||
def _function_script(mode: str, script: str, *types: str) -> str:
|
||||
"""The class of a script whose actions run as one function."""
|
||||
return (
|
||||
f"script::FunctionScript<{_mode_class(mode, *types)}, "
|
||||
f"esphome__{script}__fn{_template_tail(types)}>"
|
||||
)
|
||||
|
||||
|
||||
def _assert_function_script(main_cpp: str, mode: str, script: str, *types: str) -> None:
|
||||
assert (
|
||||
_constructed(main_cpp, script) == f"{_function_script(mode, script, *types)}()"
|
||||
)
|
||||
|
||||
|
||||
def _wait_class(script_class: str) -> str:
|
||||
return f"script::ScriptWaitAction<{script_class}>"
|
||||
|
||||
|
||||
def _script_body(main_cpp: str, script: str) -> str:
|
||||
"""The function a script runs; it has no Automation."""
|
||||
assert not re.search(rf"\bAutomation<.*?>\({script}\);", main_cpp)
|
||||
return _function_body(main_cpp, f"esphome__{script}__fn")
|
||||
|
||||
|
||||
def test_inline_actions_run_as_one_function(generate_main) -> None:
|
||||
"""A script of inline actions calls one function, declared before the script."""
|
||||
main_cpp = generate_main(CONFIG)
|
||||
assert _constructed(main_cpp, "inline_single") == "script::SingleScript<>()"
|
||||
body = _function_body(main_cpp, _run_function(main_cpp, "inline_single"))
|
||||
class_ = _function_script("SingleMode", "inline_single")
|
||||
assert _constructed(main_cpp, "inline_single") == f"{class_}()"
|
||||
declaration = main_cpp.index(
|
||||
"static void esphome__inline_single__fn(const int & esphome__running);"
|
||||
)
|
||||
assert declaration < main_cpp.index(f"alignas({class_})")
|
||||
assert f"static {class_} *const inline_single" in main_cpp
|
||||
body = _script_body(main_cpp, "inline_single")
|
||||
assert '"INLINE first"' in body
|
||||
assert "counter->value() += 1;" in body
|
||||
# A script can be stopped from inside, so the rest of the run checks for it
|
||||
@@ -58,31 +84,37 @@ def test_inline_actions_merge_into_one_action(generate_main) -> None:
|
||||
|
||||
|
||||
def test_one_lambda_action(generate_main) -> None:
|
||||
"""A single inline action keeps its own action object."""
|
||||
"""A single inline action is the whole function."""
|
||||
main_cpp = generate_main(CONFIG)
|
||||
constructed = _constructed(main_cpp, _single_action(main_cpp, "one_action"))
|
||||
assert constructed.startswith("StatelessLambdaAction<>(")
|
||||
assert '"ONE action"' in constructed
|
||||
body = _script_body(main_cpp, "one_action")
|
||||
assert '"ONE action"' in body
|
||||
assert "esphome__running" not in body
|
||||
|
||||
|
||||
def test_modes(generate_main) -> None:
|
||||
"""Each mode has its own class; max_runs is set on queued and parallel scripts."""
|
||||
"""Each mode has its own template; max_runs is set on queued and parallel scripts."""
|
||||
main_cpp = generate_main(CONFIG)
|
||||
assert _constructed(main_cpp, "inline_restart") == "script::RestartScript<>()"
|
||||
assert _constructed(main_cpp, "inline_queued") == "script::QueueingScript<>()"
|
||||
assert _constructed(main_cpp, "inline_parallel") == "script::ParallelScript<>()"
|
||||
for mode, script in (
|
||||
("RestartMode", "inline_restart"),
|
||||
("ParallelMode", "inline_parallel"),
|
||||
):
|
||||
_assert_function_script(main_cpp, mode, script)
|
||||
# One class for all queued scripts, given the function
|
||||
assert _constructed(main_cpp, "inline_queued") == (
|
||||
f"script::FunctionPointerScript<{_mode_class('QueueingMode')}>"
|
||||
"(esphome__inline_queued__fn)"
|
||||
)
|
||||
assert "inline_queued->set_max_runs(3);" in main_cpp
|
||||
assert "inline_parallel->set_max_runs(2);" in main_cpp
|
||||
assert re.search(r"App\.register_component_\(inline_queued, \d+\);", main_cpp)
|
||||
for script in ("inline_queued", "inline_parallel"):
|
||||
constructed = _constructed(main_cpp, _single_action(main_cpp, script))
|
||||
assert constructed.startswith("StatelessLambdaAction<>(")
|
||||
assert '"QUEUED run"' in _script_body(main_cpp, "inline_queued")
|
||||
assert '"PARALLEL run"' in _script_body(main_cpp, "inline_parallel")
|
||||
|
||||
|
||||
def test_restart_executes_itself(generate_main) -> None:
|
||||
"""A script can execute itself; on_stop has its own Automation."""
|
||||
"""A script can execute itself; on_stop keeps its own Automation."""
|
||||
main_cpp = generate_main(CONFIG)
|
||||
body = _function_body(main_cpp, _run_function(main_cpp, "inline_restart"))
|
||||
body = _script_body(main_cpp, "inline_restart")
|
||||
assert "::inline_restart->execute();" in body
|
||||
stop = _automation(main_cpp, r"inline_restart->get_stop_trigger\(\)")
|
||||
(action,) = _actions_of(main_cpp, stop)
|
||||
@@ -92,18 +124,14 @@ def test_restart_executes_itself(generate_main) -> None:
|
||||
def test_parameters_by_value(generate_main) -> None:
|
||||
"""Lambdas take the parameters by value, so ``x *= 2`` compiles in every form."""
|
||||
main_cpp = generate_main(CONFIG)
|
||||
assert (
|
||||
_constructed(main_cpp, "with_params")
|
||||
== "script::SingleScript<int, std::string>()"
|
||||
)
|
||||
name = _run_function(main_cpp, "with_params")
|
||||
_assert_function_script(main_cpp, "SingleMode", "with_params", "int", "std::string")
|
||||
assert re.search(
|
||||
rf"static void {name}\(const int & esphome__running, "
|
||||
r"static void esphome__with_params__fn\(const int & esphome__running, "
|
||||
r"const std::remove_cvref_t<int> & x, "
|
||||
r"const std::remove_cvref_t<std::string> & s\)",
|
||||
main_cpp,
|
||||
)
|
||||
body = _function_body(main_cpp, name)
|
||||
body = _script_body(main_cpp, "with_params")
|
||||
assert body.count("[](int x, std::string s) -> void {") == 2
|
||||
assert body.count("[](int x, std::string s) -> int {") == 2
|
||||
assert "x *= 2;\n ESP_LOGI(" in body
|
||||
@@ -116,7 +144,7 @@ def test_parameters_by_value(generate_main) -> None:
|
||||
def test_script_stop_inside(generate_main) -> None:
|
||||
"""``script.stop`` without an id stops the script it is in; the next action checks."""
|
||||
main_cpp = generate_main(CONFIG)
|
||||
body = _function_body(main_cpp, _run_function(main_cpp, "stops_itself"))
|
||||
body = _script_body(main_cpp, "stops_itself")
|
||||
stop, rest = body.split("::stops_itself->stop();")
|
||||
assert '"STOPS start"' in stop
|
||||
assert rest.lstrip("\n }").startswith("if (!esphome__running)\n return;")
|
||||
@@ -126,35 +154,59 @@ def test_script_stop_inside(generate_main) -> None:
|
||||
def test_calls_itself_with_parameter(generate_main) -> None:
|
||||
"""A parallel script with a parameter can execute itself from an ``if``."""
|
||||
main_cpp = generate_main(CONFIG)
|
||||
assert _constructed(main_cpp, "calls_itself") == "script::ParallelScript<int>()"
|
||||
body = _function_body(main_cpp, _run_function(main_cpp, "calls_itself"))
|
||||
_assert_function_script(main_cpp, "ParallelMode", "calls_itself", "int")
|
||||
body = _script_body(main_cpp, "calls_itself")
|
||||
assert "return n > 0;" in body
|
||||
assert "::calls_itself->execute(std::move(esphome__arg_0));" in body
|
||||
|
||||
|
||||
def test_delay_keeps_separate_actions(generate_main) -> None:
|
||||
"""A delay keeps the Automation and its separate actions."""
|
||||
"""A delay keeps today's class, Automation and separate actions."""
|
||||
main_cpp = generate_main(CONFIG)
|
||||
assert _constructed(main_cpp, "with_delay") == "script::SingleScript<>()"
|
||||
assert "static script::SingleScript<> *const with_delay" in main_cpp
|
||||
before, delay, after = _actions_of(main_cpp, _automation(main_cpp, "with_delay"))
|
||||
assert _constructed(main_cpp, delay) == "DelayAction<>()"
|
||||
assert '"DELAY before"' in _constructed(main_cpp, before)
|
||||
assert '"DELAY after"' in _constructed(main_cpp, after)
|
||||
assert "esphome__with_delay__fn" not in main_cpp
|
||||
|
||||
|
||||
def test_manual_automation_id(generate_main) -> None:
|
||||
"""An automation_id set by hand names the Automation, so a lambda can reach it."""
|
||||
main_cpp = generate_main(CONFIG)
|
||||
assert _constructed(main_cpp, "manual_automation") == "script::SingleScript<>()"
|
||||
assert _automation(main_cpp, "manual_automation") == "manual_automation_id"
|
||||
(action,) = _actions_of(main_cpp, "manual_automation_id")
|
||||
assert '"MANUAL"' in _constructed(main_cpp, action)
|
||||
|
||||
|
||||
def test_script_wait(generate_main) -> None:
|
||||
"""``script.wait`` is a component templated on the class of the script it waits for."""
|
||||
"""``script.wait`` on a script that runs a function uses the class shared by its mode."""
|
||||
main_cpp = generate_main(CONFIG)
|
||||
(action,) = _actions_of(main_cpp, _automation(main_cpp, "waits"))
|
||||
assert (
|
||||
_constructed(main_cpp, action)
|
||||
== "script::ScriptWaitAction<script::SingleScript<>>(inline_single)"
|
||||
class_ = _wait_class(_mode_class("SingleMode"))
|
||||
assert _constructed(main_cpp, action) == f"{class_}(inline_single)"
|
||||
|
||||
|
||||
def test_script_wait_built_first(generate_main) -> None:
|
||||
"""A ``script.wait`` built before the script it waits for gets the shared class too."""
|
||||
main_cpp = generate_main(CONFIG)
|
||||
late, delay = _actions_of(main_cpp, _automation(main_cpp, "waits_early"))
|
||||
class_ = _wait_class(_mode_class("SingleMode"))
|
||||
assert _constructed(main_cpp, late) == f"{class_}(late_target)"
|
||||
assert f"static {class_} *const {late}" in main_cpp
|
||||
assert _constructed(main_cpp, delay) == (
|
||||
f"{_wait_class('script::SingleScript<>')}(with_delay)"
|
||||
)
|
||||
|
||||
|
||||
def test_script_wait_keeps_waiting_script_parameters(generate_main) -> None:
|
||||
"""Only the waited-for script's class changes, not a parameter with the same name."""
|
||||
main_cpp = generate_main(CONFIG)
|
||||
automation = _automation(main_cpp, "waits_with_param")
|
||||
wait, _ = _actions_of(main_cpp, automation)
|
||||
assert _constructed(main_cpp, wait) == (
|
||||
f"script::ScriptWaitAction<{_mode_class('SingleMode')}, "
|
||||
"script::SingleScript<>*>(late_target)"
|
||||
)
|
||||
@@ -11,6 +11,20 @@ globals:
|
||||
type: int
|
||||
|
||||
script:
|
||||
# Built before the scripts it waits for
|
||||
- id: waits_early
|
||||
then:
|
||||
- script.wait: late_target
|
||||
- script.wait: with_delay
|
||||
|
||||
# Has a parameter of the class an inline script had before it was known
|
||||
- id: waits_with_param
|
||||
parameters:
|
||||
other: script::SingleScript<>*
|
||||
then:
|
||||
- script.wait: late_target
|
||||
- lambda: other->execute();
|
||||
|
||||
# Inline actions only
|
||||
- id: inline_single
|
||||
then:
|
||||
@@ -103,3 +117,7 @@ script:
|
||||
- id: waits
|
||||
then:
|
||||
- script.wait: inline_single
|
||||
|
||||
- id: late_target
|
||||
then:
|
||||
- logger.log: "LATE target"
|
||||
@@ -1,15 +1,16 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include <functional>
|
||||
#include <utility>
|
||||
#include <memory>
|
||||
|
||||
#include "esphome/components/binary_sensor/automation.h"
|
||||
#include "esphome/components/binary_sensor/binary_sensor.h"
|
||||
#include "esphome/components/binary_sensor/filter.h"
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/base_automation.h"
|
||||
#include "esphome/core/hal.h"
|
||||
|
||||
namespace esphome::binary_sensor::testing {
|
||||
|
||||
// The click detectors read the real millis(), so every wait is at least 80 ms away from a window edge
|
||||
// The triggers read the real millis(), so every wait is at least 80 ms away from a window edge
|
||||
static constexpr uint32_t MIN_MS = 100;
|
||||
static constexpr uint32_t MAX_MS = 300;
|
||||
static constexpr uint32_t SHORT_MS = 20;
|
||||
@@ -23,24 +24,18 @@ class ClickTriggerTest : public ::testing::Test {
|
||||
void SetUp() override {
|
||||
fired = 0;
|
||||
this->sensor_.publish_state(false);
|
||||
this->automation_.add_action(&this->action_);
|
||||
}
|
||||
|
||||
// As codegen builds on_click and on_double_click
|
||||
void add_click_(uint32_t min_length = MIN_MS, uint32_t max_length = MAX_MS, BinarySensor *sensor = nullptr) {
|
||||
this->watch_(this->click_, min_length, max_length, sensor != nullptr ? *sensor : this->sensor_);
|
||||
this->click_ = std::make_unique<ClickTrigger>(sensor != nullptr ? sensor : &this->sensor_, min_length, max_length);
|
||||
this->click_->set_automation_parent(&this->automation_);
|
||||
}
|
||||
void add_double_click_(std::function<void()> on_fire = count_fire) {
|
||||
this->watch_(this->double_click_, MIN_MS, MAX_MS, this->sensor_, std::move(on_fire));
|
||||
void add_double_click_() {
|
||||
this->double_click_ = std::make_unique<DoubleClickTrigger>(&this->sensor_, MIN_MS, MAX_MS);
|
||||
this->double_click_->set_automation_parent(&this->automation_);
|
||||
}
|
||||
template<typename D>
|
||||
void watch_(D &detector, uint32_t min_length, uint32_t max_length, BinarySensor &sensor,
|
||||
std::function<void()> on_fire = count_fire) {
|
||||
sensor.add_on_state_callback([&detector, min_length, max_length, on_fire = std::move(on_fire)](bool state) {
|
||||
if (detector.check(state, min_length, max_length))
|
||||
on_fire();
|
||||
});
|
||||
}
|
||||
static void count_fire() { fired++; }
|
||||
|
||||
void press_for_(uint32_t length) {
|
||||
this->sensor_.publish_state(true);
|
||||
@@ -49,8 +44,10 @@ class ClickTriggerTest : public ::testing::Test {
|
||||
}
|
||||
|
||||
BinarySensor sensor_;
|
||||
ClickDetector click_;
|
||||
DoubleClickDetector double_click_;
|
||||
StatelessLambdaAction<> action_{[]() { fired++; }};
|
||||
Automation<> automation_;
|
||||
std::unique_ptr<ClickTrigger> click_;
|
||||
std::unique_ptr<DoubleClickTrigger> double_click_;
|
||||
};
|
||||
|
||||
TEST(ClickMatchInterval, BoundsAreInclusive) {
|
||||
@@ -201,23 +198,4 @@ TEST_F(ClickTriggerTest, DoubleClickDoesNotFireTwiceWithoutReset) {
|
||||
EXPECT_EQ(fired, 1);
|
||||
}
|
||||
|
||||
TEST_F(ClickTriggerTest, DoubleClickKeepsPressPublishedByItsActions) {
|
||||
// The times reset before the actions run, so a press the first action publishes is kept
|
||||
this->add_double_click_([this]() {
|
||||
if (++fired == 1) {
|
||||
this->sensor_.publish_state(false);
|
||||
this->sensor_.publish_state(true);
|
||||
}
|
||||
});
|
||||
this->press_for_(INSIDE_MS);
|
||||
delay(INSIDE_MS);
|
||||
this->sensor_.publish_state(true);
|
||||
EXPECT_EQ(fired, 1);
|
||||
delay(INSIDE_MS);
|
||||
this->sensor_.publish_state(false);
|
||||
delay(INSIDE_MS);
|
||||
this->sensor_.publish_state(true);
|
||||
EXPECT_EQ(fired, 2);
|
||||
}
|
||||
|
||||
} // namespace esphome::binary_sensor::testing
|
||||
@@ -0,0 +1,121 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "esphome/components/script/script.h"
|
||||
|
||||
namespace esphome::script::testing {
|
||||
|
||||
// Each body is the function of its script, as code generation emits it; the bodies reach
|
||||
// their script and record what happened through globals.
|
||||
// NOLINTBEGIN(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
static void single_body(const int &running);
|
||||
using TestSingle = FunctionScript<SingleMode<FunctionBody<>>, single_body>;
|
||||
static TestSingle *single_script;
|
||||
static int single_runs;
|
||||
static bool single_running_inside;
|
||||
static void single_body(const int &running) {
|
||||
single_runs++;
|
||||
single_running_inside = single_script->is_running();
|
||||
// Only once, so a script that is not running here fails the test instead of recursing
|
||||
if (single_runs == 1)
|
||||
single_script->execute();
|
||||
}
|
||||
|
||||
TEST(FunctionScriptTest, SingleIsRunningOnlyInsideItsBody) {
|
||||
TestSingle script;
|
||||
single_script = &script;
|
||||
single_runs = 0;
|
||||
script.execute();
|
||||
EXPECT_EQ(single_runs, 1);
|
||||
EXPECT_TRUE(single_running_inside);
|
||||
EXPECT_FALSE(script.is_running());
|
||||
}
|
||||
|
||||
static void stop_body(const int &running);
|
||||
using TestStop = FunctionScript<SingleMode<FunctionBody<>>, stop_body>;
|
||||
static TestStop *stop_script;
|
||||
static int stop_running_after;
|
||||
static void stop_body(const int &running) {
|
||||
stop_script->stop();
|
||||
stop_running_after = running;
|
||||
}
|
||||
|
||||
TEST(FunctionScriptTest, StopInsideClearsTheRunCount) {
|
||||
TestStop script;
|
||||
stop_script = &script;
|
||||
stop_running_after = -1;
|
||||
script.execute();
|
||||
EXPECT_EQ(stop_running_after, 0);
|
||||
EXPECT_FALSE(script.is_running());
|
||||
}
|
||||
|
||||
static void restart_body(const int &running);
|
||||
using TestRestart = FunctionScript<RestartMode<FunctionBody<>>, restart_body>;
|
||||
static TestRestart *restart_script;
|
||||
static int restart_starts;
|
||||
static int restart_ends;
|
||||
static void restart_body(const int &running) {
|
||||
if (restart_starts++ == 0)
|
||||
restart_script->execute();
|
||||
if (running)
|
||||
restart_ends++;
|
||||
}
|
||||
|
||||
TEST(FunctionScriptTest, RestartFromInsideStopsTheOuterRun) {
|
||||
TestRestart script;
|
||||
restart_script = &script;
|
||||
restart_starts = restart_ends = 0;
|
||||
script.execute();
|
||||
EXPECT_EQ(restart_starts, 2);
|
||||
EXPECT_EQ(restart_ends, 1);
|
||||
EXPECT_FALSE(script.is_running());
|
||||
}
|
||||
|
||||
static void parallel_body(const int &running, const int &n);
|
||||
using TestParallel = FunctionScript<ParallelMode<FunctionBody<int>, int>, parallel_body, int>;
|
||||
static TestParallel *parallel_script;
|
||||
static int parallel_runs;
|
||||
static void parallel_body(const int &running, const int &n) {
|
||||
parallel_runs++;
|
||||
if (n < 5)
|
||||
parallel_script->execute(n + 1);
|
||||
}
|
||||
|
||||
TEST(FunctionScriptTest, ParallelCountsNestedRuns) {
|
||||
TestParallel script;
|
||||
script.set_max_runs(2);
|
||||
parallel_script = &script;
|
||||
parallel_runs = 0;
|
||||
script.execute(1);
|
||||
EXPECT_EQ(parallel_runs, 2);
|
||||
EXPECT_FALSE(script.is_running());
|
||||
}
|
||||
|
||||
static void queued_body(const int &running, const int &n);
|
||||
using TestQueued = FunctionPointerScript<QueueingMode<FunctionBody<int>, int>, int>;
|
||||
// Through the base class, as external components keep scripts
|
||||
static Script<int> *queued_script;
|
||||
static int queued_last;
|
||||
static int queued_runs;
|
||||
static void queued_body(const int &running, const int &n) {
|
||||
queued_runs++;
|
||||
queued_last = n;
|
||||
if (n == 1) {
|
||||
queued_script->execute(2);
|
||||
queued_script->execute(3);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(FunctionScriptTest, QueuedRunsTheQueuedRunAfterTheBody) {
|
||||
TestQueued script(queued_body);
|
||||
script.set_max_runs(2);
|
||||
queued_script = &script;
|
||||
queued_runs = 0;
|
||||
script.execute(1);
|
||||
EXPECT_EQ(queued_runs, 2);
|
||||
EXPECT_EQ(queued_last, 2);
|
||||
EXPECT_FALSE(script.is_running());
|
||||
}
|
||||
|
||||
// NOLINTEND(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
} // namespace esphome::script::testing
|
||||
@@ -9,14 +9,14 @@ from esphome import automation
|
||||
from esphome.components.const import CONF_ON_STOP
|
||||
from esphome.components.script import (
|
||||
CONF_SCRIPT_STOP,
|
||||
SingleScript,
|
||||
CONFIG_SCHEMA,
|
||||
SCRIPT_MODES,
|
||||
default_script_stop_ids,
|
||||
to_code,
|
||||
)
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_MODE, CONF_PARAMETERS, CONF_THEN
|
||||
from esphome.core import ID
|
||||
from esphome.cpp_generator import MockObj
|
||||
|
||||
|
||||
def test_bare_script_stop_defaults_to_enclosing_script() -> None:
|
||||
@@ -103,58 +103,78 @@ async def test_on_stop_builds_stop_trigger_automation() -> None:
|
||||
stop_conf = {CONF_THEN: []}
|
||||
config = [
|
||||
{
|
||||
CONF_ID: ID("my_script", is_declaration=True, type=SingleScript),
|
||||
CONF_ID: ID("my_script", is_declaration=True, type=SCRIPT_MODES["single"]),
|
||||
CONF_MODE: "single",
|
||||
CONF_PARAMETERS: {},
|
||||
CONF_THEN: [],
|
||||
CONF_ON_STOP: stop_conf,
|
||||
},
|
||||
{
|
||||
CONF_ID: ID("other_script", is_declaration=True, type=SingleScript),
|
||||
CONF_ID: ID(
|
||||
"other_script", is_declaration=True, type=SCRIPT_MODES["single"]
|
||||
),
|
||||
CONF_MODE: "single",
|
||||
CONF_PARAMETERS: {},
|
||||
CONF_THEN: [],
|
||||
},
|
||||
]
|
||||
with (
|
||||
patch("esphome.codegen.new_Pvariable") as new_pvariable,
|
||||
patch("esphome.codegen.add"),
|
||||
patch("esphome.codegen.add_define") as add_define,
|
||||
patch(
|
||||
"esphome.automation.build_automation", new_callable=AsyncMock
|
||||
) as build_automation,
|
||||
patch(
|
||||
"esphome.automation.build_automation_or_run_function",
|
||||
new_callable=AsyncMock,
|
||||
return_value=None,
|
||||
) as build_or_run,
|
||||
):
|
||||
new_pvariable.side_effect = lambda id_, *args: MockObj(id_.id, "->")
|
||||
await to_code(config)
|
||||
|
||||
add_define.assert_called_once_with("USE_SCRIPT_ON_STOP")
|
||||
|
||||
assert [
|
||||
(str(c.args[0]), *c.args[1:]) for c in build_automation.await_args_list
|
||||
] == [
|
||||
assert [(str(c.args[0]), *c.args[1:3]) for c in build_or_run.await_args_list] == [
|
||||
("my_script", [], config[0]),
|
||||
("my_script->get_stop_trigger()", [], stop_conf),
|
||||
("other_script", [], config[1]),
|
||||
]
|
||||
assert [
|
||||
(str(c.args[0]), *c.args[1:]) for c in build_automation.await_args_list
|
||||
] == [("my_script->get_stop_trigger()", [], stop_conf)]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_no_on_stop_define_without_on_stop() -> None:
|
||||
config = [
|
||||
{
|
||||
CONF_ID: ID("my_script", is_declaration=True, type=SingleScript),
|
||||
CONF_ID: ID("my_script", is_declaration=True, type=SCRIPT_MODES["single"]),
|
||||
CONF_MODE: "single",
|
||||
CONF_PARAMETERS: {},
|
||||
CONF_THEN: [],
|
||||
}
|
||||
]
|
||||
with (
|
||||
patch("esphome.codegen.new_Pvariable") as new_pvariable,
|
||||
patch("esphome.codegen.add"),
|
||||
patch("esphome.codegen.add_define") as add_define,
|
||||
patch("esphome.automation.build_automation", new_callable=AsyncMock),
|
||||
patch(
|
||||
"esphome.automation.build_automation_or_run_function",
|
||||
new_callable=AsyncMock,
|
||||
return_value=None,
|
||||
),
|
||||
):
|
||||
new_pvariable.side_effect = lambda id_, *args: MockObj(id_.id, "->")
|
||||
await to_code(config)
|
||||
|
||||
add_define.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name", ["esphome__running", "esphome__arg_0"])
|
||||
def test_reserved_parameter_names_rejected(name: str) -> None:
|
||||
"""The reason reaches the user, at the parameter's path."""
|
||||
config = [{CONF_ID: "my_script", CONF_PARAMETERS: {name: "int"}, CONF_THEN: []}]
|
||||
with pytest.raises(cv.MultipleInvalid) as err:
|
||||
CONFIG_SCHEMA(config)
|
||||
(error,) = err.value.errors
|
||||
assert (
|
||||
error.msg == "Names starting with 'esphome__' are reserved for generated code"
|
||||
)
|
||||
assert error.path == [0, CONF_PARAMETERS, name]
|
||||
@@ -1595,7 +1595,11 @@ def apply_functions(monkeypatch: pytest.MonkeyPatch) -> list[tuple[str, str, str
|
||||
emitted: list[tuple[str, str, str]] = []
|
||||
|
||||
def fake_static_function(
|
||||
name: str, return_type: Any, parameters: list[tuple[Any, str]], body: list[str]
|
||||
name: str,
|
||||
return_type: Any,
|
||||
parameters: list[tuple[Any, str]],
|
||||
body: list[str],
|
||||
add_global_: Any = None,
|
||||
) -> RawExpression:
|
||||
assert str(return_type) == "void"
|
||||
params = ", ".join(f"{type_} {arg}" for type_, arg in parameters)
|
||||
|
||||
@@ -1173,6 +1173,32 @@ def test_static_function_declares_early_and_defines_after_includes() -> None:
|
||||
]
|
||||
|
||||
|
||||
def test_static_function_declaration_sink() -> None:
|
||||
declarations: list[cg.Statement] = []
|
||||
cg.static_function(
|
||||
"esphome__act__fn", ct.void, [], ["return;"], declarations.append
|
||||
)
|
||||
assert [str(st) for st in declarations] == ["static void esphome__act__fn();"]
|
||||
assert CORE.global_statements == []
|
||||
|
||||
|
||||
def test_late_pvariable_is_created_where_declared() -> None:
|
||||
"""A late variable is registered at once and emitted in its place, like new_Pvariable."""
|
||||
id_ = ID("late", is_declaration=True, type=ct.esphome_ns.class_("Late"))
|
||||
late = cg.LatePvariable(id_)
|
||||
assert CORE.variables[id_] is late.var
|
||||
CORE.add_global(cg.RawStatement("int after;"))
|
||||
cg.add(cg.RawStatement("after();"))
|
||||
late.global_slot.add(cg.RawStatement("int before_late;"))
|
||||
late.define(ct.esphome_ns.class_("Final"), cg.TemplateArguments(ct.int32), 5)
|
||||
assert str(id_.type) == "Final<int32_t>"
|
||||
globals_ = CORE.cpp_global_section
|
||||
assert globals_.index("Final<int32_t> *const late") < globals_.index("int after;")
|
||||
main = CORE.cpp_main_section
|
||||
assert main.index("new(late) Final<int32_t>(5);") < main.index("after();")
|
||||
assert cg.is_static_pointer(late.var)
|
||||
|
||||
|
||||
def test_static_function_accepts_statements() -> None:
|
||||
call = cg.MockObj("foo")(cg.MockObj("x") >> 2)
|
||||
cg.static_function(
|
||||
|
||||
Reference in new issue
Block a user