mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 05:07:41 +00:00
Add shorthand notation for boolean conditions (#70120)
This commit is contained in:
parent
8f4979ea17
commit
b50f369fe4
@ -973,6 +973,41 @@ def custom_serializer(schema: Any) -> Any:
|
|||||||
return voluptuous_serialize.UNSUPPORTED
|
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
|
# Schemas
|
||||||
PLATFORM_SCHEMA = vol.Schema(
|
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(
|
OR_CONDITION_SCHEMA = vol.Schema(
|
||||||
{
|
{
|
||||||
**CONDITION_BASE_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(
|
NOT_CONDITION_SCHEMA = vol.Schema(
|
||||||
{
|
{
|
||||||
**CONDITION_BASE_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(
|
DEVICE_CONDITION_BASE_SCHEMA = vol.Schema(
|
||||||
{
|
{
|
||||||
**CONDITION_BASE_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(
|
CONDITION_SCHEMA: vol.Schema = vol.Schema(
|
||||||
vol.Any(
|
vol.Any(
|
||||||
key_value_schemas(
|
vol.All(
|
||||||
CONF_CONDITION,
|
expand_condition_shorthand,
|
||||||
{
|
key_value_schemas(
|
||||||
"and": AND_CONDITION_SCHEMA,
|
CONF_CONDITION,
|
||||||
"device": DEVICE_CONDITION_SCHEMA,
|
{
|
||||||
"not": NOT_CONDITION_SCHEMA,
|
"and": AND_CONDITION_SCHEMA,
|
||||||
"numeric_state": NUMERIC_STATE_CONDITION_SCHEMA,
|
"device": DEVICE_CONDITION_SCHEMA,
|
||||||
"or": OR_CONDITION_SCHEMA,
|
"not": NOT_CONDITION_SCHEMA,
|
||||||
"state": STATE_CONDITION_SCHEMA,
|
"numeric_state": NUMERIC_STATE_CONDITION_SCHEMA,
|
||||||
"sun": SUN_CONDITION_SCHEMA,
|
"or": OR_CONDITION_SCHEMA,
|
||||||
"template": TEMPLATE_CONDITION_SCHEMA,
|
"state": STATE_CONDITION_SCHEMA,
|
||||||
"time": TIME_CONDITION_SCHEMA,
|
"sun": SUN_CONDITION_SCHEMA,
|
||||||
"trigger": TRIGGER_CONDITION_SCHEMA,
|
"template": TEMPLATE_CONDITION_SCHEMA,
|
||||||
"zone": ZONE_CONDITION_SCHEMA,
|
"time": TIME_CONDITION_SCHEMA,
|
||||||
},
|
"trigger": TRIGGER_CONDITION_SCHEMA,
|
||||||
|
"zone": ZONE_CONDITION_SCHEMA,
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
dynamic_template_condition_action,
|
dynamic_template_condition_action,
|
||||||
)
|
)
|
||||||
@ -1318,23 +1399,26 @@ dynamic_template_condition_action = vol.All(
|
|||||||
|
|
||||||
|
|
||||||
CONDITION_ACTION_SCHEMA: vol.Schema = vol.Schema(
|
CONDITION_ACTION_SCHEMA: vol.Schema = vol.Schema(
|
||||||
key_value_schemas(
|
vol.All(
|
||||||
CONF_CONDITION,
|
expand_condition_shorthand,
|
||||||
{
|
key_value_schemas(
|
||||||
"and": AND_CONDITION_SCHEMA,
|
CONF_CONDITION,
|
||||||
"device": DEVICE_CONDITION_SCHEMA,
|
{
|
||||||
"not": NOT_CONDITION_SCHEMA,
|
"and": AND_CONDITION_SCHEMA,
|
||||||
"numeric_state": NUMERIC_STATE_CONDITION_SCHEMA,
|
"device": DEVICE_CONDITION_SCHEMA,
|
||||||
"or": OR_CONDITION_SCHEMA,
|
"not": NOT_CONDITION_SCHEMA,
|
||||||
"state": STATE_CONDITION_SCHEMA,
|
"numeric_state": NUMERIC_STATE_CONDITION_SCHEMA,
|
||||||
"sun": SUN_CONDITION_SCHEMA,
|
"or": OR_CONDITION_SCHEMA,
|
||||||
"template": TEMPLATE_CONDITION_SCHEMA,
|
"state": STATE_CONDITION_SCHEMA,
|
||||||
"time": TIME_CONDITION_SCHEMA,
|
"sun": SUN_CONDITION_SCHEMA,
|
||||||
"trigger": TRIGGER_CONDITION_SCHEMA,
|
"template": TEMPLATE_CONDITION_SCHEMA,
|
||||||
"zone": ZONE_CONDITION_SCHEMA,
|
"time": TIME_CONDITION_SCHEMA,
|
||||||
},
|
"trigger": TRIGGER_CONDITION_SCHEMA,
|
||||||
dynamic_template_condition_action,
|
"zone": ZONE_CONDITION_SCHEMA,
|
||||||
"a valid template",
|
},
|
||||||
|
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
|
from unittest.mock import AsyncMock, patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components import sun
|
from homeassistant.components import sun
|
||||||
import homeassistant.components.automation as automation
|
import homeassistant.components.automation as automation
|
||||||
@ -288,6 +289,101 @@ async def test_and_condition_with_template(hass):
|
|||||||
assert test(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):
|
async def test_or_condition(hass):
|
||||||
"""Test the 'or' condition."""
|
"""Test the 'or' condition."""
|
||||||
config = {
|
config = {
|
||||||
@ -471,6 +567,36 @@ async def test_or_condition_with_template(hass):
|
|||||||
assert test(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):
|
async def test_not_condition(hass):
|
||||||
"""Test the 'not' condition."""
|
"""Test the 'not' condition."""
|
||||||
config = {
|
config = {
|
||||||
@ -670,6 +796,42 @@ async def test_not_condition_with_template(hass):
|
|||||||
assert not test(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):
|
async def test_time_window(hass):
|
||||||
"""Test time condition windows."""
|
"""Test time condition windows."""
|
||||||
sixam = "06:00:00"
|
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):
|
async def test_shorthand_template_condition(hass, caplog):
|
||||||
"""Test if we can use shorthand template conditions in a script."""
|
"""Test if we can use shorthand template conditions in a script."""
|
||||||
event = "test_event"
|
event = "test_event"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user