Add shorthand notation for boolean conditions (#70120)
parent
8f4979ea17
commit
b50f369fe4
|
@ -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",
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue