Fill the in_range bounds at code generation so the validated config is unchanged

This commit is contained in:
J. Nick Koston
2026-10-15 14:17:49 -10:00
parent f4d640bbd2
commit 8fcd37c2ef
4 changed files with 20 additions and 15 deletions
+10 -7
View File
@@ -355,11 +355,6 @@ class ApplyField:
return ApplyCall(target, ((self.conf_key, self.type_, self.const_fn),))
def fill_in_range_bounds(config: ConfigType) -> ConfigType:
"""Give an absent ``above`` or ``below`` the value NaN, which ``IN_RANGE_CHECK`` renders."""
return {CONF_ABOVE: math.nan, CONF_BELOW: math.nan, **config}
def _in_range_low(config: ConfigType, value: float) -> str:
return "-INFINITY" if math.isnan(value) else str(cg.safe_exp(value))
@@ -371,11 +366,12 @@ def _in_range_high(config: ConfigType, value: float) -> str:
return str(cg.safe_exp(value))
# ``above <= state <= below`` on the parent's float ``state``, after ``fill_in_range_bounds``.
# ``above <= state <= below`` on the parent's float ``state``; register with ``IN_RANGE_DEFAULTS``.
IN_RANGE_CHECK = ApplyCall(
"state >= {} && {parent}->state <= {}",
((CONF_ABOVE, cg.float_, _in_range_low), (CONF_BELOW, cg.float_, _in_range_high)),
)
IN_RANGE_DEFAULTS = {CONF_ABOVE: math.nan, CONF_BELOW: math.nan}
def _config_lookup(config: ConfigType, key: str | tuple[str, ...]) -> Any:
@@ -596,7 +592,11 @@ async def _render_check(
def register_apply_condition(
name: str, schema: cv.Schema, check: str | ApplyCall, id_key: str = CONF_ID
name: str,
schema: cv.Schema,
check: str | ApplyCall,
id_key: str = CONF_ID,
defaults: ConfigType | None = None,
) -> None:
"""Register a condition that is one expression on its parent, with no C++ class.
@@ -605,6 +605,7 @@ def register_apply_condition(
against config values, all of which must be present. Write ``== false`` to negate.
A ``cg.std_string`` constant stays in flash on ESP8266 and supports only ``==`` or ``!=``
against a ``std::string`` or ``StringRef`` member; other string types are plain literals.
``defaults`` fills absent keys at code generation only, so the validated config is unchanged.
Generates one static predicate and an ``ApplyCondition`` templated on it.
"""
call = check if isinstance(check, ApplyCall) else ApplyCall(check)
@@ -619,6 +620,8 @@ def register_apply_condition(
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
if defaults:
config = {**defaults, **config}
parent = await _apply_parent(config, id_key)
lambda_args = _apply_lambda_args(args)
values = _apply_values(config, members)
+4 -4
View File
@@ -166,8 +166,6 @@ ValueRangeTrigger = number_ns.class_(
"ValueRangeTrigger", automation.Trigger.template(cg.float_), cg.Component
)
# Conditions
NumberMode = number_ns.enum("NumberMode")
# Schema default that also matches the C++ initializer in number_traits.h; codegen
@@ -339,12 +337,14 @@ NUMBER_IN_RANGE_CONDITION_SCHEMA = cv.All(
cv.Optional(CONF_BELOW): cv.float_,
},
cv.has_at_least_one_key(CONF_ABOVE, CONF_BELOW),
automation.fill_in_range_bounds,
)
automation.register_apply_condition(
"number.in_range", NUMBER_IN_RANGE_CONDITION_SCHEMA, automation.IN_RANGE_CHECK
"number.in_range",
NUMBER_IN_RANGE_CONDITION_SCHEMA,
automation.IN_RANGE_CHECK,
defaults=automation.IN_RANGE_DEFAULTS,
)
+4 -2
View File
@@ -1106,12 +1106,14 @@ SENSOR_IN_RANGE_CONDITION_SCHEMA = cv.All(
cv.Optional(CONF_BELOW): cv.float_,
},
cv.has_at_least_one_key(CONF_ABOVE, CONF_BELOW),
automation.fill_in_range_bounds,
)
automation.register_apply_condition(
"sensor.in_range", SENSOR_IN_RANGE_CONDITION_SCHEMA, automation.IN_RANGE_CHECK
"sensor.in_range",
SENSOR_IN_RANGE_CONDITION_SCHEMA,
automation.IN_RANGE_CHECK,
defaults=automation.IN_RANGE_DEFAULTS,
)
+2 -2
View File
@@ -9,6 +9,7 @@ import pytest
from esphome.automation import (
IN_RANGE_CHECK,
IN_RANGE_DEFAULTS,
ApplyAction,
ApplyCall,
ApplyCondition,
@@ -26,7 +27,6 @@ from esphome.automation import (
build_parent_callback_automation,
build_trigger_automations,
build_trigger_callback,
fill_in_range_bounds,
has_non_synchronous_actions,
literal_with_length,
maybe_simple_id,
@@ -1613,7 +1613,7 @@ async def test_nested_repeat_lambda_parameters_are_distinct() -> None:
def _render_in_range(config: dict) -> tuple[str, str]:
config = fill_in_range_bounds(config)
config = {**IN_RANGE_DEFAULTS, **config}
(above_key, _, low), (below_key, _, high) = IN_RANGE_CHECK.members
return low(config, config[above_key]), high(config, config[below_key])