From 7a8b7224c8639b7e7f3fd22e2e596f15ac23c53d Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 4 Mar 2021 22:09:08 +0100 Subject: [PATCH] Don't raise on known non-matching states in numeric state condition (#47378) --- homeassistant/helpers/condition.py | 25 ++++--- .../triggers/test_numeric_state.py | 61 +---------------- tests/helpers/test_condition.py | 68 +++++++++---------- 3 files changed, 48 insertions(+), 106 deletions(-) diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py index 1abbf550bb1..bc1ff21b9cc 100644 --- a/homeassistant/helpers/condition.py +++ b/homeassistant/helpers/condition.py @@ -412,10 +412,9 @@ def async_numeric_state( "numeric_state", f"template error: {ex}" ) from ex + # Known states that never match the numeric condition if value in (STATE_UNAVAILABLE, STATE_UNKNOWN): - raise ConditionErrorMessage( - "numeric_state", f"state of {entity_id} is unavailable" - ) + return False try: fvalue = float(value) @@ -428,13 +427,15 @@ def async_numeric_state( if below is not None: if isinstance(below, str): 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_UNKNOWN, ): - raise ConditionErrorMessage( - "numeric_state", f"the 'below' entity {below} is unavailable" - ) + return False try: if fvalue >= float(below_entity.state): condition_trace_set_result( @@ -455,13 +456,15 @@ def async_numeric_state( if above is not None: if isinstance(above, str): 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_UNKNOWN, ): - raise ConditionErrorMessage( - "numeric_state", f"the 'above' entity {above} is unavailable" - ) + return False try: if fvalue <= float(above_entity.state): condition_trace_set_result( diff --git a/tests/components/homeassistant/triggers/test_numeric_state.py b/tests/components/homeassistant/triggers/test_numeric_state.py index 831e20b78a1..9eb9ac79a94 100644 --- a/tests/components/homeassistant/triggers/test_numeric_state.py +++ b/tests/components/homeassistant/triggers/test_numeric_state.py @@ -10,12 +10,7 @@ import homeassistant.components.automation as automation from homeassistant.components.homeassistant.triggers import ( numeric_state as numeric_state_trigger, ) -from homeassistant.const import ( - ATTR_ENTITY_ID, - ENTITY_MATCH_ALL, - SERVICE_TURN_OFF, - STATE_UNAVAILABLE, -) +from homeassistant.const import ATTR_ENTITY_ID, ENTITY_MATCH_ALL, SERVICE_TURN_OFF from homeassistant.core import Context from homeassistant.setup import async_setup_component 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 -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")) async def test_if_fires_on_entity_change_below_to_above(hass, calls, above): """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 -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.""" hass.states.async_set("test.entity", 0) 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() assert len(calls) == 0 - caplog.clear() - caplog.set_level(logging.WARNING) - async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=3)) hass.states.async_set("test.entity", "unavailable") await hass.async_block_till_done() 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)) hass.states.async_set("test.entity", 101) await hass.async_block_till_done() diff --git a/tests/helpers/test_condition.py b/tests/helpers/test_condition.py index b3e950131b0..cfed8ebbcf6 100644 --- a/tests/helpers/test_condition.py +++ b/tests/helpers/test_condition.py @@ -1,5 +1,4 @@ """Test the condition helper.""" -from logging import WARNING from unittest.mock import patch import pytest @@ -693,27 +692,6 @@ async def test_time_using_input_datetime(hass): 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): """Test that state raises ConditionError on errors.""" # No entity @@ -961,6 +939,26 @@ async def test_state_using_input_entities(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): """Test that numeric_state raises ConditionError on errors.""" # Unknown entities @@ -1007,20 +1005,6 @@ async def test_numeric_state_raises(hass): hass.states.async_set("sensor.temperature", 50) 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 with pytest.raises(ConditionError, match="cannot be processed as a number"): 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) 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( "input_number", "set_value", @@ -1193,6 +1183,12 @@ async def test_numeric_state_using_input_number(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): condition.async_numeric_state( hass, entity="sensor.temperature", below="input_number.not_exist"