mirror of
https://github.com/esphome/esphome.git
synced 2026-11-15 01:25:57 +00:00
Compare commits
38
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5849d87a07 | ||
|
|
113eb659d5 | ||
|
|
c7b321eba0 | ||
|
|
a9ae0380cb | ||
|
|
0bf9e7681a | ||
|
|
9d65f4fe20 | ||
|
|
a48151a993 | ||
|
|
3482d33176 | ||
|
|
6ecd846999 | ||
|
|
4912a88528 | ||
|
|
6eb97bf7ee | ||
|
|
f41f1c711a | ||
|
|
475d794012 | ||
|
|
42ea2a2821 | ||
|
|
e0e25ac7cc | ||
|
|
948a9eeb27 | ||
|
|
65a83c12b6 | ||
|
|
7d8b9c31b2 | ||
|
|
d9d8518171 | ||
|
|
e6f6c0f09b | ||
|
|
7815874624 | ||
|
|
d9082378c5 | ||
|
|
95f7423424 | ||
|
|
fef7a15521 | ||
|
|
0b6fc2c3c5 | ||
|
|
15810c176c | ||
|
|
436baa9375 | ||
|
|
4705fbb703 | ||
|
|
ce70abbbb4 | ||
|
|
6b5ae000d6 | ||
|
|
2cfd2a8f51 | ||
|
|
77574a50eb | ||
|
|
c52beb6d5e | ||
|
|
dde3aaf50e | ||
|
|
6f7af22b41 | ||
|
|
de0bf34e84 | ||
|
|
7bd7547983 | ||
|
|
aae52f2265 |
No files matched your search
+68
-44
@@ -1,11 +1,11 @@
|
||||
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
|
||||
import string
|
||||
import struct
|
||||
from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
@@ -1152,6 +1152,9 @@ 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")
|
||||
@@ -1769,44 +1772,25 @@ async def lambda_action_to_code(
|
||||
return new_lambda_pvariable(action_id, lambda_, StatelessLambdaAction, template_arg)
|
||||
|
||||
|
||||
_POLLING_COMPONENT_SCHEMA = maybe_simple_id(
|
||||
{cv.Required(CONF_ID): cv.use_id(cg.PollingComponent)}
|
||||
register_simple_action(
|
||||
"component.update",
|
||||
UpdateComponentAction,
|
||||
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_simple_action(
|
||||
"component.suspend",
|
||||
SuspendComponentAction,
|
||||
maybe_simple_id({cv.Required(CONF_ID): cv.use_id(cg.PollingComponent)}),
|
||||
synchronous=True,
|
||||
)
|
||||
|
||||
|
||||
@register_action(
|
||||
"component.resume",
|
||||
ApplyAction,
|
||||
ResumeComponentAction,
|
||||
maybe_simple_id(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.use_id(cg.PollingComponent),
|
||||
@@ -1823,16 +1807,12 @@ async def component_resume_action_to_code(
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
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)
|
||||
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
|
||||
|
||||
|
||||
async def build_action(
|
||||
@@ -2144,6 +2124,17 @@ def _direct_function(
|
||||
)
|
||||
|
||||
|
||||
def _call_target(
|
||||
plan: list[MockObj | list[_InlineAction]],
|
||||
args: TemplateArgsType,
|
||||
config: ConfigType,
|
||||
) -> MockObj | str:
|
||||
"""What a generated callback calls: the direct function, else ``<automation>->trigger``."""
|
||||
if (target := _direct_function(plan, args, config)) is None:
|
||||
target = parent_ref(_automation_from_plan(plan, args, config)).trigger
|
||||
return target
|
||||
|
||||
|
||||
def _when_call(
|
||||
when: str | ApplyCall | None, parent: MockObj | None
|
||||
) -> ApplyCall | None:
|
||||
@@ -2161,6 +2152,40 @@ async def build_automation(
|
||||
return await new_automation(args, config, trigger)
|
||||
|
||||
|
||||
async def build_component_automation(
|
||||
config: ConfigType, component_class: MockObjClass, priority: float | None = None
|
||||
) -> MockObj:
|
||||
"""Create and register the Component that runs a no-argument automation.
|
||||
|
||||
That is ``component_class<function[, priority bits]>``, whose function runs the actions
|
||||
directly when they are all inline, else triggers their Automation. A ``trigger_id`` set
|
||||
by hand keeps its Trigger, built with ``priority`` if given.
|
||||
"""
|
||||
trigger_id: ID = config[CONF_TRIGGER_ID]
|
||||
if trigger_id.is_manual:
|
||||
var = cg.new_Pvariable(trigger_id, *([] if priority is None else [priority]))
|
||||
await cg.register_component(var, config)
|
||||
await build_automation(var, [], config)
|
||||
return var
|
||||
# Declared and registered before the actions are built, which can wait for other ids
|
||||
name = _apply_function_name(trigger_id)
|
||||
define = cg.declare_static_function(name, cg.void, [])
|
||||
id_ = trigger_id.copy()
|
||||
id_.type = component_class
|
||||
bits = [] if priority is None else [_float_bits(priority)]
|
||||
var = cg.new_Pvariable(id_, cg.TemplateArguments(cg.RawExpression(name), *bits))
|
||||
await cg.register_component(var, config)
|
||||
plan = await _build_automation_plan([], config)
|
||||
define([str(cg.statement(CallExpression(_call_target(plan, [], config))))])
|
||||
return var
|
||||
|
||||
|
||||
def _float_bits(value: float) -> HexInt:
|
||||
"""``value`` as a C++ float's bit pattern, for a template argument."""
|
||||
(bits,) = struct.unpack("<I", struct.pack("<f", value))
|
||||
return HexInt(bits)
|
||||
|
||||
|
||||
async def build_trigger_callback(
|
||||
args: TemplateArgsType,
|
||||
config: ConfigType,
|
||||
@@ -2194,8 +2219,7 @@ async def _trigger_callback(
|
||||
call: ApplyCall | None,
|
||||
parent: MockObj | None,
|
||||
) -> LambdaExpression:
|
||||
if (target := _direct_function(plan, args, config)) is None:
|
||||
target = f"{parent_ref(_automation_from_plan(plan, args, config))}->trigger"
|
||||
target = _call_target(plan, args, config)
|
||||
statements: list[str] = []
|
||||
if call is not None:
|
||||
members = call.members
|
||||
|
||||
@@ -31,6 +31,7 @@ from esphome.cpp_generator import ( # noqa: F401
|
||||
add_global,
|
||||
add_library,
|
||||
add_platformio_option,
|
||||
declare_static_function,
|
||||
extern_progmem_array,
|
||||
get_variable,
|
||||
get_variable_with_full_id,
|
||||
|
||||
@@ -171,6 +171,41 @@ class LoopTrigger : public Trigger<>, public Component {
|
||||
float get_setup_priority() const override { return setup_priority::DATA; }
|
||||
};
|
||||
|
||||
/// Component whose setup priority is the float with bit pattern PRIORITY_BITS, since GCC 10 (ESP8266)
|
||||
/// has no float template parameters. Templated on the priority only, so each priority has one copy.
|
||||
template<uint32_t PRIORITY_BITS> class PriorityComponent : public Component {
|
||||
public:
|
||||
float get_setup_priority() const override { return bit_cast<float>(PRIORITY_BITS); }
|
||||
};
|
||||
|
||||
/// Bit pattern of setup_priority::DATA (600.0f), the Component default.
|
||||
static constexpr uint32_t SETUP_PRIORITY_DATA_BITS = 0x44160000;
|
||||
// bit_cast is constexpr only where std::bit_cast exists, which GCC 10 (ESP8266) lacks
|
||||
#if __cpp_lib_bit_cast >= 201806
|
||||
static_assert(bit_cast<uint32_t>(setup_priority::DATA) == SETUP_PRIORITY_DATA_BITS);
|
||||
#endif
|
||||
|
||||
/// The default priority needs no override.
|
||||
template<> class PriorityComponent<SETUP_PRIORITY_DATA_BITS> : public Component {};
|
||||
|
||||
/// Calls Fn from setup(), for on_boot.
|
||||
template<auto Fn, uint32_t PRIORITY_BITS> class StartupCallback final : public PriorityComponent<PRIORITY_BITS> {
|
||||
public:
|
||||
void setup() override { Fn(); }
|
||||
};
|
||||
|
||||
/// Calls Fn from on_shutdown(), for on_shutdown.
|
||||
template<auto Fn, uint32_t PRIORITY_BITS> class ShutdownCallback final : public PriorityComponent<PRIORITY_BITS> {
|
||||
public:
|
||||
void on_shutdown() override { Fn(); }
|
||||
};
|
||||
|
||||
/// Calls Fn from loop(), for on_loop.
|
||||
template<auto Fn> class LoopCallback final : public Component {
|
||||
public:
|
||||
void loop() override { Fn(); }
|
||||
};
|
||||
|
||||
#ifdef ESPHOME_PROJECT_NAME
|
||||
class ProjectUpdateTrigger : public Trigger<std::string>, public Component {
|
||||
public:
|
||||
@@ -595,4 +630,52 @@ 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
|
||||
+12
-14
@@ -124,6 +124,9 @@ ShutdownTrigger = cg.esphome_ns.class_(
|
||||
LoopTrigger = cg.esphome_ns.class_(
|
||||
"LoopTrigger", cg.Component, automation.Trigger.template()
|
||||
)
|
||||
StartupCallback = cg.esphome_ns.class_("StartupCallback", cg.Component)
|
||||
ShutdownCallback = cg.esphome_ns.class_("ShutdownCallback", cg.Component)
|
||||
LoopCallback = cg.esphome_ns.class_("LoopCallback", cg.Component)
|
||||
ProjectUpdateTrigger = cg.esphome_ns.class_(
|
||||
"ProjectUpdateTrigger", cg.Component, automation.Trigger.template(cg.std_string)
|
||||
)
|
||||
@@ -785,20 +788,15 @@ async def _add_environment_variables(env_vars: dict[str, str]) -> None:
|
||||
|
||||
@coroutine_with_priority(CoroPriority.AUTOMATION)
|
||||
async def _add_automations(config):
|
||||
for conf in config.get(CONF_ON_BOOT, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], conf.get(CONF_PRIORITY))
|
||||
await cg.register_component(trigger, conf)
|
||||
await automation.build_automation(trigger, [], conf)
|
||||
|
||||
for conf in config.get(CONF_ON_SHUTDOWN, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], conf.get(CONF_PRIORITY))
|
||||
await cg.register_component(trigger, conf)
|
||||
await automation.build_automation(trigger, [], conf)
|
||||
|
||||
for conf in config.get(CONF_ON_LOOP, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID])
|
||||
await cg.register_component(trigger, conf)
|
||||
await automation.build_automation(trigger, [], conf)
|
||||
for key, component_class in (
|
||||
(CONF_ON_BOOT, StartupCallback),
|
||||
(CONF_ON_SHUTDOWN, ShutdownCallback),
|
||||
(CONF_ON_LOOP, LoopCallback),
|
||||
):
|
||||
for conf in config.get(key, []):
|
||||
await automation.build_component_automation(
|
||||
conf, component_class, conf.get(CONF_PRIORITY)
|
||||
)
|
||||
|
||||
|
||||
# Datetime component has special subtypes that need additional defines
|
||||
|
||||
@@ -883,14 +883,13 @@ def new_Pvariable(id_: ID, *args: SafeExpType) -> "MockObj":
|
||||
return Pvariable(id_, rhs)
|
||||
|
||||
|
||||
def static_function(
|
||||
def declare_static_function(
|
||||
name: str,
|
||||
return_type: SafeExpType,
|
||||
parameters: TemplateArgsType,
|
||||
body: list[str | Statement],
|
||||
) -> 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.
|
||||
) -> Callable[[list[str | Statement]], RawExpression]:
|
||||
"""Declare ``static <return_type> <name>(parameters)`` now and return the function that
|
||||
emits its body, for a caller that names the function before the body is known.
|
||||
|
||||
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
|
||||
@@ -900,10 +899,24 @@ def static_function(
|
||||
f"static {safe_exp(return_type)} {name}({ParameterListExpression(*parameters)})"
|
||||
)
|
||||
add_global(RawStatement(f"{signature};"))
|
||||
add_global_after_includes(
|
||||
RawStatement(f"{signature} {{\n{indent(chr(10).join(map(str, body)))}\n}}")
|
||||
)
|
||||
return RawExpression(name)
|
||||
|
||||
def define(body: list[str | Statement]) -> RawExpression:
|
||||
add_global_after_includes(
|
||||
RawStatement(f"{signature} {{\n{indent(chr(10).join(map(str, body)))}\n}}")
|
||||
)
|
||||
return RawExpression(name)
|
||||
|
||||
return define
|
||||
|
||||
|
||||
def static_function(
|
||||
name: str,
|
||||
return_type: SafeExpType,
|
||||
parameters: TemplateArgsType,
|
||||
body: list[str | Statement],
|
||||
) -> RawExpression:
|
||||
"""``declare_static_function`` with the body known at once."""
|
||||
return declare_static_function(name, return_type, parameters)(body)
|
||||
|
||||
|
||||
@dataclass
|
||||
|
||||
@@ -30,14 +30,6 @@ def _automation_of(main_cpp: str, trigger: str) -> str:
|
||||
return match.group(1)
|
||||
|
||||
|
||||
def _has_delay(main_cpp: str, trigger: str) -> bool:
|
||||
automation = _automation_of(main_cpp, trigger)
|
||||
return (
|
||||
re.search(rf"{automation}->add_action\(delayaction_id\w*\);", main_cpp)
|
||||
is not None
|
||||
)
|
||||
|
||||
|
||||
def _registered(main_cpp: str) -> list[str]:
|
||||
return re.findall(r"App\.register_component_\((\w+), \d+\);", main_cpp)
|
||||
|
||||
@@ -56,18 +48,59 @@ def test_core_automations_registration_order(main_cpp: str) -> None:
|
||||
]
|
||||
|
||||
|
||||
def _function_body(main_cpp: str, name: str) -> str:
|
||||
match = re.search(rf"static void {name}\(\) \{{(.*?)\n\}}\n", main_cpp, re.DOTALL)
|
||||
assert match is not None, name
|
||||
return match.group(1)
|
||||
|
||||
|
||||
def _actions_body(main_cpp: str, function: str) -> str:
|
||||
"""The body of the function of inline actions that ``function`` calls."""
|
||||
match = re.fullmatch(r"\n (\w+)\(\);", _function_body(main_cpp, function))
|
||||
assert match is not None, function
|
||||
return _function_body(main_cpp, match.group(1))
|
||||
|
||||
|
||||
def _delayed_automation(main_cpp: str, function: str) -> str:
|
||||
"""The Automation that ``function`` triggers, checked to hold a delay; found through the
|
||||
function rather than an id counter."""
|
||||
match = re.fullmatch(
|
||||
r"\n ::(\w+)->trigger\(\);", _function_body(main_cpp, function)
|
||||
)
|
||||
assert match is not None, function
|
||||
automation = match.group(1)
|
||||
assert f"new({automation}) Automation<>();" in main_cpp
|
||||
assert re.search(rf"{automation}->add_action\(delayaction_id\w*\);", main_cpp)
|
||||
return automation
|
||||
|
||||
|
||||
def test_on_boot_inline_priorities(main_cpp: str) -> None:
|
||||
"""All-inline on_boot blocks keep their configured priority, default 600."""
|
||||
assert _constructed(main_cpp, "startuptrigger_id") == "StartupTrigger(800.0f)"
|
||||
assert _constructed(main_cpp, "startuptrigger_id_2") == "StartupTrigger(600.0f)"
|
||||
_automation_of(main_cpp, "startuptrigger_id")
|
||||
_automation_of(main_cpp, "startuptrigger_id_2")
|
||||
"""All-inline on_boot blocks call one function from setup() at their priority, default 600.
|
||||
|
||||
The priority is the float's bit pattern: 0x44480000 is 800.0f, 0x44160000 is 600.0f.
|
||||
"""
|
||||
assert _constructed(main_cpp, "startuptrigger_id") == (
|
||||
"StartupCallback<esphome__startuptrigger_id__fn, 0x44480000>()"
|
||||
)
|
||||
assert _constructed(main_cpp, "startuptrigger_id_2") == (
|
||||
"StartupCallback<esphome__startuptrigger_id_2__fn, 0x44160000>()"
|
||||
)
|
||||
assert "Automation<>(startuptrigger_id" not in main_cpp
|
||||
# Both actions of the first block run in the one function, with no stop checks
|
||||
body = _actions_body(main_cpp, "esphome__startuptrigger_id__fn")
|
||||
assert "boot high" in body
|
||||
assert "boot high lambda" in body
|
||||
assert "esphome__running" not in body
|
||||
assert "Automation" not in body
|
||||
assert "boot default" in _actions_body(main_cpp, "esphome__startuptrigger_id_2__fn")
|
||||
|
||||
|
||||
def test_on_boot_delay_keeps_trigger(main_cpp: str) -> None:
|
||||
"""A delay needs the Trigger and its Automation of separate actions."""
|
||||
assert _constructed(main_cpp, "startuptrigger_id_3") == "StartupTrigger(-100.0f)"
|
||||
assert _has_delay(main_cpp, "startuptrigger_id_3")
|
||||
def test_on_boot_delay_triggers_automation(main_cpp: str) -> None:
|
||||
"""A delay keeps the Automation of separate actions, which the function triggers."""
|
||||
assert _constructed(main_cpp, "startuptrigger_id_3") == (
|
||||
"StartupCallback<esphome__startuptrigger_id_3__fn, 0xC2C80000>()"
|
||||
)
|
||||
_delayed_automation(main_cpp, "esphome__startuptrigger_id_3__fn")
|
||||
|
||||
|
||||
def test_on_boot_manual_trigger_id_keeps_trigger(main_cpp: str) -> None:
|
||||
@@ -77,19 +110,30 @@ def test_on_boot_manual_trigger_id_keeps_trigger(main_cpp: str) -> None:
|
||||
|
||||
|
||||
def test_on_shutdown_priorities(main_cpp: str) -> None:
|
||||
"""on_shutdown blocks keep their priority; a delay keeps the Trigger."""
|
||||
assert _constructed(main_cpp, "shutdowntrigger_id") == "ShutdownTrigger(700.0f)"
|
||||
_automation_of(main_cpp, "shutdowntrigger_id")
|
||||
assert _constructed(main_cpp, "shutdowntrigger_id_2") == "ShutdownTrigger(600.0f)"
|
||||
assert _has_delay(main_cpp, "shutdowntrigger_id_2")
|
||||
"""on_shutdown calls its function at its priority; a delay keeps the Automation."""
|
||||
assert _constructed(main_cpp, "shutdowntrigger_id") == (
|
||||
"ShutdownCallback<esphome__shutdowntrigger_id__fn, 0x442F0000>()"
|
||||
)
|
||||
assert "shutdown inline" in _actions_body(
|
||||
main_cpp, "esphome__shutdowntrigger_id__fn"
|
||||
)
|
||||
assert _constructed(main_cpp, "shutdowntrigger_id_2") == (
|
||||
"ShutdownCallback<esphome__shutdowntrigger_id_2__fn, 0x44160000>()"
|
||||
)
|
||||
_delayed_automation(main_cpp, "esphome__shutdowntrigger_id_2__fn")
|
||||
|
||||
|
||||
def test_on_loop(main_cpp: str) -> None:
|
||||
"""on_loop blocks are looping components; a delay keeps the Trigger."""
|
||||
assert _constructed(main_cpp, "looptrigger_id") == "LoopTrigger()"
|
||||
_automation_of(main_cpp, "looptrigger_id")
|
||||
assert _constructed(main_cpp, "looptrigger_id_2") == "LoopTrigger()"
|
||||
assert _has_delay(main_cpp, "looptrigger_id_2")
|
||||
"""on_loop calls its function each loop; a delay keeps the Automation."""
|
||||
assert _constructed(main_cpp, "looptrigger_id") == (
|
||||
"LoopCallback<esphome__looptrigger_id__fn>()"
|
||||
)
|
||||
assert "loop inline" in _actions_body(main_cpp, "esphome__looptrigger_id__fn")
|
||||
assert "HasLoopOverride<LoopCallback<esphome__looptrigger_id__fn>>" in main_cpp
|
||||
assert _constructed(main_cpp, "looptrigger_id_2") == (
|
||||
"LoopCallback<esphome__looptrigger_id_2__fn>()"
|
||||
)
|
||||
_delayed_automation(main_cpp, "esphome__looptrigger_id_2__fn")
|
||||
|
||||
|
||||
def test_on_boot_registered_before_its_actions_are_built(
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: nodemcu-32s
|
||||
|
||||
logger:
|
||||
|
||||
interval:
|
||||
- interval: 1s
|
||||
then:
|
||||
- logger.log: inline tick
|
||||
- lambda: ESP_LOGD("test", "inline lambda");
|
||||
- interval: 2s
|
||||
startup_delay: 5s
|
||||
then:
|
||||
- logger.log: delayed start tick
|
||||
- interval: 3s
|
||||
then:
|
||||
- logger.log: before delay
|
||||
- delay: 10ms
|
||||
- logger.log: after delay
|
||||
- id: named_interval
|
||||
interval: 4s
|
||||
startup_delay: 3s
|
||||
then:
|
||||
- logger.log: named tick
|
||||
- interval: 5s
|
||||
setup_priority: 250
|
||||
then:
|
||||
# The script is declared later, so building this action waits for it
|
||||
- script.execute: later_script
|
||||
|
||||
script:
|
||||
- id: later_script
|
||||
then:
|
||||
- logger.log: script ran
|
||||
|
||||
sensor:
|
||||
- platform: template
|
||||
id: some_sensor
|
||||
lambda: return 1.0;
|
||||
@@ -0,0 +1,72 @@
|
||||
"""Tests for the code generated for interval blocks."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
import re
|
||||
|
||||
import pytest
|
||||
|
||||
CONFIG = "interval_codegen.yaml"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def main_cpp(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> str:
|
||||
return generate_main(component_config_path(CONFIG))
|
||||
|
||||
|
||||
def _constructed(main_cpp: str, var: str) -> str:
|
||||
match = re.search(rf"new\({var}\) ([^;]+);", main_cpp)
|
||||
assert match is not None, var
|
||||
return match.group(1)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("var", "expected", "update_interval"),
|
||||
[
|
||||
("interval_intervaltrigger_id", "interval::IntervalTrigger()", 1000),
|
||||
(
|
||||
"interval_intervaltrigger_id_2",
|
||||
"interval::DelayedIntervalTrigger(5000)",
|
||||
2000,
|
||||
),
|
||||
("interval_intervaltrigger_id_3", "interval::IntervalTrigger()", 3000),
|
||||
("named_interval", "interval::DelayedIntervalTrigger(3000)", 4000),
|
||||
("interval_intervaltrigger_id_4", "interval::IntervalTrigger()", 5000),
|
||||
],
|
||||
)
|
||||
def test_interval_constructed(
|
||||
main_cpp: str, var: str, expected: str, update_interval: int
|
||||
) -> None:
|
||||
"""Each interval is built with its startup delay, if any, and gets its update interval."""
|
||||
assert _constructed(main_cpp, var) == expected
|
||||
assert f"{var}->set_update_interval({update_interval});" in main_cpp
|
||||
|
||||
|
||||
def test_interval_delay_keeps_automation(main_cpp: str) -> None:
|
||||
"""A delay needs the Automation of separate actions."""
|
||||
automation = re.search(
|
||||
r"new\((\w+)\) Automation<>\(interval_intervaltrigger_id_3\);", main_cpp
|
||||
)
|
||||
assert automation is not None
|
||||
assert re.search(
|
||||
rf"{automation.group(1)}->add_action\(delayaction_id\w*\);", main_cpp
|
||||
)
|
||||
|
||||
|
||||
def test_interval_registration_order(main_cpp: str) -> None:
|
||||
"""Intervals register in config order; one waiting for a later id keeps its place."""
|
||||
registered = re.findall(r"App\.register_component_\((\w+), \d+\);", main_cpp)
|
||||
assert registered[-6:] == [
|
||||
"interval_intervaltrigger_id",
|
||||
"interval_intervaltrigger_id_2",
|
||||
"interval_intervaltrigger_id_3",
|
||||
"named_interval",
|
||||
"interval_intervaltrigger_id_4",
|
||||
"some_sensor",
|
||||
]
|
||||
assert main_cpp.index(
|
||||
"App.register_component_(interval_intervaltrigger_id_4"
|
||||
) < main_cpp.index("new(later_script)")
|
||||
@@ -0,0 +1,64 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "esphome/core/base_automation.h"
|
||||
|
||||
namespace esphome::core::testing {
|
||||
|
||||
namespace {
|
||||
|
||||
int calls = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
void count_call() { calls++; }
|
||||
|
||||
// Bit patterns of 800.0f, 600.0f, -100.0f and 250.5f, as the code generator writes them
|
||||
constexpr uint32_t PRIORITY_800 = 0x44480000;
|
||||
constexpr uint32_t PRIORITY_MINUS_100 = 0xC2C80000;
|
||||
constexpr uint32_t PRIORITY_250_5 = 0x437A8000;
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(CoreAutomationCallbacks, StartupCallbackRunsFromSetupAtItsPriority) {
|
||||
calls = 0;
|
||||
StartupCallback<count_call, PRIORITY_800> high;
|
||||
StartupCallback<count_call, PRIORITY_MINUS_100> late;
|
||||
StartupCallback<count_call, PRIORITY_250_5> fractional;
|
||||
EXPECT_EQ(high.get_setup_priority(), 800.0f);
|
||||
EXPECT_EQ(late.get_setup_priority(), -100.0f);
|
||||
EXPECT_EQ(fractional.get_setup_priority(), 250.5f);
|
||||
StartupCallback<count_call, SETUP_PRIORITY_DATA_BITS> standard;
|
||||
EXPECT_EQ(standard.get_setup_priority(), setup_priority::DATA);
|
||||
|
||||
high.loop();
|
||||
high.on_shutdown();
|
||||
EXPECT_EQ(calls, 0);
|
||||
high.setup();
|
||||
EXPECT_EQ(calls, 1);
|
||||
}
|
||||
|
||||
TEST(CoreAutomationCallbacks, ShutdownCallbackRunsFromOnShutdownAtItsPriority) {
|
||||
calls = 0;
|
||||
ShutdownCallback<count_call, PRIORITY_MINUS_100> shutdown;
|
||||
EXPECT_EQ(shutdown.get_setup_priority(), -100.0f);
|
||||
|
||||
shutdown.setup();
|
||||
shutdown.loop();
|
||||
EXPECT_EQ(calls, 0);
|
||||
shutdown.on_shutdown();
|
||||
EXPECT_EQ(calls, 1);
|
||||
}
|
||||
|
||||
TEST(CoreAutomationCallbacks, LoopCallbackRunsEachLoopAtDataPriority) {
|
||||
calls = 0;
|
||||
LoopCallback<count_call> looping;
|
||||
EXPECT_EQ(looping.get_setup_priority(), setup_priority::DATA);
|
||||
EXPECT_TRUE(HasLoopOverride<LoopCallback<count_call>>::value);
|
||||
EXPECT_FALSE((HasLoopOverride<StartupCallback<count_call, PRIORITY_800>>::value));
|
||||
|
||||
looping.setup();
|
||||
EXPECT_EQ(calls, 0);
|
||||
looping.loop();
|
||||
looping.loop();
|
||||
EXPECT_EQ(calls, 2);
|
||||
}
|
||||
|
||||
} // namespace esphome::core::testing
|
||||
@@ -0,0 +1,34 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "esphome/components/interval/interval.h"
|
||||
#include "esphome/core/application.h"
|
||||
|
||||
namespace esphome::interval::testing {
|
||||
|
||||
class CountAction : public Action<> {
|
||||
public:
|
||||
explicit CountAction(int *count) : count_(count) {}
|
||||
void play() override { (*this->count_)++; }
|
||||
|
||||
protected:
|
||||
int *count_;
|
||||
};
|
||||
|
||||
TEST(IntervalTrigger, UpdateRunsTheAutomationAtDataPriority) {
|
||||
IntervalTrigger trigger;
|
||||
Automation<> automation{&trigger};
|
||||
int count = 0;
|
||||
CountAction action{&count};
|
||||
automation.add_action(&action);
|
||||
|
||||
EXPECT_EQ(trigger.get_setup_priority(), setup_priority::DATA);
|
||||
EXPECT_FALSE(HasLoopOverride<IntervalTrigger>::value);
|
||||
trigger.set_update_interval(1000);
|
||||
EXPECT_EQ(trigger.get_update_interval(), 1000u);
|
||||
|
||||
trigger.update();
|
||||
trigger.update();
|
||||
EXPECT_EQ(count, 2);
|
||||
}
|
||||
|
||||
} // namespace esphome::interval::testing
|
||||
@@ -0,0 +1,46 @@
|
||||
esphome:
|
||||
name: interval-runs
|
||||
|
||||
host:
|
||||
api:
|
||||
logger:
|
||||
level: DEBUG
|
||||
|
||||
globals:
|
||||
- id: inline_count
|
||||
type: int
|
||||
initial_value: "0"
|
||||
- id: action_count
|
||||
type: int
|
||||
initial_value: "0"
|
||||
- id: startup_count
|
||||
type: int
|
||||
initial_value: "0"
|
||||
|
||||
interval:
|
||||
# All actions inline
|
||||
- interval: 50ms
|
||||
then:
|
||||
- lambda: id(inline_count) += 1;
|
||||
- lambda: |-
|
||||
if (id(inline_count) == 1) {
|
||||
ESP_LOGI("test", "INLINE first uptime=%u", (unsigned) millis());
|
||||
} else if (id(inline_count) == 20) {
|
||||
ESP_LOGI("test", "INLINE ran 20 times");
|
||||
}
|
||||
# A delay between the actions
|
||||
- interval: 100ms
|
||||
then:
|
||||
- lambda: |-
|
||||
id(action_count) += 1;
|
||||
ESP_LOGI("test", "ACTION start %d", id(action_count));
|
||||
- delay: 20ms
|
||||
- lambda: ESP_LOGI("test", "ACTION end %d", id(action_count));
|
||||
# First run waits for the startup delay
|
||||
- interval: 50ms
|
||||
startup_delay: 1000ms
|
||||
then:
|
||||
- lambda: |-
|
||||
if (++id(startup_count) == 1) {
|
||||
ESP_LOGI("test", "STARTUP first uptime=%u inline_count=%d", (unsigned) millis(), id(inline_count));
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
"""Integration test that intervals run repeatedly and honour their startup delay."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
|
||||
import pytest
|
||||
|
||||
from .log_utils import LineWaiter
|
||||
from .types import RunCompiledFunction
|
||||
|
||||
INLINE_FIRST = re.compile(r"INLINE first uptime=(\d+)")
|
||||
STARTUP_FIRST = re.compile(r"STARTUP first uptime=(\d+) inline_count=(\d+)")
|
||||
ACTION = re.compile(r"ACTION (start|end) (\d+)")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_interval_runs(
|
||||
yaml_config: str,
|
||||
run_compiled: RunCompiledFunction,
|
||||
) -> None:
|
||||
"""Inline and delayed actions repeat; the startup delay holds the first run back."""
|
||||
waiter = LineWaiter()
|
||||
inline_future = waiter.expect(INLINE_FIRST)
|
||||
startup_future = waiter.expect(STARTUP_FIRST)
|
||||
|
||||
async with run_compiled(yaml_config, line_callback=waiter.callback):
|
||||
inline = await waiter.wait(inline_future)
|
||||
startup = await waiter.wait(startup_future)
|
||||
await waiter.wait_for_each("INLINE ran 20 times", "ACTION end 3")
|
||||
|
||||
# The startup delay held the first run back while the inline interval kept running
|
||||
assert int(startup.group(1)) - int(inline.group(1)) >= 800
|
||||
assert int(startup.group(2)) >= 10
|
||||
# Each run of the delayed actions finished before the next started
|
||||
actions = [m.groups() for line in waiter.lines if (m := ACTION.search(line))]
|
||||
assert actions[:6] == [
|
||||
("start", "1"),
|
||||
("end", "1"),
|
||||
("start", "2"),
|
||||
("end", "2"),
|
||||
("start", "3"),
|
||||
("end", "3"),
|
||||
]
|
||||
@@ -2441,25 +2441,16 @@ def polling_component() -> None:
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.usefixtures("polling_component")
|
||||
@pytest.mark.parametrize(
|
||||
("name", "call"),
|
||||
[
|
||||
("component.update", "update()"),
|
||||
("component.suspend", "stop_poller()"),
|
||||
("component.resume", "start_poller()"),
|
||||
],
|
||||
"name",
|
||||
["component.update", "component.suspend", "component.resume"],
|
||||
)
|
||||
async def test_component_action_codegen(name: str, call: str) -> None:
|
||||
"""The call goes through ``PollingComponent`` and only once the component is ready."""
|
||||
async def test_component_action_codegen(name: str) -> None:
|
||||
"""Each action is an object constructed with the component, with no interval set."""
|
||||
await build_action(_component_action(name), TEMPLATE_ARG, [])
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -2475,17 +2466,14 @@ async def test_component_resume_constant_interval_codegen() -> None:
|
||||
)
|
||||
|
||||
assert (
|
||||
" if (esphome__component.is_ready()) {\n"
|
||||
" esphome__component.set_update_interval(250);\n"
|
||||
" esphome__component.start_poller();\n"
|
||||
" }\n"
|
||||
) in CORE.cpp_global_section
|
||||
"act->set_update_interval([]() -> uint32_t {\n return 250;\n});"
|
||||
) in CORE.cpp_main_section
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.usefixtures("polling_component")
|
||||
async def test_component_resume_interval_lambda_codegen() -> None:
|
||||
"""A templated interval is computed from the trigger args, only once ready."""
|
||||
"""A templated interval is a lambda over the trigger args."""
|
||||
await build_action(
|
||||
_component_action(
|
||||
"component.resume", {CONF_UPDATE_INTERVAL: Lambda("return x * 1000;")}
|
||||
@@ -2494,91 +2482,27 @@ 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 (
|
||||
"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"
|
||||
"act->set_update_interval([](uint32_t x) -> uint32_t {\n"
|
||||
" return x * 1000;\n"
|
||||
" }(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
|
||||
"});"
|
||||
) in main
|
||||
|
||||
|
||||
@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.
|
||||
|
||||
It is called rather than reduced, so even a single return modifies only its own copy.
|
||||
"""
|
||||
"""The user's interval lambda gets the trigger args by value, so it may modify them."""
|
||||
await build_action(
|
||||
_component_action("component.resume", {CONF_UPDATE_INTERVAL: Lambda(body)}),
|
||||
cg.TemplateArguments(cg.uint32),
|
||||
[(cg.uint32, "x")],
|
||||
)
|
||||
|
||||
assert (
|
||||
f"set_update_interval([](uint32_t x) -> uint32_t {{\n {body}\n }}(x));"
|
||||
in CORE.cpp_global_section
|
||||
)
|
||||
assert f"[](uint32_t x) -> uint32_t {{\n {body}\n}}" in CORE.cpp_main_section
|
||||
|
||||
|
||||
COMPONENT_ID = ID("comp", type=cg.Component)
|
||||
|
||||
@@ -1173,6 +1173,21 @@ def test_static_function_declares_early_and_defines_after_includes() -> None:
|
||||
]
|
||||
|
||||
|
||||
def test_declare_static_function_defines_the_body_later() -> None:
|
||||
"""The declaration is emitted at once; the body only when define() is called."""
|
||||
define = cg.declare_static_function("esphome__act__fn", ct.void, [])
|
||||
assert [str(st) for st in CORE.global_statements] == [
|
||||
"static void esphome__act__fn();"
|
||||
]
|
||||
assert CORE.global_statements_after_includes == []
|
||||
|
||||
fn = define(["first();", "second();"])
|
||||
assert str(fn) == "esphome__act__fn"
|
||||
assert [str(st) for st in CORE.global_statements_after_includes] == [
|
||||
"static void esphome__act__fn() {\n first();\n second();\n}"
|
||||
]
|
||||
|
||||
|
||||
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