mirror of
https://github.com/esphome/esphome.git
synced 2026-09-25 19:55:13 +08:00
[core][cover] Add register_apply_condition for conditions that only test the parent (#19540)
This commit is contained in:
@@ -470,6 +470,19 @@ file does, and it is the authority when they disagree. The most useful starting
|
||||
Register with `automation.register_simple_condition("my_component.is_active", MyCondition, schema)`;
|
||||
`register_bare_condition`, `register_parented_condition` and the decorator follow the action rules.
|
||||
|
||||
**Conditions that only test their parent need no C++ class either.** Register them with
|
||||
`register_apply_condition`; the expression is applied to the parent, and an `ApplyCall` compares
|
||||
against config values.
|
||||
```python
|
||||
automation.register_apply_condition("my_component.is_active", schema, "is_active()")
|
||||
automation.register_apply_condition(
|
||||
"my_component.state_is",
|
||||
schema,
|
||||
automation.ApplyCall("state == {}", ((CONF_STATE, cg.bool_),)),
|
||||
)
|
||||
```
|
||||
`cover.is_open`, `rtttl.is_playing` and `component.is_idle` are in-tree examples.
|
||||
|
||||
* **Type Hints:** Type-hint all function signatures, including test functions and config validators (e.g. `def validate_x(config: ConfigType) -> ConfigType:`, `def test_x() -> None:`). Import `ConfigType` from `esphome.types`.
|
||||
|
||||
* **Configuration Validation:**
|
||||
|
||||
+118
-42
@@ -214,6 +214,7 @@ validate_condition = cv.validate_registry_entry("condition", CONDITION_REGISTRY)
|
||||
validate_condition_list = cv.validate_registry("condition", CONDITION_REGISTRY)
|
||||
|
||||
ApplyAction = cg.esphome_ns.class_("ApplyAction", Action)
|
||||
ApplyCondition = cg.esphome_ns.class_("ApplyCondition", Condition)
|
||||
|
||||
|
||||
def flash_string(config: ConfigType, value: str) -> str:
|
||||
@@ -255,6 +256,13 @@ class ApplyCall:
|
||||
f"apply target {self.target!r}: each arg is (conf_key, type_[, const_fn])"
|
||||
)
|
||||
|
||||
@property
|
||||
def members(self) -> list[tuple[Any, Any, Any]]:
|
||||
"""Each arg as ``(conf_key, type_, const_fn or None)``."""
|
||||
return [
|
||||
(arg[0], arg[1], arg[2] if len(arg) == 3 else None) for arg in self.args
|
||||
]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ApplyField:
|
||||
@@ -322,6 +330,58 @@ def _check_key_in_schema(
|
||||
schema = schema.schema[markers[part]]
|
||||
|
||||
|
||||
async def _apply_parent(config: ConfigType) -> str:
|
||||
# Global-scope qualified so a trigger arg named like the id cannot shadow it.
|
||||
return f"::{await cg.get_variable(config[CONF_ID])}"
|
||||
|
||||
|
||||
def _apply_lambda_args(args: TemplateArgsType) -> TemplateArgsType:
|
||||
# Must match ApplyAction::ApplyFn and ApplyCondition::CheckFn exactly for the function
|
||||
# pointer conversion.
|
||||
return [
|
||||
(cg.RawExpression(f"const std::remove_cvref_t<{cg.safe_exp(t)}> &"), arg)
|
||||
for t, arg in args
|
||||
]
|
||||
|
||||
|
||||
async def _render_values(
|
||||
name: str,
|
||||
target: str,
|
||||
members: list[tuple[Any, Any, Any]],
|
||||
values: list[Any],
|
||||
config: ConfigType,
|
||||
parent: str,
|
||||
lambda_args: TemplateArgsType,
|
||||
compare: bool = False,
|
||||
) -> list[str]:
|
||||
"""Render the argument text of one statement; every key must be present.
|
||||
|
||||
``compare``: an inlined lambda expression is parenthesized so it binds as a whole
|
||||
beside an operator.
|
||||
"""
|
||||
if any(value is None for value in values):
|
||||
keys = [key for key, _, _ in members]
|
||||
raise EsphomeError(f"{name}: {target!r} needs all of {keys}")
|
||||
exprs: list[str] = []
|
||||
for (_, type_, const_fn), value in zip(members, values, strict=True):
|
||||
if isinstance(value, Lambda):
|
||||
if isinstance(type_, str):
|
||||
type_ = cg.RawExpression(type_.format(parent=parent))
|
||||
inner = await cg.process_lambda(value, lambda_args, return_type=type_)
|
||||
expr = call_lambda(inner)
|
||||
bare = compare and isinstance(expr, cg.RawExpression)
|
||||
exprs.append(f"({expr})" if bare else str(expr))
|
||||
elif const_fn is not None:
|
||||
exprs.append(const_fn(config, value))
|
||||
else:
|
||||
exprs.append(str(cg.safe_exp(value)))
|
||||
return exprs
|
||||
|
||||
|
||||
def _apply_values(config: ConfigType, members: list[tuple[Any, Any, Any]]) -> list[Any]:
|
||||
return [_config_lookup(config, key) for key, _, _ in members]
|
||||
|
||||
|
||||
def register_apply_action(
|
||||
name: str,
|
||||
schema: cv.Schema,
|
||||
@@ -334,10 +394,14 @@ def register_apply_action(
|
||||
in, lambdas are called inline with the trigger args. With ``call`` every statement targets
|
||||
the call object ``auto apply_call = parent->call()``, and ``apply_call.perform()`` is appended.
|
||||
"""
|
||||
# An action stores the value, so a std::string constant stays in flash on ESP8266.
|
||||
statements_spec = [
|
||||
(
|
||||
c.target,
|
||||
[(arg[0], arg[1], arg[2] if len(arg) == 3 else None) for arg in c.args],
|
||||
[
|
||||
(key, t, fn or (flash_string if t is cg.std_string else None))
|
||||
for key, t, fn in c.members
|
||||
],
|
||||
)
|
||||
for c in (f if isinstance(f, ApplyCall) else f.call() for f in fields)
|
||||
]
|
||||
@@ -351,37 +415,17 @@ def register_apply_action(
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
# Global-scope qualified so a trigger arg named like the id cannot shadow it.
|
||||
parent = f"::{await cg.get_variable(config[CONF_ID])}"
|
||||
# Must match ApplyAction::ApplyFn exactly for the function pointer conversion.
|
||||
lambda_args = [
|
||||
(cg.RawExpression(f"const std::remove_cvref_t<{cg.safe_exp(t)}> &"), arg)
|
||||
for t, arg in args
|
||||
]
|
||||
parent = await _apply_parent(config)
|
||||
lambda_args = _apply_lambda_args(args)
|
||||
receiver = "apply_call." if call else f"{parent}->"
|
||||
statements: list[str] = []
|
||||
for target, members in statements_spec:
|
||||
values = [_config_lookup(config, key) for key, _, _ in members]
|
||||
values = _apply_values(config, members)
|
||||
if members and all(value is None for value in values):
|
||||
continue
|
||||
if any(value is None for value in values):
|
||||
keys = [key for key, _, _ in members]
|
||||
raise EsphomeError(f"{name}: {target!r} needs all of {keys}")
|
||||
exprs: list[str] = []
|
||||
for (_, type_, const_fn), value in zip(members, values, strict=True):
|
||||
if isinstance(value, Lambda):
|
||||
if isinstance(type_, str):
|
||||
type_ = cg.RawExpression(type_.format(parent=parent))
|
||||
inner = await cg.process_lambda(
|
||||
value, lambda_args, return_type=type_
|
||||
)
|
||||
exprs.append(str(call_lambda(inner)))
|
||||
elif const_fn is not None:
|
||||
exprs.append(const_fn(config, value))
|
||||
elif type_ is cg.std_string:
|
||||
exprs.append(flash_string(config, value))
|
||||
else:
|
||||
exprs.append(str(cg.safe_exp(value)))
|
||||
exprs = await _render_values(
|
||||
name, target, members, values, config, parent, lambda_args
|
||||
)
|
||||
statements.append(f"{receiver}{target.format(*exprs)};")
|
||||
if call:
|
||||
statements = [
|
||||
@@ -397,6 +441,51 @@ def register_apply_action(
|
||||
register_action(name, ApplyAction, schema, synchronous=True)(builder)
|
||||
|
||||
|
||||
def register_apply_condition(
|
||||
name: str, schema: cv.Schema, check: str | ApplyCall
|
||||
) -> None:
|
||||
"""Register a condition that is one expression on its parent, with no C++ class.
|
||||
|
||||
``check`` is applied to the parent: ``"is_playing()"`` becomes ``parent->is_playing()``; an
|
||||
``ApplyCall`` such as ``ApplyCall("state == {}", ((CONF_STATE, cg.bool_),))`` compares
|
||||
against config values, all of which must be present. Write ``== false`` to negate.
|
||||
String constants are plain literals, so compare a ``std::string`` or ``StringRef`` member.
|
||||
Generates one stateless function for ``ApplyCondition<Ts...>``.
|
||||
"""
|
||||
call = check if isinstance(check, ApplyCall) else ApplyCall(check)
|
||||
members = call.members
|
||||
for conf_key, _, _ in members:
|
||||
_check_key_in_schema(name, schema, conf_key)
|
||||
|
||||
async def builder(
|
||||
config: ConfigType,
|
||||
condition_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
parent = await _apply_parent(config)
|
||||
lambda_args = _apply_lambda_args(args)
|
||||
exprs = await _render_values(
|
||||
name,
|
||||
call.target,
|
||||
members,
|
||||
_apply_values(config, members),
|
||||
config,
|
||||
parent,
|
||||
lambda_args,
|
||||
compare=True,
|
||||
)
|
||||
check_lambda = LambdaExpression(
|
||||
[f"return {parent}->{call.target.format(*exprs)};"],
|
||||
lambda_args,
|
||||
capture="",
|
||||
return_type=cg.bool_,
|
||||
)
|
||||
return cg.new_Pvariable(condition_id, template_arg, check_lambda)
|
||||
|
||||
register_condition(name, ApplyCondition, schema)(builder)
|
||||
|
||||
|
||||
def validate_potentially_and_condition(value):
|
||||
if isinstance(value, list):
|
||||
with cv.remove_prepend_path(["and"]):
|
||||
@@ -643,28 +732,15 @@ async def for_condition_to_code(
|
||||
return var
|
||||
|
||||
|
||||
@register_condition(
|
||||
register_apply_condition(
|
||||
"component.is_idle",
|
||||
LambdaCondition,
|
||||
maybe_simple_id(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.use_id(cg.Component),
|
||||
}
|
||||
),
|
||||
"is_idle()",
|
||||
)
|
||||
async def component_is_idle_condition_to_code(
|
||||
config: ConfigType,
|
||||
condition_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
comp = await cg.get_variable(config[CONF_ID])
|
||||
lambda_ = await cg.process_lambda(
|
||||
Lambda(f"return {comp}->is_idle();"), args, return_type=bool
|
||||
)
|
||||
return new_lambda_pvariable(
|
||||
condition_id, lambda_, StatelessLambdaCondition, template_arg
|
||||
)
|
||||
|
||||
|
||||
@register_action(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import logging
|
||||
|
||||
from esphome import automation
|
||||
from esphome.automation import Condition, maybe_simple_id
|
||||
from esphome.automation import maybe_simple_id
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import mqtt, web_server
|
||||
import esphome.config_validation as cv
|
||||
@@ -36,7 +36,7 @@ from esphome.const import (
|
||||
DEVICE_CLASS_SHUTTER,
|
||||
DEVICE_CLASS_WINDOW,
|
||||
)
|
||||
from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority
|
||||
from esphome.core import CORE, CoroPriority, coroutine_with_priority
|
||||
from esphome.core.entity_helpers import (
|
||||
entity_duplicate_validator,
|
||||
queue_entity_register,
|
||||
@@ -44,7 +44,7 @@ from esphome.core.entity_helpers import (
|
||||
setup_entity,
|
||||
)
|
||||
from esphome.cpp_generator import MockObj, MockObjClass
|
||||
from esphome.types import ConfigType, SafeExpType, TemplateArgsType
|
||||
from esphome.types import ConfigType, SafeExpType
|
||||
|
||||
IS_PLATFORM_COMPONENT = True
|
||||
|
||||
@@ -87,8 +87,6 @@ COVER_OPERATIONS = {
|
||||
validate_cover_operation = cv.enum(COVER_OPERATIONS, upper=True)
|
||||
|
||||
# Actions
|
||||
CoverIsOpenCondition = cover_ns.class_("CoverIsOpenCondition", Condition)
|
||||
CoverIsClosedCondition = cover_ns.class_("CoverIsClosedCondition", Condition)
|
||||
CoverOpenedTrigger = cover_ns.class_(
|
||||
"CoverOpenedTrigger", automation.Trigger.template()
|
||||
)
|
||||
@@ -287,19 +285,12 @@ COVER_CONDITION_SCHEMA = cv.maybe_simple_value(
|
||||
)
|
||||
|
||||
|
||||
async def cover_condition_to_code(
|
||||
config: ConfigType, condition_id: ID, template_arg: MockObj, args: TemplateArgsType
|
||||
) -> MockObj:
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
return cg.new_Pvariable(condition_id, template_arg, paren)
|
||||
|
||||
|
||||
automation.register_condition(
|
||||
"cover.is_open", CoverIsOpenCondition, COVER_CONDITION_SCHEMA
|
||||
)(cover_condition_to_code)
|
||||
automation.register_condition(
|
||||
"cover.is_closed", CoverIsClosedCondition, COVER_CONDITION_SCHEMA
|
||||
)(cover_condition_to_code)
|
||||
automation.register_apply_condition(
|
||||
"cover.is_open", COVER_CONDITION_SCHEMA, f"position == {COVER_OPEN}"
|
||||
)
|
||||
automation.register_apply_condition(
|
||||
"cover.is_closed", COVER_CONDITION_SCHEMA, f"position == {COVER_CLOSED}"
|
||||
)
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.CORE)
|
||||
|
||||
@@ -6,19 +6,6 @@
|
||||
|
||||
namespace esphome::cover {
|
||||
|
||||
template<bool OPEN, typename... Ts> class CoverPositionCondition final : public Condition<Ts...> {
|
||||
public:
|
||||
CoverPositionCondition(Cover *cover) : cover_(cover) {}
|
||||
|
||||
bool check(const Ts &...x) override { return this->cover_->position == (OPEN ? COVER_OPEN : COVER_CLOSED); }
|
||||
|
||||
protected:
|
||||
Cover *cover_;
|
||||
};
|
||||
|
||||
template<typename... Ts> using CoverIsOpenCondition = CoverPositionCondition<true, Ts...>;
|
||||
template<typename... Ts> using CoverIsClosedCondition = CoverPositionCondition<false, Ts...>;
|
||||
|
||||
template<bool OPEN> class CoverPositionTrigger final : public Trigger<> {
|
||||
public:
|
||||
CoverPositionTrigger(Cover *a_cover) : cover_(a_cover) {
|
||||
|
||||
@@ -18,7 +18,6 @@ CONF_ON_FINISHED_PLAYBACK = "on_finished_playback"
|
||||
rtttl_ns = cg.esphome_ns.namespace("rtttl")
|
||||
|
||||
Rtttl = rtttl_ns.class_("Rtttl", cg.Component)
|
||||
IsPlayingCondition = rtttl_ns.class_("IsPlayingCondition", automation.Condition)
|
||||
|
||||
MULTI_CONF = True
|
||||
|
||||
@@ -120,12 +119,12 @@ automation.register_apply_action(
|
||||
)
|
||||
|
||||
|
||||
automation.register_parented_condition(
|
||||
automation.register_apply_condition(
|
||||
"rtttl.is_playing",
|
||||
IsPlayingCondition,
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(Rtttl),
|
||||
}
|
||||
),
|
||||
"is_playing()",
|
||||
)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
@@ -116,9 +115,4 @@ class Rtttl final : public Component {
|
||||
#endif
|
||||
};
|
||||
|
||||
template<typename... Ts> class IsPlayingCondition final : public Condition<Ts...>, public Parented<Rtttl> {
|
||||
public:
|
||||
bool check(const Ts &...x) override { return this->parent_->is_playing(); }
|
||||
};
|
||||
|
||||
} // namespace esphome::rtttl
|
||||
|
||||
@@ -267,6 +267,18 @@ template<typename... Ts> class ApplyAction final : public Action<Ts...> {
|
||||
ApplyFn apply_;
|
||||
};
|
||||
|
||||
/// Condition counterpart of ApplyAction: one codegen-generated predicate with the parent baked in.
|
||||
template<typename... Ts> class ApplyCondition final : public Condition<Ts...> {
|
||||
public:
|
||||
using CheckFn = bool (*)(const std::remove_cvref_t<Ts> &...);
|
||||
explicit ApplyCondition(CheckFn check) : check_(check) {}
|
||||
|
||||
bool check(const Ts &...x) override { return this->check_(x...); }
|
||||
|
||||
protected:
|
||||
CheckFn check_;
|
||||
};
|
||||
|
||||
/// Simple continuation action that calls play_next_ on a parent action.
|
||||
/// Used internally by IfAction, WhileAction, RepeatAction, etc. to chain actions.
|
||||
/// Memory: 4-8 bytes (parent pointer) vs 40 bytes (LambdaAction with std::function).
|
||||
|
||||
@@ -551,11 +551,17 @@ text:
|
||||
update_interval: 1s
|
||||
lambda: |
|
||||
return std::string{"Hello!"};
|
||||
# Exercise a register_apply_condition inside a trigger with a std::string
|
||||
# argument, so ApplyCondition<std::string> is compiled.
|
||||
set_action:
|
||||
then:
|
||||
- logger.log:
|
||||
format: Template Text set to %s
|
||||
args: ["x.c_str()"]
|
||||
- if:
|
||||
condition:
|
||||
cover.is_open: template_cover_with_triggers
|
||||
then:
|
||||
- logger.log:
|
||||
format: Template Text set to %s
|
||||
args: ["x.c_str()"]
|
||||
|
||||
alarm_control_panel:
|
||||
- platform: template
|
||||
|
||||
@@ -10,6 +10,7 @@ import pytest
|
||||
from esphome.automation import (
|
||||
ApplyAction,
|
||||
ApplyCall,
|
||||
ApplyCondition,
|
||||
ApplyField,
|
||||
CallbackAutomation,
|
||||
TriggerForwarder,
|
||||
@@ -19,6 +20,7 @@ from esphome.automation import (
|
||||
has_non_synchronous_actions,
|
||||
maybe_simple_id,
|
||||
register_apply_action,
|
||||
register_apply_condition,
|
||||
register_bare_action,
|
||||
register_bare_condition,
|
||||
register_parented_action,
|
||||
@@ -602,6 +604,20 @@ def test_shared_builders_keep_synchronous_flag(
|
||||
assert actions["my.parented"].synchronous is synchronous
|
||||
|
||||
|
||||
async def _run_entry(
|
||||
entry: RegistryEntry,
|
||||
config: dict[str, object],
|
||||
args: list[tuple[object, str]] | None,
|
||||
platform: str,
|
||||
) -> RegistryEntry:
|
||||
"""Run a registered builder with the given config, trigger args and platform."""
|
||||
CORE.data[KEY_CORE] = {KEY_TARGET_PLATFORM: platform}
|
||||
args = args or []
|
||||
template_arg = cg.TemplateArguments(*(t for t, _ in args))
|
||||
await entry.fun({CONF_ID: PARENT_ID, **config}, ID("obj_1"), template_arg, args)
|
||||
return entry
|
||||
|
||||
|
||||
async def _run_apply_action(
|
||||
registries: tuple[Registry, Registry],
|
||||
fields: tuple[ApplyField | ApplyCall, ...],
|
||||
@@ -611,14 +627,22 @@ async def _run_apply_action(
|
||||
platform: str = "esp32",
|
||||
) -> RegistryEntry:
|
||||
"""Register an apply action and run its builder with the given config."""
|
||||
CORE.data[KEY_CORE] = {KEY_TARGET_PLATFORM: platform}
|
||||
actions, _ = registries
|
||||
register_apply_action("my.apply", None, *fields, call=call)
|
||||
entry = actions["my.apply"]
|
||||
args = args or []
|
||||
template_arg = cg.TemplateArguments(*(t for t, _ in args))
|
||||
await entry.fun({CONF_ID: PARENT_ID, **config}, ID("obj_1"), template_arg, args)
|
||||
return entry
|
||||
return await _run_entry(actions["my.apply"], config, args, platform)
|
||||
|
||||
|
||||
async def _run_apply_condition(
|
||||
registries: tuple[Registry, Registry],
|
||||
check: str | ApplyCall,
|
||||
config: dict[str, object],
|
||||
args: list[tuple[object, str]] | None = None,
|
||||
platform: str = "esp32",
|
||||
) -> RegistryEntry:
|
||||
"""Register an apply condition and run its builder with the given config."""
|
||||
_, conditions = registries
|
||||
register_apply_condition("my.check", None, check)
|
||||
return await _run_entry(conditions["my.check"], config, args, platform)
|
||||
|
||||
|
||||
def _apply_lambda(mock_cg: MockCodegen) -> str:
|
||||
@@ -774,6 +798,13 @@ def test_apply_registration_checks(registries: tuple[Registry, Registry]) -> Non
|
||||
register_apply_action("my.ok", schema, ApplyField("kp", "set_kp", cg.float_))
|
||||
with pytest.raises(ValueError, match="'kd' is not in the schema"):
|
||||
register_apply_action("my.bad", schema, ApplyField("kd", "set_kd", cg.float_))
|
||||
register_apply_condition(
|
||||
"my.is", schema, ApplyCall("kp == {}", (("kp", cg.float_),))
|
||||
)
|
||||
with pytest.raises(ValueError, match="'kd' is not in the schema"):
|
||||
register_apply_condition(
|
||||
"my.bad_is", schema, ApplyCall("kd == {}", (("kd", cg.float_),))
|
||||
)
|
||||
either = cv.Any(schema, cv.Schema({cv.Optional("kd"): cv.float_}))
|
||||
register_apply_action("my.any", either, ApplyField("kd", "set_kd", cg.float_))
|
||||
for wrapped in (
|
||||
@@ -805,3 +836,67 @@ async def test_apply_string_constant_stays_in_flash_on_esp8266(
|
||||
assert f'::{PARENT_OBJ}->play(progmem_string(ESPHOME_F("a:b")));' in _apply_lambda(
|
||||
mock_cg
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_register_apply_condition_predicate(
|
||||
registries: tuple[Registry, Registry], mock_cg: MockCodegen
|
||||
) -> None:
|
||||
entry = await _run_apply_condition(
|
||||
registries, "is_playing()", {}, args=[(cg.int32, "x")]
|
||||
)
|
||||
assert entry.type_id is ApplyCondition
|
||||
condition_id, template_arg, check = mock_cg.new_pvariable.call_args.args
|
||||
assert condition_id == ID("obj_1")
|
||||
assert str(template_arg) == "<int32_t>"
|
||||
text = str(check)
|
||||
assert text.startswith("[](const std::remove_cvref_t<int32_t> & x) -> bool {")
|
||||
assert f"return ::{PARENT_OBJ}->is_playing();" in text
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_apply_condition_compares_config_value(
|
||||
registries: tuple[Registry, Registry], mock_cg: MockCodegen
|
||||
) -> None:
|
||||
check = ApplyCall("state == {}", (("state", cg.bool_),))
|
||||
await _run_apply_condition(registries, check, {"state": True})
|
||||
assert f"return ::{PARENT_OBJ}->state == true;" in _apply_lambda(mock_cg)
|
||||
|
||||
with pytest.raises(EsphomeError, match="needs all of"):
|
||||
await _run_apply_condition(registries, check, {})
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("platform", ["esp32", "esp8266"])
|
||||
async def test_apply_condition_string_constant_is_a_plain_literal(
|
||||
registries: tuple[Registry, Registry], mock_cg: MockCodegen, platform: str
|
||||
) -> None:
|
||||
check = ApplyCall("state == {}", (("state", cg.std_string),))
|
||||
await _run_apply_condition(registries, check, {"state": "two"}, platform=platform)
|
||||
assert f'return ::{PARENT_OBJ}->state == "two";' in _apply_lambda(mock_cg)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
("body", "expected", "called"),
|
||||
[
|
||||
("return x;", "->state == (x);", False),
|
||||
('return x.empty() ? "e" : x;', '->state == (x.empty() ? "e" : x);', False),
|
||||
('if (x.empty()) return "e";\nreturn x;', "}(x);", True),
|
||||
],
|
||||
)
|
||||
async def test_apply_condition_string_lambda_paths(
|
||||
registries: tuple[Registry, Registry],
|
||||
mock_cg: MockCodegen,
|
||||
body: str,
|
||||
expected: str,
|
||||
called: bool,
|
||||
) -> None:
|
||||
"""A single return is inlined with no copy; a longer body is a called std::string lambda."""
|
||||
check = ApplyCall("state == {}", (("state", cg.std_string),))
|
||||
await _run_apply_condition(
|
||||
registries, check, {"state": Lambda(body)}, args=[(cg.std_string, "x")]
|
||||
)
|
||||
text = _apply_lambda(mock_cg)
|
||||
assert expected in text
|
||||
assert ("-> std::string {" in text) is called
|
||||
|
||||
Reference in New Issue
Block a user