core/tests/helpers/test_condition.py

1553 lines
46 KiB
Python

"""Test the condition helper."""
from unittest.mock import patch
import pytest
from homeassistant.exceptions import ConditionError, HomeAssistantError
from homeassistant.helpers import condition
from homeassistant.helpers.template import Template
from homeassistant.setup import async_setup_component
from homeassistant.util import dt
def assert_element(trace_element, expected_element, path):
"""Assert a trace element is as expected.
Note: Unused variable path is passed to get helpful errors from pytest.
"""
for result_key, result in expected_element.get("result", {}).items():
assert trace_element._result[result_key] == result
if "error_type" in expected_element:
assert isinstance(trace_element._error, expected_element["error_type"])
else:
assert trace_element._error is None
def assert_condition_trace(expected):
"""Assert a trace condition sequence is as expected."""
condition_trace = condition.condition_trace_get()
condition.condition_trace_clear()
expected_trace_keys = list(expected.keys())
assert list(condition_trace.keys()) == expected_trace_keys
for trace_key_index, key in enumerate(expected_trace_keys):
assert len(condition_trace[key]) == len(expected[key])
for index, element in enumerate(expected[key]):
path = f"[{trace_key_index}][{index}]"
assert_element(condition_trace[key][index], element, path)
async def test_invalid_condition(hass):
"""Test if invalid condition raises."""
with pytest.raises(HomeAssistantError):
await condition.async_from_config(
hass,
{
"condition": "invalid",
"conditions": [
{
"condition": "state",
"entity_id": "sensor.temperature",
"state": "100",
},
],
},
)
async def test_and_condition(hass):
"""Test the 'and' condition."""
test = await condition.async_from_config(
hass,
{
"alias": "And Condition",
"condition": "and",
"conditions": [
{
"condition": "state",
"entity_id": "sensor.temperature",
"state": "100",
},
{
"condition": "numeric_state",
"entity_id": "sensor.temperature",
"below": 110,
},
],
},
)
with pytest.raises(ConditionError):
test(hass)
assert_condition_trace(
{
"": [{"error_type": ConditionError}],
"conditions/0": [{"error_type": ConditionError}],
"conditions/0/entity_id/0": [{"error_type": ConditionError}],
"conditions/1": [{"error_type": ConditionError}],
"conditions/1/entity_id/0": [{"error_type": ConditionError}],
}
)
hass.states.async_set("sensor.temperature", 120)
assert not test(hass)
assert_condition_trace(
{
"": [{"result": {"result": False}}],
"conditions/0": [{"result": {"result": False}}],
"conditions/0/entity_id/0": [{"result": {"result": False}}],
}
)
hass.states.async_set("sensor.temperature", 105)
assert not test(hass)
assert_condition_trace(
{
"": [{"result": {"result": False}}],
"conditions/0": [{"result": {"result": False}}],
"conditions/0/entity_id/0": [{"result": {"result": False}}],
}
)
hass.states.async_set("sensor.temperature", 100)
assert test(hass)
assert_condition_trace(
{
"": [{"result": {"result": True}}],
"conditions/0": [{"result": {"result": True}}],
"conditions/0/entity_id/0": [{"result": {"result": True}}],
"conditions/1": [{"result": {"result": True}}],
"conditions/1/entity_id/0": [{"result": {"result": True}}],
}
)
async def test_and_condition_raises(hass):
"""Test the 'and' condition."""
test = await condition.async_from_config(
hass,
{
"alias": "And Condition",
"condition": "and",
"conditions": [
{
"condition": "state",
"entity_id": "sensor.temperature",
"state": "100",
},
{
"condition": "numeric_state",
"entity_id": "sensor.temperature2",
"above": 110,
},
],
},
)
# All subconditions raise, the AND-condition should raise
with pytest.raises(ConditionError):
test(hass)
assert_condition_trace(
{
"": [{"error_type": ConditionError}],
"conditions/0": [{"error_type": ConditionError}],
"conditions/0/entity_id/0": [{"error_type": ConditionError}],
"conditions/1": [{"error_type": ConditionError}],
"conditions/1/entity_id/0": [{"error_type": ConditionError}],
}
)
# The first subconditions raises, the second returns True, the AND-condition
# should raise
hass.states.async_set("sensor.temperature2", 120)
with pytest.raises(ConditionError):
test(hass)
assert_condition_trace(
{
"": [{"error_type": ConditionError}],
"conditions/0": [{"error_type": ConditionError}],
"conditions/0/entity_id/0": [{"error_type": ConditionError}],
"conditions/1": [{"result": {"result": True}}],
"conditions/1/entity_id/0": [{"result": {"result": True}}],
}
)
# The first subconditions raises, the second returns False, the AND-condition
# should return False
hass.states.async_set("sensor.temperature2", 90)
assert not test(hass)
assert_condition_trace(
{
"": [{"result": {"result": False}}],
"conditions/0": [{"error_type": ConditionError}],
"conditions/0/entity_id/0": [{"error_type": ConditionError}],
"conditions/1": [{"result": {"result": False}}],
"conditions/1/entity_id/0": [{"result": {"result": False}}],
}
)
async def test_and_condition_with_template(hass):
"""Test the 'and' condition."""
test = await condition.async_from_config(
hass,
{
"condition": "and",
"conditions": [
{
"alias": "Template Condition",
"condition": "template",
"value_template": '{{ states.sensor.temperature.state == "100" }}',
},
{
"condition": "numeric_state",
"entity_id": "sensor.temperature",
"below": 110,
},
],
},
)
hass.states.async_set("sensor.temperature", 120)
assert not test(hass)
hass.states.async_set("sensor.temperature", 105)
assert not test(hass)
hass.states.async_set("sensor.temperature", 100)
assert test(hass)
async def test_or_condition(hass):
"""Test the 'or' condition."""
test = await condition.async_from_config(
hass,
{
"alias": "Or Condition",
"condition": "or",
"conditions": [
{
"condition": "state",
"entity_id": "sensor.temperature",
"state": "100",
},
{
"condition": "numeric_state",
"entity_id": "sensor.temperature",
"below": 110,
},
],
},
)
with pytest.raises(ConditionError):
test(hass)
assert_condition_trace(
{
"": [{"error_type": ConditionError}],
"conditions/0": [{"error_type": ConditionError}],
"conditions/0/entity_id/0": [{"error_type": ConditionError}],
"conditions/1": [{"error_type": ConditionError}],
"conditions/1/entity_id/0": [{"error_type": ConditionError}],
}
)
hass.states.async_set("sensor.temperature", 120)
assert not test(hass)
assert_condition_trace(
{
"": [{"result": {"result": False}}],
"conditions/0": [{"result": {"result": False}}],
"conditions/0/entity_id/0": [{"result": {"result": False}}],
"conditions/1": [{"result": {"result": False}}],
"conditions/1/entity_id/0": [{"result": {"result": False}}],
}
)
hass.states.async_set("sensor.temperature", 105)
assert test(hass)
assert_condition_trace(
{
"": [{"result": {"result": True}}],
"conditions/0": [{"result": {"result": False}}],
"conditions/0/entity_id/0": [{"result": {"result": False}}],
"conditions/1": [{"result": {"result": True}}],
"conditions/1/entity_id/0": [{"result": {"result": True}}],
}
)
hass.states.async_set("sensor.temperature", 100)
assert test(hass)
assert_condition_trace(
{
"": [{"result": {"result": True}}],
"conditions/0": [{"result": {"result": True}}],
"conditions/0/entity_id/0": [{"result": {"result": True}}],
}
)
async def test_or_condition_raises(hass):
"""Test the 'or' condition."""
test = await condition.async_from_config(
hass,
{
"alias": "Or Condition",
"condition": "or",
"conditions": [
{
"condition": "state",
"entity_id": "sensor.temperature",
"state": "100",
},
{
"condition": "numeric_state",
"entity_id": "sensor.temperature2",
"above": 110,
},
],
},
)
# All subconditions raise, the OR-condition should raise
with pytest.raises(ConditionError):
test(hass)
assert_condition_trace(
{
"": [{"error_type": ConditionError}],
"conditions/0": [{"error_type": ConditionError}],
"conditions/0/entity_id/0": [{"error_type": ConditionError}],
"conditions/1": [{"error_type": ConditionError}],
"conditions/1/entity_id/0": [{"error_type": ConditionError}],
}
)
# The first subconditions raises, the second returns False, the OR-condition
# should raise
hass.states.async_set("sensor.temperature2", 100)
with pytest.raises(ConditionError):
test(hass)
assert_condition_trace(
{
"": [{"error_type": ConditionError}],
"conditions/0": [{"error_type": ConditionError}],
"conditions/0/entity_id/0": [{"error_type": ConditionError}],
"conditions/1": [{"result": {"result": False}}],
"conditions/1/entity_id/0": [{"result": {"result": False}}],
}
)
# The first subconditions raises, the second returns True, the OR-condition
# should return True
hass.states.async_set("sensor.temperature2", 120)
assert test(hass)
assert_condition_trace(
{
"": [{"result": {"result": True}}],
"conditions/0": [{"error_type": ConditionError}],
"conditions/0/entity_id/0": [{"error_type": ConditionError}],
"conditions/1": [{"result": {"result": True}}],
"conditions/1/entity_id/0": [{"result": {"result": True}}],
}
)
async def test_or_condition_with_template(hass):
"""Test the 'or' condition."""
test = await condition.async_from_config(
hass,
{
"condition": "or",
"conditions": [
{'{{ states.sensor.temperature.state == "100" }}'},
{
"condition": "numeric_state",
"entity_id": "sensor.temperature",
"below": 110,
},
],
},
)
hass.states.async_set("sensor.temperature", 120)
assert not test(hass)
hass.states.async_set("sensor.temperature", 105)
assert test(hass)
hass.states.async_set("sensor.temperature", 100)
assert test(hass)
async def test_not_condition(hass):
"""Test the 'not' condition."""
test = await condition.async_from_config(
hass,
{
"alias": "Not Condition",
"condition": "not",
"conditions": [
{
"condition": "state",
"entity_id": "sensor.temperature",
"state": "100",
},
{
"condition": "numeric_state",
"entity_id": "sensor.temperature",
"below": 50,
},
],
},
)
with pytest.raises(ConditionError):
test(hass)
assert_condition_trace(
{
"": [{"error_type": ConditionError}],
"conditions/0": [{"error_type": ConditionError}],
"conditions/0/entity_id/0": [{"error_type": ConditionError}],
"conditions/1": [{"error_type": ConditionError}],
"conditions/1/entity_id/0": [{"error_type": ConditionError}],
}
)
hass.states.async_set("sensor.temperature", 101)
assert test(hass)
assert_condition_trace(
{
"": [{"result": {"result": True}}],
"conditions/0": [{"result": {"result": False}}],
"conditions/0/entity_id/0": [{"result": {"result": False}}],
"conditions/1": [{"result": {"result": False}}],
"conditions/1/entity_id/0": [{"result": {"result": False}}],
}
)
hass.states.async_set("sensor.temperature", 50)
assert test(hass)
assert_condition_trace(
{
"": [{"result": {"result": True}}],
"conditions/0": [{"result": {"result": False}}],
"conditions/0/entity_id/0": [{"result": {"result": False}}],
"conditions/1": [{"result": {"result": False}}],
"conditions/1/entity_id/0": [{"result": {"result": False}}],
}
)
hass.states.async_set("sensor.temperature", 49)
assert not test(hass)
assert_condition_trace(
{
"": [{"result": {"result": False}}],
"conditions/0": [{"result": {"result": False}}],
"conditions/0/entity_id/0": [{"result": {"result": False}}],
"conditions/1": [{"result": {"result": True}}],
"conditions/1/entity_id/0": [{"result": {"result": True}}],
}
)
hass.states.async_set("sensor.temperature", 100)
assert not test(hass)
assert_condition_trace(
{
"": [{"result": {"result": False}}],
"conditions/0": [{"result": {"result": True}}],
"conditions/0/entity_id/0": [{"result": {"result": True}}],
}
)
async def test_not_condition_raises(hass):
"""Test the 'and' condition."""
test = await condition.async_from_config(
hass,
{
"alias": "Not Condition",
"condition": "not",
"conditions": [
{
"condition": "state",
"entity_id": "sensor.temperature",
"state": "100",
},
{
"condition": "numeric_state",
"entity_id": "sensor.temperature2",
"below": 50,
},
],
},
)
# All subconditions raise, the NOT-condition should raise
with pytest.raises(ConditionError):
test(hass)
assert_condition_trace(
{
"": [{"error_type": ConditionError}],
"conditions/0": [{"error_type": ConditionError}],
"conditions/0/entity_id/0": [{"error_type": ConditionError}],
"conditions/1": [{"error_type": ConditionError}],
"conditions/1/entity_id/0": [{"error_type": ConditionError}],
}
)
# The first subconditions raises, the second returns False, the NOT-condition
# should raise
hass.states.async_set("sensor.temperature2", 90)
with pytest.raises(ConditionError):
test(hass)
assert_condition_trace(
{
"": [{"error_type": ConditionError}],
"conditions/0": [{"error_type": ConditionError}],
"conditions/0/entity_id/0": [{"error_type": ConditionError}],
"conditions/1": [{"result": {"result": False}}],
"conditions/1/entity_id/0": [{"result": {"result": False}}],
}
)
# The first subconditions raises, the second returns True, the NOT-condition
# should return False
hass.states.async_set("sensor.temperature2", 40)
assert not test(hass)
assert_condition_trace(
{
"": [{"result": {"result": False}}],
"conditions/0": [{"error_type": ConditionError}],
"conditions/0/entity_id/0": [{"error_type": ConditionError}],
"conditions/1": [{"result": {"result": True}}],
"conditions/1/entity_id/0": [{"result": {"result": True}}],
}
)
async def test_not_condition_with_template(hass):
"""Test the 'or' condition."""
test = await condition.async_from_config(
hass,
{
"condition": "not",
"conditions": [
{
"condition": "template",
"value_template": '{{ states.sensor.temperature.state == "100" }}',
},
{
"condition": "numeric_state",
"entity_id": "sensor.temperature",
"below": 50,
},
],
},
)
hass.states.async_set("sensor.temperature", 101)
assert test(hass)
hass.states.async_set("sensor.temperature", 50)
assert test(hass)
hass.states.async_set("sensor.temperature", 49)
assert not test(hass)
hass.states.async_set("sensor.temperature", 100)
assert not test(hass)
async def test_time_window(hass):
"""Test time condition windows."""
sixam = "06:00:00"
sixpm = "18:00:00"
test1 = await condition.async_from_config(
hass,
{"alias": "Time Cond", "condition": "time", "after": sixam, "before": sixpm},
)
test2 = await condition.async_from_config(
hass,
{"alias": "Time Cond", "condition": "time", "after": sixpm, "before": sixam},
)
with patch(
"homeassistant.helpers.condition.dt_util.now",
return_value=dt.now().replace(hour=3),
):
assert not test1(hass)
assert test2(hass)
with patch(
"homeassistant.helpers.condition.dt_util.now",
return_value=dt.now().replace(hour=9),
):
assert test1(hass)
assert not test2(hass)
with patch(
"homeassistant.helpers.condition.dt_util.now",
return_value=dt.now().replace(hour=15),
):
assert test1(hass)
assert not test2(hass)
with patch(
"homeassistant.helpers.condition.dt_util.now",
return_value=dt.now().replace(hour=21),
):
assert not test1(hass)
assert test2(hass)
async def test_time_using_input_datetime(hass):
"""Test time conditions using input_datetime entities."""
await async_setup_component(
hass,
"input_datetime",
{
"input_datetime": {
"am": {"has_date": True, "has_time": True},
"pm": {"has_date": True, "has_time": True},
}
},
)
await hass.services.async_call(
"input_datetime",
"set_datetime",
{
"entity_id": "input_datetime.am",
"datetime": str(
dt.now()
.replace(hour=6, minute=0, second=0, microsecond=0)
.replace(tzinfo=None)
),
},
blocking=True,
)
await hass.services.async_call(
"input_datetime",
"set_datetime",
{
"entity_id": "input_datetime.pm",
"datetime": str(
dt.now()
.replace(hour=18, minute=0, second=0, microsecond=0)
.replace(tzinfo=None)
),
},
blocking=True,
)
with patch(
"homeassistant.helpers.condition.dt_util.now",
return_value=dt.now().replace(hour=3),
):
assert not condition.time(
hass, after="input_datetime.am", before="input_datetime.pm"
)
assert condition.time(
hass, after="input_datetime.pm", before="input_datetime.am"
)
with patch(
"homeassistant.helpers.condition.dt_util.now",
return_value=dt.now().replace(hour=9),
):
assert condition.time(
hass, after="input_datetime.am", before="input_datetime.pm"
)
assert not condition.time(
hass, after="input_datetime.pm", before="input_datetime.am"
)
with patch(
"homeassistant.helpers.condition.dt_util.now",
return_value=dt.now().replace(hour=15),
):
assert condition.time(
hass, after="input_datetime.am", before="input_datetime.pm"
)
assert not condition.time(
hass, after="input_datetime.pm", before="input_datetime.am"
)
with patch(
"homeassistant.helpers.condition.dt_util.now",
return_value=dt.now().replace(hour=21),
):
assert not condition.time(
hass, after="input_datetime.am", before="input_datetime.pm"
)
assert condition.time(
hass, after="input_datetime.pm", before="input_datetime.am"
)
with pytest.raises(ConditionError):
condition.time(hass, after="input_datetime.not_existing")
with pytest.raises(ConditionError):
condition.time(hass, before="input_datetime.not_existing")
async def test_state_raises(hass):
"""Test that state raises ConditionError on errors."""
# No entity
with pytest.raises(ConditionError, match="no entity"):
condition.state(hass, entity=None, req_state="missing")
# Unknown entities
test = await condition.async_from_config(
hass,
{
"condition": "state",
"entity_id": ["sensor.door_unknown", "sensor.window_unknown"],
"state": "open",
},
)
with pytest.raises(ConditionError, match="unknown entity.*door"):
test(hass)
with pytest.raises(ConditionError, match="unknown entity.*window"):
test(hass)
# Unknown attribute
with pytest.raises(ConditionError, match=r"attribute .* does not exist"):
test = await condition.async_from_config(
hass,
{
"condition": "state",
"entity_id": "sensor.door",
"attribute": "model",
"state": "acme",
},
)
hass.states.async_set("sensor.door", "open")
test(hass)
# Unknown state entity
with pytest.raises(ConditionError, match="input_text.missing"):
test = await condition.async_from_config(
hass,
{
"condition": "state",
"entity_id": "sensor.door",
"state": "input_text.missing",
},
)
hass.states.async_set("sensor.door", "open")
test(hass)
async def test_state_multiple_entities(hass):
"""Test with multiple entities in condition."""
test = await condition.async_from_config(
hass,
{
"condition": "and",
"conditions": [
{
"condition": "state",
"entity_id": ["sensor.temperature_1", "sensor.temperature_2"],
"state": "100",
},
],
},
)
hass.states.async_set("sensor.temperature_1", 100)
hass.states.async_set("sensor.temperature_2", 100)
assert test(hass)
hass.states.async_set("sensor.temperature_1", 101)
hass.states.async_set("sensor.temperature_2", 100)
assert not test(hass)
hass.states.async_set("sensor.temperature_1", 100)
hass.states.async_set("sensor.temperature_2", 101)
assert not test(hass)
async def test_multiple_states(hass):
"""Test with multiple states in condition."""
test = await condition.async_from_config(
hass,
{
"condition": "and",
"conditions": [
{
"alias": "State Condition",
"condition": "state",
"entity_id": "sensor.temperature",
"state": ["100", "200"],
},
],
},
)
hass.states.async_set("sensor.temperature", 100)
assert test(hass)
hass.states.async_set("sensor.temperature", 200)
assert test(hass)
hass.states.async_set("sensor.temperature", 42)
assert not test(hass)
async def test_state_attribute(hass):
"""Test with state attribute in condition."""
test = await condition.async_from_config(
hass,
{
"condition": "and",
"conditions": [
{
"condition": "state",
"entity_id": "sensor.temperature",
"attribute": "attribute1",
"state": 200,
},
],
},
)
hass.states.async_set("sensor.temperature", 100, {"unkown_attr": 200})
with pytest.raises(ConditionError):
test(hass)
hass.states.async_set("sensor.temperature", 100, {"attribute1": 200})
assert test(hass)
hass.states.async_set("sensor.temperature", 100, {"attribute1": "200"})
assert not test(hass)
hass.states.async_set("sensor.temperature", 100, {"attribute1": 201})
assert not test(hass)
hass.states.async_set("sensor.temperature", 100, {"attribute1": None})
assert not test(hass)
async def test_state_attribute_boolean(hass):
"""Test with boolean state attribute in condition."""
test = await condition.async_from_config(
hass,
{
"condition": "state",
"entity_id": "sensor.temperature",
"attribute": "happening",
"state": False,
},
)
hass.states.async_set("sensor.temperature", 100, {"happening": 200})
assert not test(hass)
hass.states.async_set("sensor.temperature", 100, {"happening": True})
assert not test(hass)
hass.states.async_set("sensor.temperature", 100, {"no_happening": 201})
with pytest.raises(ConditionError):
test(hass)
hass.states.async_set("sensor.temperature", 100, {"happening": False})
assert test(hass)
async def test_state_using_input_entities(hass):
"""Test state conditions using input_* entities."""
await async_setup_component(
hass,
"input_text",
{
"input_text": {
"hello": {"initial": "goodbye"},
}
},
)
await async_setup_component(
hass,
"input_select",
{
"input_select": {
"hello": {"options": ["cya", "goodbye", "welcome"], "initial": "cya"},
}
},
)
test = await condition.async_from_config(
hass,
{
"condition": "and",
"conditions": [
{
"condition": "state",
"entity_id": "sensor.salut",
"state": [
"input_text.hello",
"input_select.hello",
"salut",
],
},
],
},
)
hass.states.async_set("sensor.salut", "goodbye")
assert test(hass)
hass.states.async_set("sensor.salut", "salut")
assert test(hass)
hass.states.async_set("sensor.salut", "hello")
assert not test(hass)
await hass.services.async_call(
"input_text",
"set_value",
{
"entity_id": "input_text.hello",
"value": "hi",
},
blocking=True,
)
assert not test(hass)
hass.states.async_set("sensor.salut", "hi")
assert test(hass)
hass.states.async_set("sensor.salut", "cya")
assert test(hass)
await hass.services.async_call(
"input_select",
"select_option",
{
"entity_id": "input_select.hello",
"option": "welcome",
},
blocking=True,
)
assert not test(hass)
hass.states.async_set("sensor.salut", "welcome")
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
test = await condition.async_from_config(
hass,
{
"condition": "numeric_state",
"entity_id": ["sensor.temperature_unknown", "sensor.humidity_unknown"],
"above": 0,
},
)
with pytest.raises(ConditionError, match="unknown entity.*temperature"):
test(hass)
with pytest.raises(ConditionError, match="unknown entity.*humidity"):
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)
# 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)
# Below entity not a number
with pytest.raises(
ConditionError,
match="'below'.*input_number.missing.*cannot be processed as a number",
):
hass.states.async_set("input_number.missing", "number")
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)
# Above entity not a number
with pytest.raises(
ConditionError,
match="'above'.*input_number.missing.*cannot be processed as a number",
):
hass.states.async_set("input_number.missing", "number")
test(hass)
async def test_numeric_state_multiple_entities(hass):
"""Test with multiple entities in condition."""
test = await condition.async_from_config(
hass,
{
"condition": "and",
"conditions": [
{
"alias": "Numeric State Condition",
"condition": "numeric_state",
"entity_id": ["sensor.temperature_1", "sensor.temperature_2"],
"below": 50,
},
],
},
)
hass.states.async_set("sensor.temperature_1", 49)
hass.states.async_set("sensor.temperature_2", 49)
assert test(hass)
hass.states.async_set("sensor.temperature_1", 50)
hass.states.async_set("sensor.temperature_2", 49)
assert not test(hass)
hass.states.async_set("sensor.temperature_1", 49)
hass.states.async_set("sensor.temperature_2", 50)
assert not test(hass)
async def test_numeric_state_attribute(hass):
"""Test with numeric state attribute in condition."""
test = await condition.async_from_config(
hass,
{
"condition": "and",
"conditions": [
{
"condition": "numeric_state",
"entity_id": "sensor.temperature",
"attribute": "attribute1",
"below": 50,
},
],
},
)
hass.states.async_set("sensor.temperature", 100, {"unkown_attr": 10})
with pytest.raises(ConditionError):
assert test(hass)
hass.states.async_set("sensor.temperature", 100, {"attribute1": 49})
assert test(hass)
hass.states.async_set("sensor.temperature", 100, {"attribute1": "49"})
assert test(hass)
hass.states.async_set("sensor.temperature", 100, {"attribute1": 51})
assert not test(hass)
hass.states.async_set("sensor.temperature", 100, {"attribute1": None})
with pytest.raises(ConditionError):
assert test(hass)
async def test_numeric_state_using_input_number(hass):
"""Test numeric_state conditions using input_number entities."""
await async_setup_component(
hass,
"input_number",
{
"input_number": {
"low": {"min": 0, "max": 255, "initial": 10},
"high": {"min": 0, "max": 255, "initial": 100},
}
},
)
test = await condition.async_from_config(
hass,
{
"condition": "and",
"conditions": [
{
"condition": "numeric_state",
"entity_id": "sensor.temperature",
"below": "input_number.high",
"above": "input_number.low",
},
],
},
)
hass.states.async_set("sensor.temperature", 42)
assert test(hass)
hass.states.async_set("sensor.temperature", 10)
assert not test(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",
{
"entity_id": "input_number.high",
"value": 101,
},
blocking=True,
)
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"
)
with pytest.raises(ConditionError):
condition.async_numeric_state(
hass, entity="sensor.temperature", above="input_number.not_exist"
)
async def test_zone_raises(hass):
"""Test that zone raises ConditionError on errors."""
test = await condition.async_from_config(
hass,
{
"condition": "zone",
"entity_id": "device_tracker.cat",
"zone": "zone.home",
},
)
with pytest.raises(ConditionError, match="no zone"):
condition.zone(hass, zone_ent=None, entity="sensor.any")
with pytest.raises(ConditionError, match="unknown zone"):
test(hass)
hass.states.async_set(
"zone.home",
"zoning",
{"name": "home", "latitude": 2.1, "longitude": 1.1, "radius": 10},
)
with pytest.raises(ConditionError, match="no entity"):
condition.zone(hass, zone_ent="zone.home", entity=None)
with pytest.raises(ConditionError, match="unknown entity"):
test(hass)
hass.states.async_set(
"device_tracker.cat",
"home",
{"friendly_name": "cat"},
)
with pytest.raises(ConditionError, match="latitude"):
test(hass)
hass.states.async_set(
"device_tracker.cat",
"home",
{"friendly_name": "cat", "latitude": 2.1},
)
with pytest.raises(ConditionError, match="longitude"):
test(hass)
hass.states.async_set(
"device_tracker.cat",
"home",
{"friendly_name": "cat", "latitude": 2.1, "longitude": 1.1},
)
# All okay, now test multiple failed conditions
assert test(hass)
test = await condition.async_from_config(
hass,
{
"condition": "zone",
"entity_id": ["device_tracker.cat", "device_tracker.dog"],
"zone": ["zone.home", "zone.work"],
},
)
with pytest.raises(ConditionError, match="dog"):
test(hass)
with pytest.raises(ConditionError, match="work"):
test(hass)
hass.states.async_set(
"zone.work",
"zoning",
{"name": "work", "latitude": 20, "longitude": 10, "radius": 25000},
)
hass.states.async_set(
"device_tracker.dog",
"work",
{"friendly_name": "dog", "latitude": 20.1, "longitude": 10.1},
)
assert test(hass)
async def test_zone_multiple_entities(hass):
"""Test with multiple entities in condition."""
test = await condition.async_from_config(
hass,
{
"condition": "and",
"conditions": [
{
"alias": "Zone Condition",
"condition": "zone",
"entity_id": ["device_tracker.person_1", "device_tracker.person_2"],
"zone": "zone.home",
},
],
},
)
hass.states.async_set(
"zone.home",
"zoning",
{"name": "home", "latitude": 2.1, "longitude": 1.1, "radius": 10},
)
hass.states.async_set(
"device_tracker.person_1",
"home",
{"friendly_name": "person_1", "latitude": 2.1, "longitude": 1.1},
)
hass.states.async_set(
"device_tracker.person_2",
"home",
{"friendly_name": "person_2", "latitude": 2.1, "longitude": 1.1},
)
assert test(hass)
hass.states.async_set(
"device_tracker.person_1",
"home",
{"friendly_name": "person_1", "latitude": 20.1, "longitude": 10.1},
)
hass.states.async_set(
"device_tracker.person_2",
"home",
{"friendly_name": "person_2", "latitude": 2.1, "longitude": 1.1},
)
assert not test(hass)
hass.states.async_set(
"device_tracker.person_1",
"home",
{"friendly_name": "person_1", "latitude": 2.1, "longitude": 1.1},
)
hass.states.async_set(
"device_tracker.person_2",
"home",
{"friendly_name": "person_2", "latitude": 20.1, "longitude": 10.1},
)
assert not test(hass)
async def test_multiple_zones(hass):
"""Test with multiple entities in condition."""
test = await condition.async_from_config(
hass,
{
"condition": "and",
"conditions": [
{
"condition": "zone",
"entity_id": "device_tracker.person",
"zone": ["zone.home", "zone.work"],
},
],
},
)
hass.states.async_set(
"zone.home",
"zoning",
{"name": "home", "latitude": 2.1, "longitude": 1.1, "radius": 10},
)
hass.states.async_set(
"zone.work",
"zoning",
{"name": "work", "latitude": 20.1, "longitude": 10.1, "radius": 10},
)
hass.states.async_set(
"device_tracker.person",
"home",
{"friendly_name": "person", "latitude": 2.1, "longitude": 1.1},
)
assert test(hass)
hass.states.async_set(
"device_tracker.person",
"home",
{"friendly_name": "person", "latitude": 20.1, "longitude": 10.1},
)
assert test(hass)
hass.states.async_set(
"device_tracker.person",
"home",
{"friendly_name": "person", "latitude": 50.1, "longitude": 20.1},
)
assert not test(hass)
async def test_extract_entities():
"""Test extracting entities."""
assert condition.async_extract_entities(
{
"condition": "and",
"conditions": [
{
"condition": "state",
"entity_id": "sensor.temperature",
"state": "100",
},
{
"condition": "numeric_state",
"entity_id": "sensor.temperature_2",
"below": 110,
},
{
"condition": "not",
"conditions": [
{
"condition": "state",
"entity_id": "sensor.temperature_3",
"state": "100",
},
{
"condition": "numeric_state",
"entity_id": "sensor.temperature_4",
"below": 110,
},
],
},
{
"condition": "or",
"conditions": [
{
"condition": "state",
"entity_id": "sensor.temperature_5",
"state": "100",
},
{
"condition": "numeric_state",
"entity_id": "sensor.temperature_6",
"below": 110,
},
],
},
{
"condition": "state",
"entity_id": ["sensor.temperature_7", "sensor.temperature_8"],
"state": "100",
},
{
"condition": "numeric_state",
"entity_id": ["sensor.temperature_9", "sensor.temperature_10"],
"below": 110,
},
Template("{{ is_state('light.example', 'on') }}"),
],
}
) == {
"sensor.temperature",
"sensor.temperature_2",
"sensor.temperature_3",
"sensor.temperature_4",
"sensor.temperature_5",
"sensor.temperature_6",
"sensor.temperature_7",
"sensor.temperature_8",
"sensor.temperature_9",
"sensor.temperature_10",
}
async def test_extract_devices():
"""Test extracting devices."""
assert (
condition.async_extract_devices(
{
"condition": "and",
"conditions": [
{"condition": "device", "device_id": "abcd", "domain": "light"},
{"condition": "device", "device_id": "qwer", "domain": "switch"},
{
"condition": "state",
"entity_id": "sensor.not_a_device",
"state": "100",
},
{
"condition": "not",
"conditions": [
{
"condition": "device",
"device_id": "abcd_not",
"domain": "light",
},
{
"condition": "device",
"device_id": "qwer_not",
"domain": "switch",
},
],
},
{
"condition": "or",
"conditions": [
{
"condition": "device",
"device_id": "abcd_or",
"domain": "light",
},
{
"condition": "device",
"device_id": "qwer_or",
"domain": "switch",
},
],
},
Template("{{ is_state('light.example', 'on') }}"),
],
}
)
== {"abcd", "qwer", "abcd_not", "qwer_not", "abcd_or", "qwer_or"}
)
async def test_condition_template_error(hass):
"""Test invalid template."""
test = await condition.async_from_config(
hass, {"condition": "template", "value_template": "{{ undefined.state }}"}
)
with pytest.raises(ConditionError, match="template"):
test(hass)
async def test_condition_template_invalid_results(hass):
"""Test template condition render false with invalid results."""
test = await condition.async_from_config(
hass, {"condition": "template", "value_template": "{{ 'string' }}"}
)
assert not test(hass)
test = await condition.async_from_config(
hass, {"condition": "template", "value_template": "{{ 10.1 }}"}
)
assert not test(hass)
test = await condition.async_from_config(
hass, {"condition": "template", "value_template": "{{ 42 }}"}
)
assert not test(hass)
test = await condition.async_from_config(
hass, {"condition": "template", "value_template": "{{ [1, 2, 3] }}"}
)
assert not test(hass)