Raise ConditionError for numeric_state errors ()

pull/46209/head
Anders Melchiorsen 2021-02-08 10:47:57 +01:00 committed by GitHub
parent 9e07910ab0
commit b9b1caf4d7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 352 additions and 63 deletions
homeassistant
components
automation
homeassistant/triggers
tests
components
automation
homeassistant/triggers

View File

@ -32,7 +32,7 @@ from homeassistant.core import (
callback,
split_entity_id,
)
from homeassistant.exceptions import HomeAssistantError
from homeassistant.exceptions import ConditionError, HomeAssistantError
from homeassistant.helpers import condition, extract_domain_configs, template
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import ToggleEntity
@ -588,7 +588,11 @@ async def _async_process_if(hass, config, p_config):
def if_action(variables=None):
"""AND all conditions."""
return all(check(hass, variables) for check in checks)
try:
return all(check(hass, variables) for check in checks)
except ConditionError as ex:
LOGGER.warning("Error in 'condition' evaluation: %s", ex)
return False
if_action.config = if_configs

View File

@ -17,7 +17,7 @@ from homeassistant.const import (
STATE_UNKNOWN,
)
from homeassistant.core import callback
from homeassistant.exceptions import TemplateError
from homeassistant.exceptions import ConditionError, TemplateError
from homeassistant.helpers import condition
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.event import (
@ -340,14 +340,17 @@ class BayesianBinarySensor(BinarySensorEntity):
"""Return True if numeric condition is met."""
entity = entity_observation["entity_id"]
return condition.async_numeric_state(
self.hass,
entity,
entity_observation.get("below"),
entity_observation.get("above"),
None,
entity_observation,
)
try:
return condition.async_numeric_state(
self.hass,
entity,
entity_observation.get("below"),
entity_observation.get("above"),
None,
entity_observation,
)
except ConditionError:
return False
def _process_state(self, entity_observation):
"""Return True if state conditions are met."""

View File

@ -96,13 +96,20 @@ async def async_attach_trigger(
@callback
def check_numeric_state(entity_id, from_s, to_s):
"""Return True if criteria are now met."""
if to_s is None:
try:
return condition.async_numeric_state(
hass,
to_s,
below,
above,
value_template,
variables(entity_id),
attribute,
)
except exceptions.ConditionError as err:
_LOGGER.warning("%s", err)
return False
return condition.async_numeric_state(
hass, to_s, below, above, value_template, variables(entity_id), attribute
)
@callback
def state_automation_listener(event):
"""Listen for state changes and calls action."""

View File

@ -25,6 +25,10 @@ class TemplateError(HomeAssistantError):
super().__init__(f"{exception.__class__.__name__}: {exception}")
class ConditionError(HomeAssistantError):
"""Error during condition evaluation."""
class PlatformNotReady(HomeAssistantError):
"""Error to indicate that platform is not ready."""

View File

@ -36,7 +36,7 @@ from homeassistant.const import (
WEEKDAYS,
)
from homeassistant.core import HomeAssistant, State, callback
from homeassistant.exceptions import HomeAssistantError, TemplateError
from homeassistant.exceptions import ConditionError, HomeAssistantError, TemplateError
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.sun import get_astral_event_date
from homeassistant.helpers.template import Template
@ -204,11 +204,22 @@ def async_numeric_state(
attribute: Optional[str] = None,
) -> bool:
"""Test a numeric state condition."""
if entity is None:
raise ConditionError("No entity specified")
if isinstance(entity, str):
entity_id = entity
entity = hass.states.get(entity)
if entity is None or (attribute is not None and attribute not in entity.attributes):
return False
if entity is None:
raise ConditionError(f"Unknown entity {entity_id}")
else:
entity_id = entity.entity_id
if attribute is not None and attribute not in entity.attributes:
raise ConditionError(
f"Attribute '{attribute}' (of entity {entity_id}) does not exist"
)
value: Any = None
if value_template is None:
@ -222,30 +233,27 @@ def async_numeric_state(
try:
value = value_template.async_render(variables)
except TemplateError as ex:
_LOGGER.error("Template error: %s", ex)
return False
raise ConditionError(f"Template error: {ex}") from ex
if value in (STATE_UNAVAILABLE, STATE_UNKNOWN):
return False
raise ConditionError("State is not available")
try:
fvalue = float(value)
except ValueError:
_LOGGER.warning(
"Value cannot be processed as a number: %s (Offending entity: %s)",
entity,
value,
)
return False
except ValueError as ex:
raise ConditionError(
f"Entity {entity_id} state '{value}' cannot be processed as a number"
) from ex
if below is not None:
if isinstance(below, str):
below_entity = hass.states.get(below)
if (
not below_entity
or below_entity.state in (STATE_UNAVAILABLE, STATE_UNKNOWN)
or fvalue >= float(below_entity.state)
if not below_entity or below_entity.state in (
STATE_UNAVAILABLE,
STATE_UNKNOWN,
):
raise ConditionError(f"The below entity {below} is not available")
if fvalue >= float(below_entity.state):
return False
elif fvalue >= below:
return False
@ -253,11 +261,12 @@ 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 (STATE_UNAVAILABLE, STATE_UNKNOWN)
or fvalue <= float(above_entity.state)
if not above_entity or above_entity.state in (
STATE_UNAVAILABLE,
STATE_UNKNOWN,
):
raise ConditionError(f"The above entity {above} is not available")
if fvalue <= float(above_entity.state):
return False
elif fvalue <= above:
return False

View File

@ -519,7 +519,12 @@ class _ScriptRun:
CONF_ALIAS, self._action[CONF_CONDITION]
)
cond = await self._async_get_condition(self._action)
check = cond(self._hass, self._variables)
try:
check = cond(self._hass, self._variables)
except exceptions.ConditionError as ex:
_LOGGER.warning("Error in 'condition' evaluation: %s", ex)
check = False
self._log("Test condition %s: %s", self._script.last_action, check)
if not check:
raise _StopScript
@ -570,10 +575,15 @@ class _ScriptRun:
]
for iteration in itertools.count(1):
set_repeat_var(iteration)
if self._stop.is_set() or not all(
cond(self._hass, self._variables) for cond in conditions
):
try:
if self._stop.is_set() or not all(
cond(self._hass, self._variables) for cond in conditions
):
break
except exceptions.ConditionError as ex:
_LOGGER.warning("Error in 'while' evaluation: %s", ex)
break
await async_run_sequence(iteration)
elif CONF_UNTIL in repeat:
@ -583,9 +593,13 @@ class _ScriptRun:
for iteration in itertools.count(1):
set_repeat_var(iteration)
await async_run_sequence(iteration)
if self._stop.is_set() or all(
cond(self._hass, self._variables) for cond in conditions
):
try:
if self._stop.is_set() or all(
cond(self._hass, self._variables) for cond in conditions
):
break
except exceptions.ConditionError as ex:
_LOGGER.warning("Error in 'until' evaluation: %s", ex)
break
if saved_repeat_vars:
@ -599,9 +613,14 @@ class _ScriptRun:
choose_data = await self._script._async_get_choose_data(self._step)
for conditions, script in choose_data["choices"]:
if all(condition(self._hass, self._variables) for condition in conditions):
await self._async_run_script(script)
return
try:
if all(
condition(self._hass, self._variables) for condition in conditions
):
await self._async_run_script(script)
return
except exceptions.ConditionError as ex:
_LOGGER.warning("Error in 'choose' evaluation: %s", ex)
if choose_data["default"]:
await self._async_run_script(choose_data["default"])

View File

@ -162,18 +162,20 @@ async def test_trigger_service_ignoring_condition(hass, calls):
"alias": "test",
"trigger": [{"platform": "event", "event_type": "test_event"}],
"condition": {
"condition": "state",
"condition": "numeric_state",
"entity_id": "non.existing",
"state": "beer",
"above": "1",
},
"action": {"service": "test.automation"},
}
},
)
hass.bus.async_fire("test_event")
await hass.async_block_till_done()
assert len(calls) == 0
with patch("homeassistant.components.automation.LOGGER.warning") as logwarn:
hass.bus.async_fire("test_event")
await hass.async_block_till_done()
assert len(calls) == 0
assert len(logwarn.mock_calls) == 1
await hass.services.async_call(
"automation", "trigger", {"entity_id": "automation.test"}, blocking=True

View File

@ -572,6 +572,32 @@ async def test_if_not_fires_if_entity_not_match(hass, calls, below):
assert len(calls) == 0
async def test_if_not_fires_and_warns_if_below_entity_unknown(hass, calls):
"""Test if warns with unknown below entity."""
assert await async_setup_component(
hass,
automation.DOMAIN,
{
automation.DOMAIN: {
"trigger": {
"platform": "numeric_state",
"entity_id": "test.entity",
"below": "input_number.unknown",
},
"action": {"service": "test.automation"},
}
},
)
with patch(
"homeassistant.components.homeassistant.triggers.numeric_state._LOGGER.warning"
) as logwarn:
hass.states.async_set("test.entity", 1)
await hass.async_block_till_done()
assert len(calls) == 0
assert len(logwarn.mock_calls) == 1
@pytest.mark.parametrize("below", (10, "input_number.value_10"))
async def test_if_fires_on_entity_change_below_with_attribute(hass, calls, below):
"""Test attributes change."""

View File

@ -4,7 +4,7 @@ from unittest.mock import patch
import pytest
from homeassistant.exceptions import HomeAssistantError
from homeassistant.exceptions import ConditionError, HomeAssistantError
from homeassistant.helpers import condition
from homeassistant.helpers.template import Template
from homeassistant.setup import async_setup_component
@ -338,8 +338,8 @@ async def test_time_using_input_datetime(hass):
assert not condition.time(hass, before="input_datetime.not_existing")
async def test_if_numeric_state_not_raise_on_unavailable(hass):
"""Test numeric_state doesn't raise on unavailable/unknown state."""
async def test_if_numeric_state_raises_on_unavailable(hass):
"""Test numeric_state raises on unavailable/unknown state."""
test = await condition.async_from_config(
hass,
{"condition": "numeric_state", "entity_id": "sensor.temperature", "below": 42},
@ -347,11 +347,13 @@ async def test_if_numeric_state_not_raise_on_unavailable(hass):
with patch("homeassistant.helpers.condition._LOGGER.warning") as logwarn:
hass.states.async_set("sensor.temperature", "unavailable")
assert not test(hass)
with pytest.raises(ConditionError):
test(hass)
assert len(logwarn.mock_calls) == 0
hass.states.async_set("sensor.temperature", "unknown")
assert not test(hass)
with pytest.raises(ConditionError):
test(hass)
assert len(logwarn.mock_calls) == 0
@ -550,6 +552,108 @@ async def test_state_using_input_entities(hass):
assert test(hass)
async def test_numeric_state_raises(hass):
"""Test that numeric_state raises ConditionError on errors."""
# Unknown entity_id
with pytest.raises(ConditionError, match="Unknown entity"):
test = await condition.async_from_config(
hass,
{
"condition": "numeric_state",
"entity_id": "sensor.temperature_unknown",
"above": 0,
},
)
assert test(hass)
# Unknown attribute
with pytest.raises(ConditionError, match=r"Attribute .* does not exist"):
test = await condition.async_from_config(
hass,
{
"condition": "numeric_state",
"entity_id": "sensor.temperature",
"attribute": "temperature",
"above": 0,
},
)
hass.states.async_set("sensor.temperature", 50)
test(hass)
# Template error
with pytest.raises(ConditionError, match="ZeroDivisionError"):
test = await condition.async_from_config(
hass,
{
"condition": "numeric_state",
"entity_id": "sensor.temperature",
"value_template": "{{ 1 / 0 }}",
"above": 0,
},
)
hass.states.async_set("sensor.temperature", 50)
test(hass)
# Unavailable state
with pytest.raises(ConditionError, match="State is not available"):
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(
hass,
{
"condition": "numeric_state",
"entity_id": "sensor.temperature",
"above": 0,
},
)
hass.states.async_set("sensor.temperature", "fifty")
test(hass)
# Below entity missing
with pytest.raises(ConditionError, match="below entity"):
test = await condition.async_from_config(
hass,
{
"condition": "numeric_state",
"entity_id": "sensor.temperature",
"below": "input_number.missing",
},
)
hass.states.async_set("sensor.temperature", 50)
test(hass)
# Above entity missing
with pytest.raises(ConditionError, match="above entity"):
test = await condition.async_from_config(
hass,
{
"condition": "numeric_state",
"entity_id": "sensor.temperature",
"above": "input_number.missing",
},
)
hass.states.async_set("sensor.temperature", 50)
test(hass)
async def test_numeric_state_multiple_entities(hass):
"""Test with multiple entities in condition."""
test = await condition.async_from_config(
@ -660,12 +764,14 @@ async def test_numeric_state_using_input_number(hass):
)
assert test(hass)
assert not condition.async_numeric_state(
hass, entity="sensor.temperature", below="input_number.not_exist"
)
assert not condition.async_numeric_state(
hass, entity="sensor.temperature", above="input_number.not_exist"
)
with pytest.raises(ConditionError):
condition.async_numeric_state(
hass, entity="sensor.temperature", below="input_number.not_exist"
)
with pytest.raises(ConditionError):
condition.async_numeric_state(
hass, entity="sensor.temperature", above="input_number.not_exist"
)
async def test_zone_multiple_entities(hass):

View File

@ -990,6 +990,32 @@ async def test_wait_for_trigger_generated_exception(hass, caplog):
assert "something bad" in caplog.text
async def test_condition_warning(hass):
"""Test warning on condition."""
event = "test_event"
events = async_capture_events(hass, event)
sequence = cv.SCRIPT_SCHEMA(
[
{"event": event},
{
"condition": "numeric_state",
"entity_id": "test.entity",
"above": 0,
},
{"event": event},
]
)
script_obj = script.Script(hass, sequence, "Test Name", "test_domain")
hass.states.async_set("test.entity", "string")
with patch("homeassistant.helpers.script._LOGGER.warning") as logwarn:
await script_obj.async_run(context=Context())
await hass.async_block_till_done()
assert len(logwarn.mock_calls) == 1
assert len(events) == 1
async def test_condition_basic(hass):
"""Test if we can use conditions in a script."""
event = "test_event"
@ -1100,6 +1126,44 @@ async def test_repeat_count(hass):
assert event.data.get("last") == (index == count - 1)
@pytest.mark.parametrize("condition", ["while", "until"])
async def test_repeat_condition_warning(hass, condition):
"""Test warning on repeat conditions."""
event = "test_event"
events = async_capture_events(hass, event)
count = 0 if condition == "while" else 1
sequence = {
"repeat": {
"sequence": [
{
"event": event,
},
],
}
}
sequence["repeat"][condition] = {
"condition": "numeric_state",
"entity_id": "sensor.test",
"value_template": "{{ unassigned_variable }}",
"above": "0",
}
script_obj = script.Script(
hass, cv.SCRIPT_SCHEMA(sequence), f"Test {condition}", "test_domain"
)
# wait_started = async_watch_for_action(script_obj, "wait")
hass.states.async_set("sensor.test", "1")
with patch("homeassistant.helpers.script._LOGGER.warning") as logwarn:
hass.async_create_task(script_obj.async_run(context=Context()))
await asyncio.wait_for(hass.async_block_till_done(), 1)
assert len(logwarn.mock_calls) == 1
assert len(events) == count
@pytest.mark.parametrize("condition", ["while", "until"])
@pytest.mark.parametrize("direct_template", [False, True])
async def test_repeat_conditional(hass, condition, direct_template):
@ -1305,6 +1369,51 @@ async def test_repeat_nested(hass, variables, first_last, inside_x):
}
async def test_choose_warning(hass):
"""Test warning on choose."""
event = "test_event"
events = async_capture_events(hass, event)
sequence = cv.SCRIPT_SCHEMA(
{
"choose": [
{
"conditions": {
"condition": "numeric_state",
"entity_id": "test.entity",
"value_template": "{{ undefined_a + undefined_b }}",
"above": 1,
},
"sequence": {"event": event, "event_data": {"choice": "first"}},
},
{
"conditions": {
"condition": "numeric_state",
"entity_id": "test.entity",
"value_template": "{{ 'string' }}",
"above": 2,
},
"sequence": {"event": event, "event_data": {"choice": "second"}},
},
],
"default": {"event": event, "event_data": {"choice": "default"}},
}
)
script_obj = script.Script(hass, sequence, "Test Name", "test_domain")
hass.states.async_set("test.entity", "9")
await hass.async_block_till_done()
with patch("homeassistant.helpers.script._LOGGER.warning") as logwarn:
await script_obj.async_run(context=Context())
await hass.async_block_till_done()
print(logwarn.mock_calls)
assert len(logwarn.mock_calls) == 2
assert len(events) == 1
assert events[0].data["choice"] == "default"
@pytest.mark.parametrize("var,result", [(1, "first"), (2, "second"), (3, "default")])
async def test_choose(hass, var, result):
"""Test choose action."""