mirror of
https://github.com/esphome/esphome.git
synced 2026-11-15 01:25:57 +00:00
Compare commits
14
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0bb86f8094 | ||
|
|
bf9e2b7a54 | ||
|
|
fd000ead9f | ||
|
|
42956e0173 | ||
|
|
7edd1a8ea7 | ||
|
|
35e03009e6 | ||
|
|
df69028380 | ||
|
|
46c0cd859f | ||
|
|
419660bb67 | ||
|
|
22ba443107 | ||
|
|
4015cea895 | ||
|
|
d4a8b149a9 | ||
|
|
97732c9a25 | ||
|
|
e3bb14be43 |
No files matched your search
@@ -123,10 +123,17 @@ Both live in `esphome/core/helpers.h` and accept `std::function`, lambdas and fo
|
||||
their templatized `add()` method. `LazyCallbackManager` is a drop-in replacement for `CallbackManager` that defers allocation until the first
|
||||
callback is added. Prefer it for entity-level callbacks where most instances have no subscribers.
|
||||
|
||||
A filter that keeps state between calls (timing, edges) still needs no Trigger class: give each automation a
|
||||
small object with constant initializers, created from its `CONF_TRIGGER_ID` before the automations are built,
|
||||
and let `when` ask it, e.g. `ApplyCall("{}->check(state, {}, {})", ((CONF_TRIGGER_ID, ...), ...))`. An `ID`
|
||||
value in an `ApplyCall` is written as `::id`, so a callback parameter cannot hide it. `on_click` in
|
||||
`esphome/components/binary_sensor/__init__.py` is the in-tree example; once a second component needs this,
|
||||
move the object creation into `CallbackAutomation`.
|
||||
|
||||
## Triggers: Trigger class method
|
||||
|
||||
Use `build_automation()` with a `Trigger<Ts...>` subclass only when the forwarder needs **mutable state
|
||||
beyond a single `Automation*` pointer** (e.g. edge detection tracking previous state, timing logic).
|
||||
Use `build_automation()` with a `Trigger<Ts...>` subclass only when the state must be set up from the
|
||||
parent (in the constructor or in `setup()`), or the trigger needs its own timers.
|
||||
|
||||
**Python:**
|
||||
```python
|
||||
|
||||
+2
-47
@@ -5,7 +5,6 @@ import logging
|
||||
import math
|
||||
import re
|
||||
import string
|
||||
import struct
|
||||
from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
@@ -2124,17 +2123,6 @@ 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:
|
||||
@@ -2152,40 +2140,6 @@ 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,
|
||||
@@ -2219,7 +2173,8 @@ async def _trigger_callback(
|
||||
call: ApplyCall | None,
|
||||
parent: MockObj | None,
|
||||
) -> LambdaExpression:
|
||||
target = _call_target(plan, args, config)
|
||||
if (target := _direct_function(plan, args, config)) is None:
|
||||
target = f"{parent_ref(_automation_from_plan(plan, args, config))}->trigger"
|
||||
statements: list[str] = []
|
||||
if call is not None:
|
||||
members = call.members
|
||||
|
||||
@@ -31,7 +31,6 @@ 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,
|
||||
|
||||
@@ -134,10 +134,8 @@ BinarySensorInitiallyOff = binary_sensor_ns.class_(
|
||||
BinarySensorPtr = BinarySensor.operator("ptr")
|
||||
|
||||
# Triggers
|
||||
ClickTrigger = binary_sensor_ns.class_("ClickTrigger", automation.Trigger.template())
|
||||
DoubleClickTrigger = binary_sensor_ns.class_(
|
||||
"DoubleClickTrigger", automation.Trigger.template()
|
||||
)
|
||||
ClickDetector = binary_sensor_ns.class_("ClickDetector")
|
||||
DoubleClickDetector = binary_sensor_ns.class_("DoubleClickDetector")
|
||||
MultiClickTrigger = binary_sensor_ns.class_(
|
||||
"MultiClickTrigger", automation.Trigger.template(), cg.Component
|
||||
)
|
||||
@@ -450,6 +448,23 @@ def validate_click_timing(value):
|
||||
return value
|
||||
|
||||
|
||||
def _click_automation_schema(detector: MockObjClass) -> cv.All:
|
||||
return cv.All(
|
||||
automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(detector),
|
||||
cv.Optional(
|
||||
CONF_MIN_LENGTH, default="50ms"
|
||||
): cv.positive_time_period_milliseconds,
|
||||
cv.Optional(
|
||||
CONF_MAX_LENGTH, default="350ms"
|
||||
): cv.positive_time_period_milliseconds,
|
||||
}
|
||||
),
|
||||
validate_click_timing,
|
||||
)
|
||||
|
||||
|
||||
def validate_publish_initial_state(value):
|
||||
value = cv.boolean(value)
|
||||
_LOGGER.warning(
|
||||
@@ -482,35 +497,9 @@ _BINARY_SENSOR_SCHEMA = (
|
||||
): validate_filters,
|
||||
cv.Optional(CONF_ON_PRESS): automation.validate_automation({}),
|
||||
cv.Optional(CONF_ON_RELEASE): automation.validate_automation({}),
|
||||
cv.Optional(CONF_ON_CLICK): cv.All(
|
||||
automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ClickTrigger),
|
||||
cv.Optional(
|
||||
CONF_MIN_LENGTH, default="50ms"
|
||||
): cv.positive_time_period_milliseconds,
|
||||
cv.Optional(
|
||||
CONF_MAX_LENGTH, default="350ms"
|
||||
): cv.positive_time_period_milliseconds,
|
||||
}
|
||||
),
|
||||
validate_click_timing,
|
||||
),
|
||||
cv.Optional(CONF_ON_DOUBLE_CLICK): cv.All(
|
||||
automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
DoubleClickTrigger
|
||||
),
|
||||
cv.Optional(
|
||||
CONF_MIN_LENGTH, default="50ms"
|
||||
): cv.positive_time_period_milliseconds,
|
||||
cv.Optional(
|
||||
CONF_MAX_LENGTH, default="350ms"
|
||||
): cv.positive_time_period_milliseconds,
|
||||
}
|
||||
),
|
||||
validate_click_timing,
|
||||
cv.Optional(CONF_ON_CLICK): _click_automation_schema(ClickDetector),
|
||||
cv.Optional(CONF_ON_DOUBLE_CLICK): _click_automation_schema(
|
||||
DoubleClickDetector
|
||||
),
|
||||
cv.Optional(CONF_ON_MULTI_CLICK): automation.validate_automation(
|
||||
{
|
||||
@@ -564,6 +553,11 @@ def binary_sensor_schema(
|
||||
return _BINARY_SENSOR_SCHEMA.extend(schema)
|
||||
|
||||
|
||||
_CLICK_DETECTORS = (
|
||||
(CONF_ON_CLICK, ClickDetector),
|
||||
(CONF_ON_DOUBLE_CLICK, DoubleClickDetector),
|
||||
)
|
||||
|
||||
_CALLBACK_AUTOMATIONS = (
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_PRESS,
|
||||
@@ -586,32 +580,40 @@ _CALLBACK_AUTOMATIONS = (
|
||||
"add_full_state_callback",
|
||||
[(cg.optional.template(bool), "x_previous"), (cg.optional.template(bool), "x")],
|
||||
),
|
||||
*(
|
||||
automation.CallbackAutomation(
|
||||
conf_key,
|
||||
"add_on_state_callback",
|
||||
params=[(bool, "state")],
|
||||
forward=[],
|
||||
when=automation.ApplyCall(
|
||||
"{}->check(state, {}, {})",
|
||||
(
|
||||
(CONF_TRIGGER_ID, detector.operator("ptr")),
|
||||
(CONF_MIN_LENGTH, cg.uint32),
|
||||
(CONF_MAX_LENGTH, cg.uint32),
|
||||
),
|
||||
),
|
||||
)
|
||||
for conf_key, detector in _CLICK_DETECTORS
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.AUTOMATION)
|
||||
async def _build_binary_sensor_automations(var, config):
|
||||
# The detectors must exist before the automations that name them are built
|
||||
if clicks := [c for key, _ in _CLICK_DETECTORS for c in config.get(key, ())]:
|
||||
cg.add_define("USE_BINARY_SENSOR_CLICK_TRIGGER")
|
||||
for conf in clicks:
|
||||
cg.new_Pvariable(conf[CONF_TRIGGER_ID])
|
||||
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
|
||||
|
||||
if config.get(CONF_ON_CLICK) or config.get(CONF_ON_DOUBLE_CLICK):
|
||||
cg.add_define("USE_BINARY_SENSOR_CLICK_TRIGGER")
|
||||
if config.get(CONF_ON_MULTI_CLICK):
|
||||
cg.add_define("USE_BINARY_SENSOR_MULTI_CLICK_TRIGGER")
|
||||
if config.get(CONF_ON_RAW_VALUE):
|
||||
cg.add_define("USE_BINARY_SENSOR_RAW_CALLBACK")
|
||||
|
||||
for conf in config.get(CONF_ON_CLICK, []):
|
||||
trigger = cg.new_Pvariable(
|
||||
conf[CONF_TRIGGER_ID], var, conf[CONF_MIN_LENGTH], conf[CONF_MAX_LENGTH]
|
||||
)
|
||||
await automation.build_automation(trigger, [], conf)
|
||||
|
||||
for conf in config.get(CONF_ON_DOUBLE_CLICK, []):
|
||||
trigger = cg.new_Pvariable(
|
||||
conf[CONF_TRIGGER_ID], var, conf[CONF_MIN_LENGTH], conf[CONF_MAX_LENGTH]
|
||||
)
|
||||
await automation.build_automation(trigger, [], conf)
|
||||
|
||||
for conf in config.get(CONF_ON_MULTI_CLICK, []):
|
||||
timings = [
|
||||
cg.StructInitializer(
|
||||
|
||||
@@ -136,6 +136,28 @@ bool match_interval(uint32_t min_length, uint32_t max_length, uint32_t length) {
|
||||
return length >= min_length && length <= max_length;
|
||||
}
|
||||
}
|
||||
|
||||
bool ClickDetector::check(bool state, uint32_t min_length, uint32_t max_length) {
|
||||
if (state) {
|
||||
this->start_time_ = millis();
|
||||
return false;
|
||||
}
|
||||
return match_interval(min_length, max_length, millis() - this->start_time_);
|
||||
}
|
||||
|
||||
bool DoubleClickDetector::check(bool state, uint32_t min_length, uint32_t max_length) {
|
||||
const uint32_t now = millis();
|
||||
if (state && this->start_time_ != 0 && this->end_time_ != 0 &&
|
||||
match_interval(min_length, max_length, this->end_time_ - this->start_time_) &&
|
||||
match_interval(min_length, max_length, now - this->end_time_)) {
|
||||
this->start_time_ = 0;
|
||||
this->end_time_ = 0;
|
||||
return true;
|
||||
}
|
||||
this->start_time_ = this->end_time_;
|
||||
this->end_time_ = now;
|
||||
return false;
|
||||
}
|
||||
#endif // USE_BINARY_SENSOR_CLICK_TRIGGER
|
||||
|
||||
} // namespace esphome::binary_sensor
|
||||
|
||||
@@ -24,54 +24,25 @@ static_assert(std::is_same_v<decltype(MultiClickTriggerEvent::state), uint32_t>
|
||||
|
||||
bool match_interval(uint32_t min_length, uint32_t max_length, uint32_t length);
|
||||
|
||||
class ClickTrigger final : public Trigger<> {
|
||||
/// Times the presses of one on_click automation.
|
||||
class ClickDetector {
|
||||
public:
|
||||
explicit ClickTrigger(BinarySensor *parent, uint32_t min_length, uint32_t max_length)
|
||||
: min_length_(min_length), max_length_(max_length) {
|
||||
parent->add_on_state_callback([this](bool state) {
|
||||
if (state) {
|
||||
this->start_time_ = millis();
|
||||
} else {
|
||||
const uint32_t length = millis() - this->start_time_;
|
||||
if (match_interval(this->min_length_, this->max_length_, length))
|
||||
this->trigger();
|
||||
}
|
||||
});
|
||||
}
|
||||
/// Whether `state` ends a press of min_length to max_length ms; a max_length of 0 means no maximum.
|
||||
bool check(bool state, uint32_t min_length, uint32_t max_length);
|
||||
|
||||
protected:
|
||||
uint32_t start_time_{0}; /// The millis() time when the click started.
|
||||
uint32_t min_length_; /// Minimum length of click. 0 means no minimum.
|
||||
uint32_t max_length_; /// Maximum length of click. 0 means no maximum.
|
||||
uint32_t start_time_{0}; ///< The millis() time when the press started.
|
||||
};
|
||||
|
||||
class DoubleClickTrigger final : public Trigger<> {
|
||||
/// Times the presses of one on_double_click automation.
|
||||
class DoubleClickDetector {
|
||||
public:
|
||||
explicit DoubleClickTrigger(BinarySensor *parent, uint32_t min_length, uint32_t max_length)
|
||||
: min_length_(min_length), max_length_(max_length) {
|
||||
parent->add_on_state_callback([this](bool state) {
|
||||
const uint32_t now = millis();
|
||||
|
||||
if (state && this->start_time_ != 0 && this->end_time_ != 0) {
|
||||
if (match_interval(this->min_length_, this->max_length_, this->end_time_ - this->start_time_) &&
|
||||
match_interval(this->min_length_, this->max_length_, now - this->end_time_)) {
|
||||
this->trigger();
|
||||
this->start_time_ = 0;
|
||||
this->end_time_ = 0;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this->start_time_ = this->end_time_;
|
||||
this->end_time_ = now;
|
||||
});
|
||||
}
|
||||
/// Whether `state` is the second press, with the first press and the gap both in the window.
|
||||
bool check(bool state, uint32_t min_length, uint32_t max_length);
|
||||
|
||||
protected:
|
||||
uint32_t start_time_{0};
|
||||
uint32_t end_time_{0};
|
||||
uint32_t min_length_; /// Minimum length of click. 0 means no minimum.
|
||||
uint32_t max_length_; /// Maximum length of click. 0 means no maximum.
|
||||
uint32_t start_time_{0}; ///< The millis() time of the edge before the last one.
|
||||
uint32_t end_time_{0}; ///< The millis() time of the last edge.
|
||||
};
|
||||
|
||||
class MultiClickTrigger final : public Trigger<>, public Component {
|
||||
|
||||
@@ -171,41 +171,6 @@ 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:
|
||||
|
||||
+14
-12
@@ -124,9 +124,6 @@ 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)
|
||||
)
|
||||
@@ -788,15 +785,20 @@ async def _add_environment_variables(env_vars: dict[str, str]) -> None:
|
||||
|
||||
@coroutine_with_priority(CoroPriority.AUTOMATION)
|
||||
async def _add_automations(config):
|
||||
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)
|
||||
)
|
||||
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)
|
||||
|
||||
|
||||
# Datetime component has special subtypes that need additional defines
|
||||
|
||||
@@ -883,13 +883,14 @@ def new_Pvariable(id_: ID, *args: SafeExpType) -> "MockObj":
|
||||
return Pvariable(id_, rhs)
|
||||
|
||||
|
||||
def declare_static_function(
|
||||
def static_function(
|
||||
name: str,
|
||||
return_type: SafeExpType,
|
||||
parameters: TemplateArgsType,
|
||||
) -> 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.
|
||||
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.
|
||||
|
||||
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
|
||||
@@ -899,24 +900,10 @@ def declare_static_function(
|
||||
f"static {safe_exp(return_type)} {name}({ParameterListExpression(*parameters)})"
|
||||
)
|
||||
add_global(RawStatement(f"{signature};"))
|
||||
|
||||
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)
|
||||
add_global_after_includes(
|
||||
RawStatement(f"{signature} {{\n{indent(chr(10).join(map(str, body)))}\n}}")
|
||||
)
|
||||
return RawExpression(name)
|
||||
|
||||
|
||||
@dataclass
|
||||
|
||||
@@ -30,6 +30,14 @@ 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)
|
||||
|
||||
@@ -48,59 +56,18 @@ 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 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")
|
||||
"""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")
|
||||
|
||||
|
||||
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_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_manual_trigger_id_keeps_trigger(main_cpp: str) -> None:
|
||||
@@ -110,30 +77,19 @@ def test_on_boot_manual_trigger_id_keeps_trigger(main_cpp: str) -> None:
|
||||
|
||||
|
||||
def test_on_shutdown_priorities(main_cpp: str) -> None:
|
||||
"""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")
|
||||
"""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")
|
||||
|
||||
|
||||
def test_on_loop(main_cpp: str) -> None:
|
||||
"""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")
|
||||
"""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")
|
||||
|
||||
|
||||
def test_on_boot_registered_before_its_actions_are_built(
|
||||
|
||||
@@ -8,18 +8,24 @@ from esphome.core import CORE
|
||||
|
||||
|
||||
def test_click_triggers(generate_main: Callable[[str | Path], str]) -> None:
|
||||
"""Each trigger gets its timing in milliseconds, defaults 50 and 350, and its own automation."""
|
||||
"""Each trigger gets a detector and a callback with its timing in milliseconds, defaults 50 and 350."""
|
||||
main_cpp = generate_main("tests/component_tests/binary_sensor/test_click.yaml")
|
||||
|
||||
triggers = re.findall(
|
||||
r"new\((\w+)\) binary_sensor::(Click|DoubleClick)Trigger\(button_a, (\d+), (\d+)\);",
|
||||
main_cpp,
|
||||
kinds = dict(
|
||||
re.findall(
|
||||
r"new\((\w+)\) binary_sensor::(Click|DoubleClick)Detector\(\);", main_cpp
|
||||
)
|
||||
)
|
||||
assert [trigger[1:] for trigger in triggers] == [
|
||||
calls = re.findall(r"if \(!\(::(\w+)->check\(state, (\d+), (\d+)\)\)\)", main_cpp)
|
||||
assert [(kinds[detector_id], *timing) for detector_id, *timing in calls] == [
|
||||
("Click", "50", "350"),
|
||||
("Click", "1000", "3000"),
|
||||
("DoubleClick", "20", "400"),
|
||||
]
|
||||
for trigger_id, *_ in triggers:
|
||||
assert f"Automation<>({trigger_id});" in main_cpp
|
||||
assert main_cpp.count("button_a->add_on_state_callback([](") == 3
|
||||
# Only the automation with a delay keeps an Automation object
|
||||
assert re.findall(r"new\((\w+)\) Automation<>\(\);", main_cpp) == [
|
||||
"automation_id_2"
|
||||
]
|
||||
assert "::automation_id_2->trigger();" in main_cpp
|
||||
assert "USE_BINARY_SENSOR_CLICK_TRIGGER" in {define.name for define in CORE.defines}
|
||||
@@ -21,7 +21,9 @@ binary_sensor:
|
||||
- delay: 1s
|
||||
- logger.log: a long click done
|
||||
on_double_click:
|
||||
- min_length: 20ms
|
||||
# Named like the callback parameter, which must not hide it
|
||||
- trigger_id: state
|
||||
min_length: 20ms
|
||||
max_length: 400ms
|
||||
then:
|
||||
- logger.log: a double click
|
||||
@@ -1,42 +0,0 @@
|
||||
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;
|
||||
@@ -1,72 +0,0 @@
|
||||
"""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)")
|
||||
@@ -1,16 +1,15 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include <memory>
|
||||
#include <functional>
|
||||
#include <utility>
|
||||
|
||||
#include "esphome/components/binary_sensor/automation.h"
|
||||
#include "esphome/components/binary_sensor/binary_sensor.h"
|
||||
#include "esphome/components/binary_sensor/filter.h"
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/base_automation.h"
|
||||
#include "esphome/core/hal.h"
|
||||
|
||||
namespace esphome::binary_sensor::testing {
|
||||
|
||||
// The triggers read the real millis(), so every wait is at least 80 ms away from a window edge
|
||||
// The click detectors read the real millis(), so every wait is at least 80 ms away from a window edge
|
||||
static constexpr uint32_t MIN_MS = 100;
|
||||
static constexpr uint32_t MAX_MS = 300;
|
||||
static constexpr uint32_t SHORT_MS = 20;
|
||||
@@ -24,18 +23,24 @@ class ClickTriggerTest : public ::testing::Test {
|
||||
void SetUp() override {
|
||||
fired = 0;
|
||||
this->sensor_.publish_state(false);
|
||||
this->automation_.add_action(&this->action_);
|
||||
}
|
||||
|
||||
// As codegen builds on_click and on_double_click
|
||||
void add_click_(uint32_t min_length = MIN_MS, uint32_t max_length = MAX_MS, BinarySensor *sensor = nullptr) {
|
||||
this->click_ = std::make_unique<ClickTrigger>(sensor != nullptr ? sensor : &this->sensor_, min_length, max_length);
|
||||
this->click_->set_automation_parent(&this->automation_);
|
||||
this->watch_(this->click_, min_length, max_length, sensor != nullptr ? *sensor : this->sensor_);
|
||||
}
|
||||
void add_double_click_() {
|
||||
this->double_click_ = std::make_unique<DoubleClickTrigger>(&this->sensor_, MIN_MS, MAX_MS);
|
||||
this->double_click_->set_automation_parent(&this->automation_);
|
||||
void add_double_click_(std::function<void()> on_fire = count_fire) {
|
||||
this->watch_(this->double_click_, MIN_MS, MAX_MS, this->sensor_, std::move(on_fire));
|
||||
}
|
||||
template<typename D>
|
||||
void watch_(D &detector, uint32_t min_length, uint32_t max_length, BinarySensor &sensor,
|
||||
std::function<void()> on_fire = count_fire) {
|
||||
sensor.add_on_state_callback([&detector, min_length, max_length, on_fire = std::move(on_fire)](bool state) {
|
||||
if (detector.check(state, min_length, max_length))
|
||||
on_fire();
|
||||
});
|
||||
}
|
||||
static void count_fire() { fired++; }
|
||||
|
||||
void press_for_(uint32_t length) {
|
||||
this->sensor_.publish_state(true);
|
||||
@@ -44,10 +49,8 @@ class ClickTriggerTest : public ::testing::Test {
|
||||
}
|
||||
|
||||
BinarySensor sensor_;
|
||||
StatelessLambdaAction<> action_{[]() { fired++; }};
|
||||
Automation<> automation_;
|
||||
std::unique_ptr<ClickTrigger> click_;
|
||||
std::unique_ptr<DoubleClickTrigger> double_click_;
|
||||
ClickDetector click_;
|
||||
DoubleClickDetector double_click_;
|
||||
};
|
||||
|
||||
TEST(ClickMatchInterval, BoundsAreInclusive) {
|
||||
@@ -198,4 +201,23 @@ TEST_F(ClickTriggerTest, DoubleClickDoesNotFireTwiceWithoutReset) {
|
||||
EXPECT_EQ(fired, 1);
|
||||
}
|
||||
|
||||
TEST_F(ClickTriggerTest, DoubleClickKeepsPressPublishedByItsActions) {
|
||||
// The times reset before the actions run, so a press the first action publishes is kept
|
||||
this->add_double_click_([this]() {
|
||||
if (++fired == 1) {
|
||||
this->sensor_.publish_state(false);
|
||||
this->sensor_.publish_state(true);
|
||||
}
|
||||
});
|
||||
this->press_for_(INSIDE_MS);
|
||||
delay(INSIDE_MS);
|
||||
this->sensor_.publish_state(true);
|
||||
EXPECT_EQ(fired, 1);
|
||||
delay(INSIDE_MS);
|
||||
this->sensor_.publish_state(false);
|
||||
delay(INSIDE_MS);
|
||||
this->sensor_.publish_state(true);
|
||||
EXPECT_EQ(fired, 2);
|
||||
}
|
||||
|
||||
} // namespace esphome::binary_sensor::testing
|
||||
@@ -1,64 +0,0 @@
|
||||
#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
|
||||
@@ -1,34 +0,0 @@
|
||||
#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
|
||||
@@ -1,46 +0,0 @@
|
||||
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));
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
"""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,21 +1173,6 @@ 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