Don't raise on known non-matching states in numeric state condition (#47378)
This commit is contained in:
parent
f05f60c4c4
commit
7a8b7224c8
3 changed files with 48 additions and 106 deletions
|
@ -412,10 +412,9 @@ def async_numeric_state(
|
||||||
"numeric_state", f"template error: {ex}"
|
"numeric_state", f"template error: {ex}"
|
||||||
) from ex
|
) from ex
|
||||||
|
|
||||||
|
# Known states that never match the numeric condition
|
||||||
if value in (STATE_UNAVAILABLE, STATE_UNKNOWN):
|
if value in (STATE_UNAVAILABLE, STATE_UNKNOWN):
|
||||||
raise ConditionErrorMessage(
|
return False
|
||||||
"numeric_state", f"state of {entity_id} is unavailable"
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
fvalue = float(value)
|
fvalue = float(value)
|
||||||
|
@ -428,13 +427,15 @@ def async_numeric_state(
|
||||||
if below is not None:
|
if below is not None:
|
||||||
if isinstance(below, str):
|
if isinstance(below, str):
|
||||||
below_entity = hass.states.get(below)
|
below_entity = hass.states.get(below)
|
||||||
if not below_entity or below_entity.state in (
|
if not below_entity:
|
||||||
|
raise ConditionErrorMessage(
|
||||||
|
"numeric_state", f"unknown 'below' entity {below}"
|
||||||
|
)
|
||||||
|
if below_entity.state in (
|
||||||
STATE_UNAVAILABLE,
|
STATE_UNAVAILABLE,
|
||||||
STATE_UNKNOWN,
|
STATE_UNKNOWN,
|
||||||
):
|
):
|
||||||
raise ConditionErrorMessage(
|
return False
|
||||||
"numeric_state", f"the 'below' entity {below} is unavailable"
|
|
||||||
)
|
|
||||||
try:
|
try:
|
||||||
if fvalue >= float(below_entity.state):
|
if fvalue >= float(below_entity.state):
|
||||||
condition_trace_set_result(
|
condition_trace_set_result(
|
||||||
|
@ -455,13 +456,15 @@ def async_numeric_state(
|
||||||
if above is not None:
|
if above is not None:
|
||||||
if isinstance(above, str):
|
if isinstance(above, str):
|
||||||
above_entity = hass.states.get(above)
|
above_entity = hass.states.get(above)
|
||||||
if not above_entity or above_entity.state in (
|
if not above_entity:
|
||||||
|
raise ConditionErrorMessage(
|
||||||
|
"numeric_state", f"unknown 'above' entity {above}"
|
||||||
|
)
|
||||||
|
if above_entity.state in (
|
||||||
STATE_UNAVAILABLE,
|
STATE_UNAVAILABLE,
|
||||||
STATE_UNKNOWN,
|
STATE_UNKNOWN,
|
||||||
):
|
):
|
||||||
raise ConditionErrorMessage(
|
return False
|
||||||
"numeric_state", f"the 'above' entity {above} is unavailable"
|
|
||||||
)
|
|
||||||
try:
|
try:
|
||||||
if fvalue <= float(above_entity.state):
|
if fvalue <= float(above_entity.state):
|
||||||
condition_trace_set_result(
|
condition_trace_set_result(
|
||||||
|
|
|
@ -10,12 +10,7 @@ import homeassistant.components.automation as automation
|
||||||
from homeassistant.components.homeassistant.triggers import (
|
from homeassistant.components.homeassistant.triggers import (
|
||||||
numeric_state as numeric_state_trigger,
|
numeric_state as numeric_state_trigger,
|
||||||
)
|
)
|
||||||
from homeassistant.const import (
|
from homeassistant.const import ATTR_ENTITY_ID, ENTITY_MATCH_ALL, SERVICE_TURN_OFF
|
||||||
ATTR_ENTITY_ID,
|
|
||||||
ENTITY_MATCH_ALL,
|
|
||||||
SERVICE_TURN_OFF,
|
|
||||||
STATE_UNAVAILABLE,
|
|
||||||
)
|
|
||||||
from homeassistant.core import Context
|
from homeassistant.core import Context
|
||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
import homeassistant.util.dt as dt_util
|
import homeassistant.util.dt as dt_util
|
||||||
|
@ -347,52 +342,6 @@ async def test_if_fires_on_entity_unavailable_at_startup(hass, calls):
|
||||||
assert len(calls) == 0
|
assert len(calls) == 0
|
||||||
|
|
||||||
|
|
||||||
async def test_if_not_fires_on_entity_unavailable(hass, calls):
|
|
||||||
"""Test the firing with entity changing to unavailable."""
|
|
||||||
# set initial state
|
|
||||||
hass.states.async_set("test.entity", 9)
|
|
||||||
await hass.async_block_till_done()
|
|
||||||
|
|
||||||
assert await async_setup_component(
|
|
||||||
hass,
|
|
||||||
automation.DOMAIN,
|
|
||||||
{
|
|
||||||
automation.DOMAIN: {
|
|
||||||
"trigger": {
|
|
||||||
"platform": "numeric_state",
|
|
||||||
"entity_id": "test.entity",
|
|
||||||
"above": 10,
|
|
||||||
},
|
|
||||||
"action": {"service": "test.automation"},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
# 11 is above 10
|
|
||||||
hass.states.async_set("test.entity", 11)
|
|
||||||
await hass.async_block_till_done()
|
|
||||||
assert len(calls) == 1
|
|
||||||
|
|
||||||
# Going to unavailable and back should not fire
|
|
||||||
hass.states.async_set("test.entity", STATE_UNAVAILABLE)
|
|
||||||
await hass.async_block_till_done()
|
|
||||||
assert len(calls) == 1
|
|
||||||
hass.states.async_set("test.entity", 11)
|
|
||||||
await hass.async_block_till_done()
|
|
||||||
assert len(calls) == 1
|
|
||||||
|
|
||||||
# Crossing threshold via unavailable should fire
|
|
||||||
hass.states.async_set("test.entity", 9)
|
|
||||||
await hass.async_block_till_done()
|
|
||||||
assert len(calls) == 1
|
|
||||||
hass.states.async_set("test.entity", STATE_UNAVAILABLE)
|
|
||||||
await hass.async_block_till_done()
|
|
||||||
assert len(calls) == 1
|
|
||||||
hass.states.async_set("test.entity", 11)
|
|
||||||
await hass.async_block_till_done()
|
|
||||||
assert len(calls) == 2
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("above", (10, "input_number.value_10"))
|
@pytest.mark.parametrize("above", (10, "input_number.value_10"))
|
||||||
async def test_if_fires_on_entity_change_below_to_above(hass, calls, above):
|
async def test_if_fires_on_entity_change_below_to_above(hass, calls, above):
|
||||||
"""Test the firing with changed entity."""
|
"""Test the firing with changed entity."""
|
||||||
|
@ -1522,7 +1471,7 @@ async def test_if_fires_on_change_with_for_template_3(hass, calls, above, below)
|
||||||
assert len(calls) == 1
|
assert len(calls) == 1
|
||||||
|
|
||||||
|
|
||||||
async def test_if_not_fires_on_error_with_for_template(hass, caplog, calls):
|
async def test_if_not_fires_on_error_with_for_template(hass, calls):
|
||||||
"""Test for not firing on error with for template."""
|
"""Test for not firing on error with for template."""
|
||||||
hass.states.async_set("test.entity", 0)
|
hass.states.async_set("test.entity", 0)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
@ -1547,17 +1496,11 @@ async def test_if_not_fires_on_error_with_for_template(hass, caplog, calls):
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
assert len(calls) == 0
|
assert len(calls) == 0
|
||||||
|
|
||||||
caplog.clear()
|
|
||||||
caplog.set_level(logging.WARNING)
|
|
||||||
|
|
||||||
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=3))
|
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=3))
|
||||||
hass.states.async_set("test.entity", "unavailable")
|
hass.states.async_set("test.entity", "unavailable")
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
assert len(calls) == 0
|
assert len(calls) == 0
|
||||||
|
|
||||||
assert len(caplog.record_tuples) == 1
|
|
||||||
assert caplog.record_tuples[0][1] == logging.WARNING
|
|
||||||
|
|
||||||
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=3))
|
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=3))
|
||||||
hass.states.async_set("test.entity", 101)
|
hass.states.async_set("test.entity", 101)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
"""Test the condition helper."""
|
"""Test the condition helper."""
|
||||||
from logging import WARNING
|
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
@ -693,27 +692,6 @@ async def test_time_using_input_datetime(hass):
|
||||||
condition.time(hass, before="input_datetime.not_existing")
|
condition.time(hass, before="input_datetime.not_existing")
|
||||||
|
|
||||||
|
|
||||||
async def test_if_numeric_state_raises_on_unavailable(hass, caplog):
|
|
||||||
"""Test numeric_state raises on unavailable/unknown state."""
|
|
||||||
test = await condition.async_from_config(
|
|
||||||
hass,
|
|
||||||
{"condition": "numeric_state", "entity_id": "sensor.temperature", "below": 42},
|
|
||||||
)
|
|
||||||
|
|
||||||
caplog.clear()
|
|
||||||
caplog.set_level(WARNING)
|
|
||||||
|
|
||||||
hass.states.async_set("sensor.temperature", "unavailable")
|
|
||||||
with pytest.raises(ConditionError):
|
|
||||||
test(hass)
|
|
||||||
assert len(caplog.record_tuples) == 0
|
|
||||||
|
|
||||||
hass.states.async_set("sensor.temperature", "unknown")
|
|
||||||
with pytest.raises(ConditionError):
|
|
||||||
test(hass)
|
|
||||||
assert len(caplog.record_tuples) == 0
|
|
||||||
|
|
||||||
|
|
||||||
async def test_state_raises(hass):
|
async def test_state_raises(hass):
|
||||||
"""Test that state raises ConditionError on errors."""
|
"""Test that state raises ConditionError on errors."""
|
||||||
# No entity
|
# No entity
|
||||||
|
@ -961,6 +939,26 @@ async def test_state_using_input_entities(hass):
|
||||||
assert test(hass)
|
assert test(hass)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_numeric_state_known_non_matching(hass):
|
||||||
|
"""Test that numeric_state doesn't match on known non-matching states."""
|
||||||
|
hass.states.async_set("sensor.temperature", "unavailable")
|
||||||
|
test = await condition.async_from_config(
|
||||||
|
hass,
|
||||||
|
{
|
||||||
|
"condition": "numeric_state",
|
||||||
|
"entity_id": "sensor.temperature",
|
||||||
|
"above": 0,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Unavailable state
|
||||||
|
assert not test(hass)
|
||||||
|
|
||||||
|
# Unknown state
|
||||||
|
hass.states.async_set("sensor.temperature", "unknown")
|
||||||
|
assert not test(hass)
|
||||||
|
|
||||||
|
|
||||||
async def test_numeric_state_raises(hass):
|
async def test_numeric_state_raises(hass):
|
||||||
"""Test that numeric_state raises ConditionError on errors."""
|
"""Test that numeric_state raises ConditionError on errors."""
|
||||||
# Unknown entities
|
# Unknown entities
|
||||||
|
@ -1007,20 +1005,6 @@ async def test_numeric_state_raises(hass):
|
||||||
hass.states.async_set("sensor.temperature", 50)
|
hass.states.async_set("sensor.temperature", 50)
|
||||||
test(hass)
|
test(hass)
|
||||||
|
|
||||||
# Unavailable state
|
|
||||||
with pytest.raises(ConditionError, match="state of .* is unavailable"):
|
|
||||||
test = await condition.async_from_config(
|
|
||||||
hass,
|
|
||||||
{
|
|
||||||
"condition": "numeric_state",
|
|
||||||
"entity_id": "sensor.temperature",
|
|
||||||
"above": 0,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
hass.states.async_set("sensor.temperature", "unavailable")
|
|
||||||
test(hass)
|
|
||||||
|
|
||||||
# Bad number
|
# Bad number
|
||||||
with pytest.raises(ConditionError, match="cannot be processed as a number"):
|
with pytest.raises(ConditionError, match="cannot be processed as a number"):
|
||||||
test = await condition.async_from_config(
|
test = await condition.async_from_config(
|
||||||
|
@ -1182,6 +1166,12 @@ async def test_numeric_state_using_input_number(hass):
|
||||||
hass.states.async_set("sensor.temperature", 100)
|
hass.states.async_set("sensor.temperature", 100)
|
||||||
assert not test(hass)
|
assert not test(hass)
|
||||||
|
|
||||||
|
hass.states.async_set("input_number.high", "unknown")
|
||||||
|
assert not test(hass)
|
||||||
|
|
||||||
|
hass.states.async_set("input_number.high", "unavailable")
|
||||||
|
assert not test(hass)
|
||||||
|
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
"input_number",
|
"input_number",
|
||||||
"set_value",
|
"set_value",
|
||||||
|
@ -1193,6 +1183,12 @@ async def test_numeric_state_using_input_number(hass):
|
||||||
)
|
)
|
||||||
assert test(hass)
|
assert test(hass)
|
||||||
|
|
||||||
|
hass.states.async_set("input_number.low", "unknown")
|
||||||
|
assert not test(hass)
|
||||||
|
|
||||||
|
hass.states.async_set("input_number.low", "unavailable")
|
||||||
|
assert not test(hass)
|
||||||
|
|
||||||
with pytest.raises(ConditionError):
|
with pytest.raises(ConditionError):
|
||||||
condition.async_numeric_state(
|
condition.async_numeric_state(
|
||||||
hass, entity="sensor.temperature", below="input_number.not_exist"
|
hass, entity="sensor.temperature", below="input_number.not_exist"
|
||||||
|
|
Loading…
Add table
Reference in a new issue