mirror of
https://github.com/esphome/esphome.git
synced 2026-11-15 17:45:57 +00:00
Compare commits
52
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2653dbb94d | ||
|
|
5849d87a07 | ||
|
|
113eb659d5 | ||
|
|
33722485e0 | ||
|
|
c7b321eba0 | ||
|
|
a9ae0380cb | ||
|
|
641ccfc361 | ||
|
|
0bf9e7681a | ||
|
|
9d65f4fe20 | ||
|
|
0d8e8a53e1 | ||
|
|
a48151a993 | ||
|
|
3482d33176 | ||
|
|
6ecd846999 | ||
|
|
4912a88528 | ||
|
|
fae6741fb3 | ||
|
|
ba4cc57f40 | ||
|
|
6eb97bf7ee | ||
|
|
f41f1c711a | ||
|
|
475d794012 | ||
|
|
773a437b45 | ||
|
|
42ea2a2821 | ||
|
|
e0e25ac7cc | ||
|
|
c071107528 | ||
|
|
948a9eeb27 | ||
|
|
65a83c12b6 | ||
|
|
3ea0067b8d | ||
|
|
7d8b9c31b2 | ||
|
|
d9d8518171 | ||
|
|
76c51140b1 | ||
|
|
e6f6c0f09b | ||
|
|
7815874624 | ||
|
|
d9082378c5 | ||
|
|
95f7423424 | ||
|
|
fef7a15521 | ||
|
|
0b6fc2c3c5 | ||
|
|
8bda76cc75 | ||
|
|
008da6dceb | ||
|
|
163f6fff55 | ||
|
|
15810c176c | ||
|
|
436baa9375 | ||
|
|
4705fbb703 | ||
|
|
b382723e9d | ||
|
|
ce70abbbb4 | ||
|
|
6b5ae000d6 | ||
|
|
2cfd2a8f51 | ||
|
|
77574a50eb | ||
|
|
c52beb6d5e | ||
|
|
dde3aaf50e | ||
|
|
6f7af22b41 | ||
|
|
de0bf34e84 | ||
|
|
7bd7547983 | ||
|
|
aae52f2265 |
No files matched your search
+55
-2
@@ -5,6 +5,7 @@ import logging
|
||||
import math
|
||||
import re
|
||||
import string
|
||||
import struct
|
||||
from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
@@ -2123,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:
|
||||
@@ -2140,6 +2152,48 @@ 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,
|
||||
*,
|
||||
id_key: str = CONF_TRIGGER_ID,
|
||||
template_args: Sequence[SafeExpType] = (),
|
||||
) -> MockObj:
|
||||
"""Create and register ``component_class<function[, priority bits], *template_args>``,
|
||||
whose function runs the actions of a no-argument automation.
|
||||
|
||||
An id under ``id_key`` set by hand keeps its Trigger, built with ``priority`` if given and
|
||||
``template_args`` as constructor arguments.
|
||||
"""
|
||||
trigger_id: ID = config[id_key]
|
||||
priority_list = [] if priority is None else [priority]
|
||||
if trigger_id.is_manual:
|
||||
var = cg.new_Pvariable(trigger_id, *priority_list, *template_args)
|
||||
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, *template_args)
|
||||
)
|
||||
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,
|
||||
@@ -2173,8 +2227,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,
|
||||
|
||||
@@ -11,6 +11,10 @@ IntervalTrigger = interval_ns.class_(
|
||||
"IntervalTrigger", automation.Trigger.template(), cg.PollingComponent
|
||||
)
|
||||
DelayedIntervalTrigger = interval_ns.class_("DelayedIntervalTrigger", IntervalTrigger)
|
||||
IntervalCallback = interval_ns.class_("IntervalCallback", cg.PollingComponent)
|
||||
DelayedIntervalCallback = interval_ns.class_(
|
||||
"DelayedIntervalCallback", IntervalCallback
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = automation.validate_automation(
|
||||
cv.Schema(
|
||||
@@ -27,12 +31,14 @@ CONFIG_SCHEMA = automation.validate_automation(
|
||||
|
||||
async def to_code(config: list[ConfigType]) -> None:
|
||||
for conf in config:
|
||||
if startup_delay := conf[CONF_STARTUP_DELAY].total_milliseconds:
|
||||
# Cut to 32 bits, as the uint32_t setter always did
|
||||
if startup_delay := conf[CONF_STARTUP_DELAY].total_milliseconds & 0xFFFFFFFF:
|
||||
# A hand-set id keeps the trigger, which takes the delay in its constructor
|
||||
conf[CONF_ID].type = DelayedIntervalTrigger
|
||||
var = cg.new_Pvariable(conf[CONF_ID], startup_delay)
|
||||
component_class, template_args = DelayedIntervalCallback, [startup_delay]
|
||||
else:
|
||||
var = cg.new_Pvariable(conf[CONF_ID])
|
||||
await cg.register_component(var, conf)
|
||||
await automation.build_automation(var, [], conf)
|
||||
|
||||
component_class, template_args = IntervalCallback, []
|
||||
var = await automation.build_component_automation(
|
||||
conf, component_class, id_key=CONF_ID, template_args=template_args
|
||||
)
|
||||
cg.add(var.set_update_interval(conf[CONF_INTERVAL]))
|
||||
@@ -0,0 +1,10 @@
|
||||
#include "interval.h"
|
||||
|
||||
namespace esphome::interval {
|
||||
|
||||
void IntervalBase::start_after_delay_(uint32_t startup_delay) {
|
||||
this->stop_poller();
|
||||
this->set_timeout(startup_delay, [this] { this->start_poller(); });
|
||||
}
|
||||
|
||||
} // namespace esphome::interval
|
||||
@@ -6,7 +6,13 @@
|
||||
|
||||
namespace esphome::interval {
|
||||
|
||||
class IntervalTrigger : public Trigger<>, public PollingComponent {
|
||||
/// Base of the interval components; holds the first update back for a startup delay.
|
||||
class IntervalBase : public PollingComponent {
|
||||
protected:
|
||||
void start_after_delay_(uint32_t startup_delay);
|
||||
};
|
||||
|
||||
class IntervalTrigger : public Trigger<>, public IntervalBase {
|
||||
public:
|
||||
void update() override { this->trigger(); }
|
||||
};
|
||||
@@ -16,13 +22,22 @@ class DelayedIntervalTrigger final : public IntervalTrigger {
|
||||
public:
|
||||
explicit DelayedIntervalTrigger(uint32_t startup_delay) : startup_delay_(startup_delay) {}
|
||||
|
||||
void setup() override {
|
||||
this->stop_poller();
|
||||
this->set_timeout(this->startup_delay_, [this] { this->start_poller(); });
|
||||
}
|
||||
void setup() override { this->start_after_delay_(this->startup_delay_); }
|
||||
|
||||
protected:
|
||||
const uint32_t startup_delay_;
|
||||
};
|
||||
|
||||
/// Calls Fn from update(), for an interval without a hand-set id.
|
||||
template<auto Fn> class IntervalCallback : public IntervalBase {
|
||||
public:
|
||||
void update() override { Fn(); }
|
||||
};
|
||||
|
||||
/// An IntervalCallback whose first run waits STARTUP_DELAY ms; only used when the delay is not zero.
|
||||
template<auto Fn, uint32_t STARTUP_DELAY> class DelayedIntervalCallback final : public IntervalCallback<Fn> {
|
||||
public:
|
||||
void setup() override { this->start_after_delay_(STARTUP_DELAY); }
|
||||
};
|
||||
|
||||
} // namespace esphome::interval
|
||||
@@ -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:
|
||||
|
||||
+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
|
||||
|
||||
@@ -8,6 +8,7 @@ import re
|
||||
from typing import NamedTuple
|
||||
|
||||
from esphome.helpers import fnv1_hash
|
||||
from tests.component_tests.helpers import constructed, function_body
|
||||
|
||||
CONFIG = "tests/component_tests/api/test_user_services.yaml"
|
||||
LEGACY_CONFIG = "tests/component_tests/api/test_user_services_legacy.yaml"
|
||||
@@ -22,24 +23,10 @@ class Service(NamedTuple):
|
||||
type: str
|
||||
|
||||
|
||||
def _constructed(main_cpp: str, id_: str) -> str:
|
||||
match = re.search(rf"\bnew\({id_}\) (.*?);\n", main_cpp, re.DOTALL)
|
||||
assert match is not None, id_
|
||||
return match.group(1)
|
||||
|
||||
|
||||
def _actions_of(main_cpp: str, automation: str) -> list[str]:
|
||||
return re.findall(rf"\b{automation}->add_action\((\w+)\);", main_cpp)
|
||||
|
||||
|
||||
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 _strings(main_cpp: str, name: str) -> tuple[str, str]:
|
||||
"""Return the id and the entries of the strings table of action ``name``."""
|
||||
match = re.search(
|
||||
@@ -70,14 +57,14 @@ def _automation(main_cpp: str, trigger: str) -> tuple[str, str]:
|
||||
|
||||
def _action_code(main_cpp: str, action: str) -> str:
|
||||
"""Return the code an action runs: its generated function, or its constructor."""
|
||||
constructed = _constructed(main_cpp, action)
|
||||
if match := re.match(r"ApplyRunAction<(\w+)[,>]", constructed):
|
||||
return _function_body(main_cpp, match.group(1))
|
||||
return constructed
|
||||
code = constructed(main_cpp, action)
|
||||
if match := re.match(r"ApplyRunAction<(\w+)[,>]", code):
|
||||
return function_body(main_cpp, match.group(1))
|
||||
return code
|
||||
|
||||
|
||||
def _action_class(main_cpp: str, action: str) -> str:
|
||||
return re.match(r"[\w:]+", _constructed(main_cpp, action)).group(0)
|
||||
return re.match(r"[\w:]+", constructed(main_cpp, action)).group(0)
|
||||
|
||||
|
||||
def test_no_args_service(generate_main) -> None:
|
||||
@@ -151,12 +138,12 @@ def test_respond_optional(generate_main) -> None:
|
||||
automation, args = _automation(main_cpp, service.id)
|
||||
assert args == "uint32_t, bool, int32_t"
|
||||
respond, unregister = _actions_of(main_cpp, automation)
|
||||
assert _constructed(main_cpp, respond) == (
|
||||
assert constructed(main_cpp, respond) == (
|
||||
"api::APIRespondAction<uint32_t, bool, int32_t>(api_apiserver_id)"
|
||||
)
|
||||
assert f"{respond}->set_is_optional_mode(true);" in main_cpp
|
||||
assert unregister == f"{service.id}__unregister"
|
||||
assert _constructed(main_cpp, unregister) == (
|
||||
assert constructed(main_cpp, unregister) == (
|
||||
"api::APIUnregisterServiceCallAction<uint32_t, bool, int32_t>(api_apiserver_id)"
|
||||
)
|
||||
|
||||
@@ -170,12 +157,12 @@ def test_respond_only(generate_main) -> None:
|
||||
assert args == "uint32_t, StringRef"
|
||||
log, respond, unregister = _actions_of(main_cpp, automation)
|
||||
assert "RESPOND_ONLY %s" in _action_code(main_cpp, log)
|
||||
assert _constructed(main_cpp, respond) == (
|
||||
assert constructed(main_cpp, respond) == (
|
||||
"api::APIRespondAction<uint32_t, StringRef>(api_apiserver_id)"
|
||||
)
|
||||
assert f"{respond}->set_is_optional_mode" not in main_cpp
|
||||
assert unregister == f"{service.id}__unregister"
|
||||
assert _constructed(main_cpp, unregister) == (
|
||||
assert constructed(main_cpp, unregister) == (
|
||||
"api::APIUnregisterServiceCallAction<uint32_t, StringRef>(api_apiserver_id)"
|
||||
)
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@ import re
|
||||
|
||||
import pytest
|
||||
|
||||
from tests.component_tests.helpers import actions_body, constructed, delayed_automation
|
||||
|
||||
CONFIG = "core_automations.yaml"
|
||||
|
||||
|
||||
@@ -17,12 +19,6 @@ def main_cpp(
|
||||
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)
|
||||
|
||||
|
||||
def _automation_of(main_cpp: str, trigger: str) -> str:
|
||||
"""The Automation built on ``trigger``, found by its trigger rather than an id counter."""
|
||||
match = re.search(rf"new\((\w+)\) Automation<>\({trigger}\);", main_cpp)
|
||||
@@ -30,14 +26,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)
|
||||
|
||||
@@ -57,39 +45,65 @@ def test_core_automations_registration_order(main_cpp: str) -> None:
|
||||
|
||||
|
||||
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:
|
||||
"""A trigger_id set by hand can be used from lambdas, so the Trigger stays."""
|
||||
assert _constructed(main_cpp, "manual_boot") == "StartupTrigger(250.5f)"
|
||||
assert constructed(main_cpp, "manual_boot") == "StartupTrigger(250.5f)"
|
||||
_automation_of(main_cpp, "manual_boot")
|
||||
|
||||
|
||||
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(
|
||||
|
||||
@@ -47,6 +47,41 @@ def get_define_value(name: str) -> str | None:
|
||||
return None
|
||||
|
||||
|
||||
def constructed(main_cpp: str, var: str) -> str:
|
||||
"""The expression placement-new builds ``var`` with, e.g. ``Foo<bar>()``."""
|
||||
match = re.search(rf"\bnew\({var}\) (.*?);\n", main_cpp, re.DOTALL)
|
||||
assert match is not None, var
|
||||
return match.group(1)
|
||||
|
||||
|
||||
def function_body(main_cpp: str, name: str) -> str:
|
||||
"""The body of the generated ``static void name(...)``."""
|
||||
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."""
|
||||
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
|
||||
|
||||
|
||||
# ESPBTUUID constant for ABCDABCD-ABCD-ABCD-ABCD-ABCDABCDABCD (bytes in BLE wire order).
|
||||
BLE_UUID128_FACTORY = (
|
||||
"from_raw(std::array<uint8_t, 16>{" + ", ".join(["0xCD", "0xAB"] * 8) + "}.data())"
|
||||
|
||||
@@ -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,78 @@
|
||||
"""Tests for the code generated for interval blocks."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
import re
|
||||
|
||||
import pytest
|
||||
|
||||
from tests.component_tests.helpers import constructed, delayed_automation
|
||||
|
||||
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 _callback(var: str, startup_delay: int = 0) -> str:
|
||||
if startup_delay:
|
||||
return (
|
||||
f"interval::DelayedIntervalCallback<esphome__{var}__fn, {startup_delay}>()"
|
||||
)
|
||||
return f"interval::IntervalCallback<esphome__{var}__fn>()"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("var", "expected", "update_interval"),
|
||||
[
|
||||
("interval_intervaltrigger_id", _callback("interval_intervaltrigger_id"), 1000),
|
||||
(
|
||||
"interval_intervaltrigger_id_2",
|
||||
_callback("interval_intervaltrigger_id_2", 5000),
|
||||
2000,
|
||||
),
|
||||
(
|
||||
"interval_intervaltrigger_id_3",
|
||||
_callback("interval_intervaltrigger_id_3"),
|
||||
3000,
|
||||
),
|
||||
("named_interval", "interval::DelayedIntervalTrigger(3000)", 4000),
|
||||
(
|
||||
"interval_intervaltrigger_id_4",
|
||||
_callback("interval_intervaltrigger_id_4"),
|
||||
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_triggers_automation(main_cpp: str) -> None:
|
||||
"""A delay keeps the Automation of separate actions, which the function triggers."""
|
||||
delayed_automation(main_cpp, "esphome__interval_intervaltrigger_id_3__fn")
|
||||
|
||||
|
||||
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)")
|
||||
@@ -6,23 +6,11 @@ ids, so adding or changing one script does not break the others.
|
||||
|
||||
import re
|
||||
|
||||
from tests.component_tests.helpers import constructed, function_body
|
||||
|
||||
CONFIG = "tests/component_tests/script/test_script_codegen.yaml"
|
||||
|
||||
|
||||
def _constructed(main_cpp: str, id_: str) -> str:
|
||||
match = re.search(rf"\bnew\({id_}\) (.*?);\n", main_cpp, re.DOTALL)
|
||||
assert match is not None, id_
|
||||
return match.group(1)
|
||||
|
||||
|
||||
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 _automation(main_cpp: str, trigger: str) -> str:
|
||||
match = re.search(rf"\bnew\((\w+)\) Automation<.*?>\({trigger}\);", main_cpp)
|
||||
assert match is not None, trigger
|
||||
@@ -40,17 +28,17 @@ def _single_action(main_cpp: str, script: str) -> str:
|
||||
|
||||
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
|
||||
code = constructed(main_cpp, _single_action(main_cpp, script))
|
||||
match = re.match(r"ApplyRunAction<(\w+)[,>]", code)
|
||||
assert match is not None, code
|
||||
return match.group(1)
|
||||
|
||||
|
||||
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."""
|
||||
main_cpp = generate_main(CONFIG)
|
||||
assert _constructed(main_cpp, "inline_single") == "script::SingleScript<>()"
|
||||
body = _function_body(main_cpp, _run_function(main_cpp, "inline_single"))
|
||||
assert constructed(main_cpp, "inline_single") == "script::SingleScript<>()"
|
||||
body = function_body(main_cpp, _run_function(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
|
||||
@@ -60,40 +48,40 @@ 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."""
|
||||
main_cpp = generate_main(CONFIG)
|
||||
constructed = _constructed(main_cpp, _single_action(main_cpp, "one_action"))
|
||||
assert constructed.startswith("StatelessLambdaAction<>(")
|
||||
assert '"ONE action"' in constructed
|
||||
code = constructed(main_cpp, _single_action(main_cpp, "one_action"))
|
||||
assert code.startswith("StatelessLambdaAction<>(")
|
||||
assert '"ONE action"' in code
|
||||
|
||||
|
||||
def test_modes(generate_main) -> None:
|
||||
"""Each mode has its own class; 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<>()"
|
||||
assert constructed(main_cpp, "inline_restart") == "script::RestartScript<>()"
|
||||
assert constructed(main_cpp, "inline_queued") == "script::QueueingScript<>()"
|
||||
assert constructed(main_cpp, "inline_parallel") == "script::ParallelScript<>()"
|
||||
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<>(")
|
||||
code = constructed(main_cpp, _single_action(main_cpp, script))
|
||||
assert code.startswith("StatelessLambdaAction<>(")
|
||||
|
||||
|
||||
def test_restart_executes_itself(generate_main) -> None:
|
||||
"""A script can execute itself; on_stop has its own Automation."""
|
||||
main_cpp = generate_main(CONFIG)
|
||||
body = _function_body(main_cpp, _run_function(main_cpp, "inline_restart"))
|
||||
body = function_body(main_cpp, _run_function(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)
|
||||
assert '"RESTART stopped"' in _constructed(main_cpp, action)
|
||||
assert '"RESTART stopped"' in constructed(main_cpp, action)
|
||||
|
||||
|
||||
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")
|
||||
constructed(main_cpp, "with_params")
|
||||
== "script::SingleScript<int, std::string>()"
|
||||
)
|
||||
name = _run_function(main_cpp, "with_params")
|
||||
@@ -103,7 +91,7 @@ def test_parameters_by_value(generate_main) -> None:
|
||||
r"const std::remove_cvref_t<std::string> & s\)",
|
||||
main_cpp,
|
||||
)
|
||||
body = _function_body(main_cpp, name)
|
||||
body = function_body(main_cpp, name)
|
||||
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 +104,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 = function_body(main_cpp, _run_function(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,8 +114,8 @@ 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 constructed(main_cpp, "calls_itself") == "script::ParallelScript<int>()"
|
||||
body = function_body(main_cpp, _run_function(main_cpp, "calls_itself"))
|
||||
assert "return n > 0;" in body
|
||||
assert "::calls_itself->execute(std::move(esphome__arg_0));" in body
|
||||
|
||||
@@ -135,11 +123,11 @@ def test_calls_itself_with_parameter(generate_main) -> None:
|
||||
def test_delay_keeps_separate_actions(generate_main) -> None:
|
||||
"""A delay keeps the Automation and its separate actions."""
|
||||
main_cpp = generate_main(CONFIG)
|
||||
assert _constructed(main_cpp, "with_delay") == "script::SingleScript<>()"
|
||||
assert constructed(main_cpp, "with_delay") == "script::SingleScript<>()"
|
||||
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 constructed(main_cpp, delay) == "DelayAction<>()"
|
||||
assert '"DELAY before"' in constructed(main_cpp, before)
|
||||
assert '"DELAY after"' in constructed(main_cpp, after)
|
||||
|
||||
|
||||
def test_manual_automation_id(generate_main) -> None:
|
||||
@@ -147,7 +135,7 @@ def test_manual_automation_id(generate_main) -> None:
|
||||
main_cpp = generate_main(CONFIG)
|
||||
assert _automation(main_cpp, "manual_automation") == "manual_automation_id"
|
||||
(action,) = _actions_of(main_cpp, "manual_automation_id")
|
||||
assert '"MANUAL"' in _constructed(main_cpp, action)
|
||||
assert '"MANUAL"' in constructed(main_cpp, action)
|
||||
|
||||
|
||||
def test_script_wait(generate_main) -> None:
|
||||
@@ -155,6 +143,6 @@ def test_script_wait(generate_main) -> None:
|
||||
main_cpp = generate_main(CONFIG)
|
||||
(action,) = _actions_of(main_cpp, _automation(main_cpp, "waits"))
|
||||
assert (
|
||||
_constructed(main_cpp, action)
|
||||
constructed(main_cpp, action)
|
||||
== "script::ScriptWaitAction<script::SingleScript<>>(inline_single)"
|
||||
)
|
||||
@@ -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,41 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "esphome/components/interval/interval.h"
|
||||
#include "esphome/core/application.h"
|
||||
|
||||
namespace esphome::interval::testing {
|
||||
|
||||
namespace {
|
||||
|
||||
int calls = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
void count_call() { calls++; }
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(IntervalCallback, UpdateCallsFnAtDataPriority) {
|
||||
calls = 0;
|
||||
IntervalCallback<count_call> interval;
|
||||
EXPECT_EQ(interval.get_setup_priority(), setup_priority::DATA);
|
||||
EXPECT_FALSE(HasLoopOverride<IntervalCallback<count_call>>::value);
|
||||
interval.set_update_interval(1000);
|
||||
EXPECT_EQ(interval.get_update_interval(), 1000u);
|
||||
|
||||
interval.setup();
|
||||
EXPECT_EQ(calls, 0);
|
||||
interval.update();
|
||||
interval.update();
|
||||
EXPECT_EQ(calls, 2);
|
||||
}
|
||||
|
||||
TEST(IntervalCallback, DelayedIntervalCallbackUpdateCallsFn) {
|
||||
// Only the delayed class overrides setup(), so a zero delay keeps Component::setup()
|
||||
EXPECT_FALSE((HasLoopOverride<DelayedIntervalCallback<count_call, 5000>>::value));
|
||||
calls = 0;
|
||||
DelayedIntervalCallback<count_call, 5000> delayed;
|
||||
EXPECT_EQ(delayed.get_setup_priority(), setup_priority::DATA);
|
||||
delayed.update();
|
||||
EXPECT_EQ(calls, 1);
|
||||
}
|
||||
|
||||
} // namespace esphome::interval::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"),
|
||||
]
|
||||
@@ -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