[sensor][number] Test in_range with every bound combination (#20847)

This commit is contained in:
J. Nick Koston
2026-10-17 15:20:52 -10:00
committed by GitHub
parent 68fa7b9847
commit 626d84bc64
3 changed files with 252 additions and 0 deletions
@@ -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"
@@ -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()]
@@ -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