Add shorthand notation for boolean conditions (#70120)

pull/70249/head
Thomas Lovén 2022-04-18 22:09:09 +02:00 committed by GitHub
parent 8f4979ea17
commit b50f369fe4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 314 additions and 32 deletions

View File

@ -973,6 +973,41 @@ def custom_serializer(schema: Any) -> Any:
return voluptuous_serialize.UNSUPPORTED
def expand_condition_shorthand(value: Any | None) -> Any:
"""Expand boolean condition shorthand notations."""
if not isinstance(value, dict) or CONF_CONDITIONS in value:
return value
for key, schema in (
("and", AND_CONDITION_SHORTHAND_SCHEMA),
("or", OR_CONDITION_SHORTHAND_SCHEMA),
("not", NOT_CONDITION_SHORTHAND_SCHEMA),
):
try:
schema(value)
return {
CONF_CONDITION: key,
CONF_CONDITIONS: value[key],
**{k: value[k] for k in value if k != key},
}
except vol.MultipleInvalid:
pass
if isinstance(value.get(CONF_CONDITION), list):
try:
CONDITION_SHORTHAND_SCHEMA(value)
return {
CONF_CONDITION: "and",
CONF_CONDITIONS: value[CONF_CONDITION],
**{k: value[k] for k in value if k != CONF_CONDITION},
}
except vol.MultipleInvalid:
pass
return value
# Schemas
PLATFORM_SCHEMA = vol.Schema(
{
@ -1236,6 +1271,17 @@ AND_CONDITION_SCHEMA = vol.Schema(
}
)
AND_CONDITION_SHORTHAND_SCHEMA = vol.Schema(
{
**CONDITION_BASE_SCHEMA,
vol.Required("and"): vol.All(
ensure_list,
# pylint: disable=unnecessary-lambda
[lambda value: CONDITION_SCHEMA(value)],
),
}
)
OR_CONDITION_SCHEMA = vol.Schema(
{
**CONDITION_BASE_SCHEMA,
@ -1248,6 +1294,17 @@ OR_CONDITION_SCHEMA = vol.Schema(
}
)
OR_CONDITION_SHORTHAND_SCHEMA = vol.Schema(
{
**CONDITION_BASE_SCHEMA,
vol.Required("or"): vol.All(
ensure_list,
# pylint: disable=unnecessary-lambda
[lambda value: CONDITION_SCHEMA(value)],
),
}
)
NOT_CONDITION_SCHEMA = vol.Schema(
{
**CONDITION_BASE_SCHEMA,
@ -1260,6 +1317,17 @@ NOT_CONDITION_SCHEMA = vol.Schema(
}
)
NOT_CONDITION_SHORTHAND_SCHEMA = vol.Schema(
{
**CONDITION_BASE_SCHEMA,
vol.Required("not"): vol.All(
ensure_list,
# pylint: disable=unnecessary-lambda
[lambda value: CONDITION_SCHEMA(value)],
),
}
)
DEVICE_CONDITION_BASE_SCHEMA = vol.Schema(
{
**CONDITION_BASE_SCHEMA,
@ -1280,24 +1348,37 @@ dynamic_template_condition_action = vol.All(
},
)
CONDITION_SHORTHAND_SCHEMA = vol.Schema(
{
**CONDITION_BASE_SCHEMA,
vol.Required(CONF_CONDITION): vol.All(
ensure_list,
# pylint: disable=unnecessary-lambda
[lambda value: CONDITION_SCHEMA(value)],
),
}
)
CONDITION_SCHEMA: vol.Schema = vol.Schema(
vol.Any(
key_value_schemas(
CONF_CONDITION,
{
"and": AND_CONDITION_SCHEMA,
"device": DEVICE_CONDITION_SCHEMA,
"not": NOT_CONDITION_SCHEMA,
"numeric_state": NUMERIC_STATE_CONDITION_SCHEMA,
"or": OR_CONDITION_SCHEMA,
"state": STATE_CONDITION_SCHEMA,
"sun": SUN_CONDITION_SCHEMA,
"template": TEMPLATE_CONDITION_SCHEMA,
"time": TIME_CONDITION_SCHEMA,
"trigger": TRIGGER_CONDITION_SCHEMA,
"zone": ZONE_CONDITION_SCHEMA,
},
vol.All(
expand_condition_shorthand,
key_value_schemas(
CONF_CONDITION,
{
"and": AND_CONDITION_SCHEMA,
"device": DEVICE_CONDITION_SCHEMA,
"not": NOT_CONDITION_SCHEMA,
"numeric_state": NUMERIC_STATE_CONDITION_SCHEMA,
"or": OR_CONDITION_SCHEMA,
"state": STATE_CONDITION_SCHEMA,
"sun": SUN_CONDITION_SCHEMA,
"template": TEMPLATE_CONDITION_SCHEMA,
"time": TIME_CONDITION_SCHEMA,
"trigger": TRIGGER_CONDITION_SCHEMA,
"zone": ZONE_CONDITION_SCHEMA,
},
),
),
dynamic_template_condition_action,
)
@ -1318,23 +1399,26 @@ dynamic_template_condition_action = vol.All(
CONDITION_ACTION_SCHEMA: vol.Schema = vol.Schema(
key_value_schemas(
CONF_CONDITION,
{
"and": AND_CONDITION_SCHEMA,
"device": DEVICE_CONDITION_SCHEMA,
"not": NOT_CONDITION_SCHEMA,
"numeric_state": NUMERIC_STATE_CONDITION_SCHEMA,
"or": OR_CONDITION_SCHEMA,
"state": STATE_CONDITION_SCHEMA,
"sun": SUN_CONDITION_SCHEMA,
"template": TEMPLATE_CONDITION_SCHEMA,
"time": TIME_CONDITION_SCHEMA,
"trigger": TRIGGER_CONDITION_SCHEMA,
"zone": ZONE_CONDITION_SCHEMA,
},
dynamic_template_condition_action,
"a valid template",
vol.All(
expand_condition_shorthand,
key_value_schemas(
CONF_CONDITION,
{
"and": AND_CONDITION_SCHEMA,
"device": DEVICE_CONDITION_SCHEMA,
"not": NOT_CONDITION_SCHEMA,
"numeric_state": NUMERIC_STATE_CONDITION_SCHEMA,
"or": OR_CONDITION_SCHEMA,
"state": STATE_CONDITION_SCHEMA,
"sun": SUN_CONDITION_SCHEMA,
"template": TEMPLATE_CONDITION_SCHEMA,
"time": TIME_CONDITION_SCHEMA,
"trigger": TRIGGER_CONDITION_SCHEMA,
"zone": ZONE_CONDITION_SCHEMA,
},
dynamic_template_condition_action,
"a list of conditions or a valid template",
),
)
)

View File

@ -3,6 +3,7 @@ from datetime import datetime
from unittest.mock import AsyncMock, patch
import pytest
import voluptuous as vol
from homeassistant.components import sun
import homeassistant.components.automation as automation
@ -288,6 +289,101 @@ async def test_and_condition_with_template(hass):
assert test(hass)
async def test_and_condition_shorthand(hass):
"""Test the 'and' condition shorthand."""
config = {
"alias": "And Condition Shorthand",
"and": [
{
"alias": "Template Condition",
"condition": "template",
"value_template": '{{ states.sensor.temperature.state == "100" }}',
},
{
"condition": "numeric_state",
"entity_id": "sensor.temperature",
"below": 110,
},
],
}
config = cv.CONDITION_SCHEMA(config)
config = await condition.async_validate_condition_config(hass, config)
test = await condition.async_from_config(hass, config)
assert config["alias"] == "And Condition Shorthand"
assert "and" not in config.keys()
hass.states.async_set("sensor.temperature", 120)
assert not test(hass)
assert_condition_trace(
{
"": [{"result": {"result": False}}],
"conditions/0": [
{"result": {"entities": ["sensor.temperature"], "result": False}}
],
}
)
hass.states.async_set("sensor.temperature", 105)
assert not test(hass)
hass.states.async_set("sensor.temperature", 100)
assert test(hass)
async def test_and_condition_list_shorthand(hass):
"""Test the 'and' condition list shorthand."""
config = {
"alias": "And Condition List Shorthand",
"condition": [
{
"alias": "Template Condition",
"condition": "template",
"value_template": '{{ states.sensor.temperature.state == "100" }}',
},
{
"condition": "numeric_state",
"entity_id": "sensor.temperature",
"below": 110,
},
],
}
config = cv.CONDITION_SCHEMA(config)
config = await condition.async_validate_condition_config(hass, config)
test = await condition.async_from_config(hass, config)
assert config["alias"] == "And Condition List Shorthand"
assert "and" not in config.keys()
hass.states.async_set("sensor.temperature", 120)
assert not test(hass)
assert_condition_trace(
{
"": [{"result": {"result": False}}],
"conditions/0": [
{"result": {"entities": ["sensor.temperature"], "result": False}}
],
}
)
hass.states.async_set("sensor.temperature", 105)
assert not test(hass)
hass.states.async_set("sensor.temperature", 100)
assert test(hass)
async def test_malformed_and_condition_list_shorthand(hass):
"""Test the 'and' condition list shorthand syntax check."""
config = {
"alias": "Bad shorthand syntax",
"condition": ["bad", "syntax"],
}
with pytest.raises(vol.MultipleInvalid):
cv.CONDITION_SCHEMA(config)
async def test_or_condition(hass):
"""Test the 'or' condition."""
config = {
@ -471,6 +567,36 @@ async def test_or_condition_with_template(hass):
assert test(hass)
async def test_or_condition_shorthand(hass):
"""Test the 'or' condition shorthand."""
config = {
"alias": "Or Condition Shorthand",
"or": [
{'{{ states.sensor.temperature.state == "100" }}'},
{
"condition": "numeric_state",
"entity_id": "sensor.temperature",
"below": 110,
},
],
}
config = cv.CONDITION_SCHEMA(config)
config = await condition.async_validate_condition_config(hass, config)
test = await condition.async_from_config(hass, config)
assert config["alias"] == "Or Condition Shorthand"
assert "or" not in config.keys()
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."""
config = {
@ -670,6 +796,42 @@ async def test_not_condition_with_template(hass):
assert not test(hass)
async def test_not_condition_shorthand(hass):
"""Test the 'or' condition shorthand."""
config = {
"alias": "Not Condition Shorthand",
"not": [
{
"condition": "template",
"value_template": '{{ states.sensor.temperature.state == "100" }}',
},
{
"condition": "numeric_state",
"entity_id": "sensor.temperature",
"below": 50,
},
],
}
config = cv.CONDITION_SCHEMA(config)
config = await condition.async_validate_condition_config(hass, config)
test = await condition.async_from_config(hass, config)
assert config["alias"] == "Not Condition Shorthand"
assert "not" not in config.keys()
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"

View File

@ -1508,6 +1508,42 @@ async def test_condition_basic(hass, caplog):
)
async def test_and_default_condition(hass, caplog):
"""Test that a list of conditions evaluates as AND."""
alias = "condition step"
sequence = cv.SCRIPT_SCHEMA(
[
{
"alias": alias,
"condition": [
{
"condition": "template",
"value_template": "{{ states.test.entity.state == 'hello' }}",
},
{
"condition": "numeric_state",
"entity_id": "sensor.temperature",
"below": 110,
},
],
},
]
)
script_obj = script.Script(hass, sequence, "Test Name", "test_domain")
hass.states.async_set("sensor.temperature", 100)
hass.states.async_set("test.entity", "hello")
await script_obj.async_run(context=Context())
await hass.async_block_till_done()
assert f"Test condition {alias}: True" in caplog.text
caplog.clear()
hass.states.async_set("sensor.temperature", 120)
await script_obj.async_run(context=Context())
await hass.async_block_till_done()
assert f"Test condition {alias}: False" in caplog.text
async def test_shorthand_template_condition(hass, caplog):
"""Test if we can use shorthand template conditions in a script."""
event = "test_event"