mirror of
https://github.com/esphome/esphome.git
synced 2026-11-15 09:36:04 +00:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5829740661 | ||
|
|
fc47adb83f | ||
|
|
ebce86fd9b | ||
|
|
d6a44e1b63 |
No files matched your search
+42
-21
@@ -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(
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user