diff --git a/esphome/automation.py b/esphome/automation.py index 7fdeb904ec6..94ea912d01d 100644 --- a/esphome/automation.py +++ b/esphome/automation.py @@ -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) diff --git a/esphome/components/number/__init__.py b/esphome/components/number/__init__.py index 8d507abcdd6..8e9e5d2d792 100644 --- a/esphome/components/number/__init__.py +++ b/esphome/components/number/__init__.py @@ -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, ) diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index ab7e21739e4..3f1a92c6219 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -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, ) diff --git a/tests/unit_tests/test_automation.py b/tests/unit_tests/test_automation.py index 6b90dcc33f7..5eab329d09c 100644 --- a/tests/unit_tests/test_automation.py +++ b/tests/unit_tests/test_automation.py @@ -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])