diff --git a/tests/components/template/common-base.yaml b/tests/components/template/common-base.yaml index f87a4e0320e..76a5e0b1a2f 100644 --- a/tests/components/template/common-base.yaml +++ b/tests/components/template/common-base.yaml @@ -76,6 +76,38 @@ esphome: date: "2021-01-01" binary_sensor: + - platform: template + id: template_sensor_above + condition: + sensor.in_range: + id: template_template_sens + above: 10.0 + - platform: template + id: template_sensor_between + condition: + sensor.in_range: + id: template_template_sens + above: 10.0 + below: 30.0 + - platform: template + id: template_number_above + condition: + number.in_range: + id: template_number + above: 10.0 + - platform: template + id: template_number_below + condition: + number.in_range: + id: template_number + below: 90.0 + - platform: template + id: template_number_between + condition: + number.in_range: + id: template_number + above: 10.0 + below: 90.0 - platform: template id: template_some_binary_sensor name: "Garage Door Open" diff --git a/tests/integration/fixtures/in_range_conditions.yaml b/tests/integration/fixtures/in_range_conditions.yaml new file mode 100644 index 00000000000..f7f636aa584 --- /dev/null +++ b/tests/integration/fixtures/in_range_conditions.yaml @@ -0,0 +1,158 @@ +esphome: + name: test-in-range-conditions + +host: + +api: + actions: + - action: run_cases + then: + - script.execute: + id: check + label: neg + value: !lambda "return -10.0f;" + - script.wait: check + - script.execute: + id: check + label: low + value: !lambda "return 0.0f;" + - script.wait: check + - script.execute: + id: check + label: mid + value: !lambda "return 5.0f;" + - script.wait: check + - script.execute: + id: check + label: high + value: !lambda "return 10.0f;" + - script.wait: check + - script.execute: + id: check + label: over + value: !lambda "return 20.0f;" + - script.wait: check + - script.execute: + id: check + label: nan + value: !lambda "return NAN;" + - script.wait: check + - script.execute: + id: check + label: neginf + value: !lambda "return -INFINITY;" + - script.wait: check + - script.execute: + id: check + label: posinf + value: !lambda "return INFINITY;" + - script.wait: check + - logger.log: "INRANGE done" + +logger: + level: DEBUG + +sensor: + - platform: template + id: range_sensor + name: Range Sensor + +number: + - platform: template + id: range_number + name: Range Number + optimistic: true + min_value: -100 + max_value: 100 + step: 0.5 + +script: + - id: check + parameters: + label: string + value: float + then: + # Set the states directly so NaN and infinities reach the conditions unchanged + - lambda: |- + id(range_sensor).publish_state(value); + id(range_number).publish_state(value); + - if: + condition: + sensor.in_range: + id: range_sensor + above: 0 + then: + - logger.log: + format: "INRANGE %s sensor_above 1" + args: [label.c_str()] + else: + - logger.log: + format: "INRANGE %s sensor_above 0" + args: [label.c_str()] + - if: + condition: + sensor.in_range: + id: range_sensor + below: 10 + then: + - logger.log: + format: "INRANGE %s sensor_below 1" + args: [label.c_str()] + else: + - logger.log: + format: "INRANGE %s sensor_below 0" + args: [label.c_str()] + - if: + condition: + sensor.in_range: + id: range_sensor + above: 0 + below: 10 + then: + - logger.log: + format: "INRANGE %s sensor_both 1" + args: [label.c_str()] + else: + - logger.log: + format: "INRANGE %s sensor_both 0" + args: [label.c_str()] + - if: + condition: + number.in_range: + id: range_number + above: 0 + then: + - logger.log: + format: "INRANGE %s number_above 1" + args: [label.c_str()] + else: + - logger.log: + format: "INRANGE %s number_above 0" + args: [label.c_str()] + - if: + condition: + number.in_range: + id: range_number + below: 10 + then: + - logger.log: + format: "INRANGE %s number_below 1" + args: [label.c_str()] + else: + - logger.log: + format: "INRANGE %s number_below 0" + args: [label.c_str()] + - if: + condition: + number.in_range: + id: range_number + above: 0 + below: 10 + then: + - logger.log: + format: "INRANGE %s number_both 1" + args: [label.c_str()] + else: + - logger.log: + format: "INRANGE %s number_both 0" + args: [label.c_str()] diff --git a/tests/integration/test_in_range_conditions.py b/tests/integration/test_in_range_conditions.py new file mode 100644 index 00000000000..667f7fe43d4 --- /dev/null +++ b/tests/integration/test_in_range_conditions.py @@ -0,0 +1,62 @@ +"""Test sensor.in_range and number.in_range against a fixed truth table.""" + +from __future__ import annotations + +import asyncio +import re + +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + +# Bounds in the fixture: above 0, below 10, both inclusive. A NaN state is never in range. +# Columns: above only, below only, both bounds. +EXPECTED = { + "neg": (0, 1, 0), # -10 + "low": (1, 1, 1), # 0, the lower bound + "mid": (1, 1, 1), # 5 + "high": (1, 1, 1), # 10, the upper bound + "over": (1, 0, 0), # 20 + "nan": (0, 0, 0), + "neginf": (0, 1, 0), + "posinf": (1, 0, 0), +} +CONDITIONS = ("above", "below", "both") +RESULT_RE = re.compile(r"INRANGE (\w+) (sensor|number)_(above|below|both) ([01])") + + +@pytest.mark.asyncio +async def test_in_range_conditions( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Each in_range condition gives the same result for sensors and numbers.""" + loop = asyncio.get_running_loop() + results: dict[tuple[str, str, str], int] = {} + done = loop.create_future() + + def check_output(line: str) -> None: + if match := RESULT_RE.search(line): + label, entity, condition, value = match.groups() + results[(label, entity, condition)] = int(value) + elif "INRANGE done" in line and not done.done(): + done.set_result(True) + + async with ( + run_compiled(yaml_config, line_callback=check_output), + api_client_connected() as client, + ): + _, services = await client.list_entities_services() + service = next((s for s in services if s.name == "run_cases"), None) + assert service is not None, "run_cases action not found" + await client.execute_service(service, {}) + await asyncio.wait_for(done, timeout=10.0) + + expected = { + (label, entity, condition): row[index] + for label, row in EXPECTED.items() + for entity in ("sensor", "number") + for index, condition in enumerate(CONDITIONS) + } + assert results == expected