Compare commits

..
10 changed files with 246 additions and 229 deletions

No files matched your search

+2 -9
View File
@@ -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
+42 -21
View File
@@ -1,6 +1,7 @@
from collections.abc import Awaitable, Callable, Sequence
from contextvars import ContextVar
from dataclasses import dataclass, field
from functools import partial
import logging
import math
import re
@@ -1151,9 +1152,6 @@ IfAction = cg.esphome_ns.class_("IfAction", Action)
WhileAction = cg.esphome_ns.class_("WhileAction", Action)
RepeatAction = cg.esphome_ns.class_("RepeatAction", Action)
WaitUntilAction = cg.esphome_ns.class_("WaitUntilAction", Action, cg.Component)
UpdateComponentAction = cg.esphome_ns.class_("UpdateComponentAction", Action)
SuspendComponentAction = cg.esphome_ns.class_("SuspendComponentAction", Action)
ResumeComponentAction = cg.esphome_ns.class_("ResumeComponentAction", Action)
Automation = cg.esphome_ns.class_("Automation")
TriggerForwarder = cg.esphome_ns.class_("TriggerForwarder")
TriggerOnTrueForwarder = cg.esphome_ns.class_("TriggerOnTrueForwarder")
@@ -1771,25 +1769,44 @@ async def lambda_action_to_code(
return new_lambda_pvariable(action_id, lambda_, StatelessLambdaAction, template_arg)
register_simple_action(
"component.update",
UpdateComponentAction,
maybe_simple_id({cv.Required(CONF_ID): cv.use_id(cg.PollingComponent)}),
synchronous=True,
_POLLING_COMPONENT_SCHEMA = maybe_simple_id(
{cv.Required(CONF_ID): cv.use_id(cg.PollingComponent)}
)
register_simple_action(
"component.suspend",
SuspendComponentAction,
maybe_simple_id({cv.Required(CONF_ID): cv.use_id(cg.PollingComponent)}),
synchronous=True,
)
async def _polling_component_action(
config: ConfigType,
action_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
calls: list[str],
) -> MockObj:
"""Run ``calls`` on the component once it is ready, through ``PollingComponent``.
An if block, not an early return, so actions merged after this one still run.
"""
parent = await _apply_parent(config)
statements = [
f"{cg.PollingComponent} &esphome__component = *{parent};",
"if (esphome__component.is_ready()) {",
*(f" esphome__component.{call};" for call in calls),
"}",
]
return apply_statements_action(action_id, template_arg, args, statements)
for _name, _call in (
("component.update", "update()"),
("component.suspend", "stop_poller()"),
):
register_action(_name, ApplyAction, _POLLING_COMPONENT_SCHEMA, synchronous=True)(
partial(_polling_component_action, calls=[_call])
)
@register_action(
"component.resume",
ResumeComponentAction,
ApplyAction,
maybe_simple_id(
{
cv.Required(CONF_ID): cv.use_id(cg.PollingComponent),
@@ -1806,12 +1823,16 @@ async def component_resume_action_to_code(
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
comp = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, comp)
if CONF_UPDATE_INTERVAL in config:
template_ = await cg.templatable(config[CONF_UPDATE_INTERVAL], args, cg.uint32)
cg.add(var.set_update_interval(template_))
return var
calls: list[str] = []
if (interval := config.get(CONF_UPDATE_INTERVAL)) is not None:
if isinstance(interval, Lambda):
# Built with the trigger's own argument types and always called, so the body
# may modify its by-value arguments
lamb = await cg.process_lambda(interval, args, return_type=cg.uint32)
interval = CallExpression(lamb, *(MockObj(name) for _, name in args))
calls.append(f"set_update_interval({cg.safe_exp(interval)})")
calls.append("start_poller()")
return await _polling_component_action(config, action_id, template_arg, args, calls)
async def build_action(
+47 -49
View File
@@ -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
+40 -11
View File
@@ -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 {
-48
View File
@@ -595,52 +595,4 @@ template<typename... Ts> class WaitUntilAction : public Action<Ts...>, public Co
uint8_t stop_generation_{0};
};
template<typename... Ts> class UpdateComponentAction : public Action<Ts...> {
public:
UpdateComponentAction(PollingComponent *component) : component_(component) {}
void play(const Ts &...x) override {
if (!this->component_->is_ready())
return;
this->component_->update();
}
protected:
PollingComponent *component_;
};
template<typename... Ts> class SuspendComponentAction : public Action<Ts...> {
public:
SuspendComponentAction(PollingComponent *component) : component_(component) {}
void play(const Ts &...x) override {
if (!this->component_->is_ready())
return;
this->component_->stop_poller();
}
protected:
PollingComponent *component_;
};
template<typename... Ts> class ResumeComponentAction : public Action<Ts...> {
public:
ResumeComponentAction(PollingComponent *component) : component_(component) {}
TEMPLATABLE_VALUE(uint32_t, update_interval)
void play(const Ts &...x) override {
if (!this->component_->is_ready()) {
return;
}
optional<uint32_t> update_interval = this->update_interval_.optional_value(x...);
if (update_interval.has_value()) {
this->component_->set_update_interval(update_interval.value());
}
this->component_->start_poller();
}
protected:
PollingComponent *component_;
};
} // namespace esphome
@@ -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
@@ -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
+93 -17
View File
@@ -2441,16 +2441,25 @@ def polling_component() -> None:
@pytest.mark.asyncio
@pytest.mark.usefixtures("polling_component")
@pytest.mark.parametrize(
"name",
["component.update", "component.suspend", "component.resume"],
("name", "call"),
[
("component.update", "update()"),
("component.suspend", "stop_poller()"),
("component.resume", "start_poller()"),
],
)
async def test_component_action_codegen(name: str) -> None:
"""Each action is an object constructed with the component, with no interval set."""
async def test_component_action_codegen(name: str, call: str) -> None:
"""The call goes through ``PollingComponent`` and only once the component is ready."""
await build_action(_component_action(name), TEMPLATE_ARG, [])
class_name = str(ACTION_REGISTRY[name].type_id)
assert f"new(act) {class_name}<>(comp);" in CORE.cpp_main_section
assert "set_update_interval" not in CORE.cpp_main_section
assert "new(act) ApplyAction<esphome__act__fn>();" in CORE.cpp_main_section
assert (
" PollingComponent &esphome__component = *::comp;\n"
" if (esphome__component.is_ready()) {\n"
f" esphome__component.{call};\n"
" }\n"
) in CORE.cpp_global_section
assert "set_update_interval" not in CORE.cpp_global_section
@pytest.mark.asyncio
@@ -2466,14 +2475,17 @@ async def test_component_resume_constant_interval_codegen() -> None:
)
assert (
"act->set_update_interval([]() -> uint32_t {\n return 250;\n});"
) in CORE.cpp_main_section
" if (esphome__component.is_ready()) {\n"
" esphome__component.set_update_interval(250);\n"
" esphome__component.start_poller();\n"
" }\n"
) in CORE.cpp_global_section
@pytest.mark.asyncio
@pytest.mark.usefixtures("polling_component")
async def test_component_resume_interval_lambda_codegen() -> None:
"""A templated interval is a lambda over the trigger args."""
"""A templated interval is computed from the trigger args, only once ready."""
await build_action(
_component_action(
"component.resume", {CONF_UPDATE_INTERVAL: Lambda("return x * 1000;")}
@@ -2482,27 +2494,91 @@ async def test_component_resume_interval_lambda_codegen() -> None:
[(cg.uint32, "x")],
)
main = CORE.cpp_main_section
assert "new(act) ResumeComponentAction<uint32_t>(comp);" in main
assert (
"act->set_update_interval([](uint32_t x) -> uint32_t {\n"
"static void esphome__act__fn(const std::remove_cvref_t<uint32_t> & x) {\n"
" PollingComponent &esphome__component = *::comp;\n"
" if (esphome__component.is_ready()) {\n"
" esphome__component.set_update_interval([](uint32_t x) -> uint32_t {\n"
" return x * 1000;\n"
"});"
) in main
" }(x));\n"
" esphome__component.start_poller();\n"
" }\n"
) in CORE.cpp_global_section
@pytest.mark.asyncio
@pytest.mark.usefixtures("apply_actions")
async def test_component_update_not_ready_still_runs_merged_actions() -> None:
"""The ready check skips only the update, never the actions merged after it."""
cg.new_Pvariable(ID("pcomp", is_declaration=True, type=cg.PollingComponent))
update = _component_action("component.update")
update["component.update"][CONF_ID] = ID("pcomp", type=cg.PollingComponent)
result = await build_action_list(
[update, _action("my.a", "act_a")], TEMPLATE_ARG, []
)
assert [str(action) for action in result] == ["act"]
text = CORE.cpp_global_section
assert text.index("esphome__component.update();") < text.index("::comp->do_a();")
# The only return is the stop check between the two actions
assert text.count("return;") == 1
@pytest.mark.asyncio
@pytest.mark.usefixtures("apply_actions")
async def test_callback_automation_if_with_component_update_is_direct() -> None:
"""An ``if`` whose branches update components becomes plain C++ in the callback."""
cg.new_Pvariable(ID("pcomp", is_declaration=True, type=cg.PollingComponent))
def update(id_: str) -> ConfigType:
return {
"component.update": {CONF_ID: ID("pcomp", type=cg.PollingComponent)},
CONF_TYPE_ID: ID(
id_, is_declaration=True, type=ApplyAction, is_manual=False
),
}
conf = _automation_conf(
{
"if": {
CONF_CONDITION: _inline_condition(),
CONF_THEN: [update("upd_then")],
CONF_ELSE: [update("upd_else")],
},
CONF_TYPE_ID: ID(
"act_if", is_declaration=True, type=IfAction, is_manual=False
),
}
)
await build_callback_automation(CALLBACK_PARENT, "add_cb", [], conf)
assert "Action" not in CORE.cpp_main_section
assert "Condition" not in CORE.cpp_main_section
text = CORE.cpp_global_section
assert "if (esphome__condition) {" in text
assert text.count("esphome__component.update();") == 2
@pytest.mark.asyncio
@pytest.mark.usefixtures("polling_component")
@pytest.mark.parametrize("body", ["x *= 1000; return x;", "return x *= 1000;"])
async def test_component_resume_interval_lambda_takes_args_by_value(body: str) -> None:
"""The user's interval lambda gets the trigger args by value, so it may modify them."""
"""The user's interval lambda gets the trigger args by value, so it may modify them.
It is called rather than reduced, so even a single return modifies only its own copy.
"""
await build_action(
_component_action("component.resume", {CONF_UPDATE_INTERVAL: Lambda(body)}),
cg.TemplateArguments(cg.uint32),
[(cg.uint32, "x")],
)
assert f"[](uint32_t x) -> uint32_t {{\n {body}\n}}" in CORE.cpp_main_section
assert (
f"set_update_interval([](uint32_t x) -> uint32_t {{\n {body}\n }}(x));"
in CORE.cpp_global_section
)
COMPONENT_ID = ID("comp", type=cg.Component)