Improve validation of device condition config (#27131)
* Improve validation of device condition config * Fix typingpull/27140/head
parent
363873dfcb
commit
c43eeee62f
|
@ -7,10 +7,10 @@ import voluptuous as vol
|
|||
from homeassistant.const import CONF_PLATFORM
|
||||
from homeassistant.config import async_log_exception, config_without_domain
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import config_per_platform, script
|
||||
from homeassistant.helpers import condition, config_per_platform, script
|
||||
from homeassistant.loader import IntegrationNotFound
|
||||
|
||||
from . import CONF_ACTION, CONF_TRIGGER, DOMAIN, PLATFORM_SCHEMA
|
||||
from . import CONF_ACTION, CONF_CONDITION, CONF_TRIGGER, DOMAIN, PLATFORM_SCHEMA
|
||||
|
||||
# mypy: allow-untyped-calls, allow-untyped-defs
|
||||
# mypy: no-check-untyped-defs, no-warn-return-any
|
||||
|
@ -33,6 +33,13 @@ async def async_validate_config_item(hass, config, full_config=None):
|
|||
triggers.append(trigger)
|
||||
config[CONF_TRIGGER] = triggers
|
||||
|
||||
if CONF_CONDITION in config:
|
||||
conditions = []
|
||||
for cond in config[CONF_CONDITION]:
|
||||
cond = await condition.async_validate_condition_config(hass, cond)
|
||||
conditions.append(cond)
|
||||
config[CONF_CONDITION] = conditions
|
||||
|
||||
actions = []
|
||||
for action in config[CONF_ACTION]:
|
||||
action = await script.async_validate_action_config(hass, action)
|
||||
|
|
|
@ -232,7 +232,8 @@ def async_condition_from_config(
|
|||
config: ConfigType, config_validation: bool
|
||||
) -> condition.ConditionCheckerType:
|
||||
"""Evaluate state based on configuration."""
|
||||
config = CONDITION_SCHEMA(config)
|
||||
if config_validation:
|
||||
config = CONDITION_SCHEMA(config)
|
||||
condition_type = config[CONF_TYPE]
|
||||
if condition_type in IS_ON:
|
||||
stat = "on"
|
||||
|
|
|
@ -2,12 +2,14 @@
|
|||
import asyncio
|
||||
import logging
|
||||
from typing import Any, List, MutableMapping
|
||||
from types import ModuleType
|
||||
|
||||
import voluptuous as vol
|
||||
import voluptuous_serialize
|
||||
|
||||
from homeassistant.const import CONF_PLATFORM, CONF_DOMAIN, CONF_DEVICE_ID
|
||||
from homeassistant.components import websocket_api
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.entity_registry import async_entries_for_device
|
||||
from homeassistant.loader import async_get_integration, IntegrationNotFound
|
||||
|
@ -63,7 +65,9 @@ async def async_setup(hass, config):
|
|||
return True
|
||||
|
||||
|
||||
async def async_get_device_automation_platform(hass, domain, automation_type):
|
||||
async def async_get_device_automation_platform(
|
||||
hass: HomeAssistant, domain: str, automation_type: str
|
||||
) -> ModuleType:
|
||||
"""Load device automation platform for integration.
|
||||
|
||||
Throws InvalidDeviceAutomationConfig if the integration is not found or does not support device automation.
|
||||
|
|
|
@ -19,7 +19,8 @@ def async_condition_from_config(
|
|||
config: ConfigType, config_validation: bool
|
||||
) -> ConditionCheckerType:
|
||||
"""Evaluate state based on configuration."""
|
||||
config = CONDITION_SCHEMA(config)
|
||||
if config_validation:
|
||||
config = CONDITION_SCHEMA(config)
|
||||
return toggle_entity.async_condition_from_config(config, config_validation)
|
||||
|
||||
|
||||
|
|
|
@ -19,7 +19,8 @@ def async_condition_from_config(
|
|||
config: ConfigType, config_validation: bool
|
||||
) -> ConditionCheckerType:
|
||||
"""Evaluate state based on configuration."""
|
||||
config = CONDITION_SCHEMA(config)
|
||||
if config_validation:
|
||||
config = CONDITION_SCHEMA(config)
|
||||
return toggle_entity.async_condition_from_config(config, config_validation)
|
||||
|
||||
|
||||
|
|
|
@ -8,29 +8,31 @@ from typing import Callable, Container, Optional, Union, cast
|
|||
|
||||
from homeassistant.helpers.template import Template
|
||||
from homeassistant.helpers.typing import ConfigType, TemplateVarsType
|
||||
from homeassistant.loader import async_get_integration
|
||||
from homeassistant.core import HomeAssistant, State
|
||||
from homeassistant.components import zone as zone_cmp
|
||||
from homeassistant.components.device_automation import (
|
||||
async_get_device_automation_platform,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
ATTR_GPS_ACCURACY,
|
||||
ATTR_LATITUDE,
|
||||
ATTR_LONGITUDE,
|
||||
CONF_ABOVE,
|
||||
CONF_AFTER,
|
||||
CONF_BEFORE,
|
||||
CONF_BELOW,
|
||||
CONF_CONDITION,
|
||||
CONF_DOMAIN,
|
||||
CONF_ENTITY_ID,
|
||||
CONF_VALUE_TEMPLATE,
|
||||
CONF_CONDITION,
|
||||
WEEKDAYS,
|
||||
CONF_STATE,
|
||||
CONF_ZONE,
|
||||
CONF_BEFORE,
|
||||
CONF_AFTER,
|
||||
CONF_VALUE_TEMPLATE,
|
||||
CONF_WEEKDAY,
|
||||
SUN_EVENT_SUNRISE,
|
||||
SUN_EVENT_SUNSET,
|
||||
CONF_BELOW,
|
||||
CONF_ABOVE,
|
||||
CONF_ZONE,
|
||||
STATE_UNAVAILABLE,
|
||||
STATE_UNKNOWN,
|
||||
SUN_EVENT_SUNRISE,
|
||||
SUN_EVENT_SUNSET,
|
||||
WEEKDAYS,
|
||||
)
|
||||
from homeassistant.exceptions import TemplateError, HomeAssistantError
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
@ -498,9 +500,32 @@ async def async_device_from_config(
|
|||
"""Test a device condition."""
|
||||
if config_validation:
|
||||
config = cv.DEVICE_CONDITION_SCHEMA(config)
|
||||
integration = await async_get_integration(hass, config[CONF_DOMAIN])
|
||||
platform = integration.get_platform("device_condition")
|
||||
platform = await async_get_device_automation_platform(
|
||||
hass, config[CONF_DOMAIN], "condition"
|
||||
)
|
||||
return cast(
|
||||
ConditionCheckerType,
|
||||
platform.async_condition_from_config(config, config_validation), # type: ignore
|
||||
)
|
||||
|
||||
|
||||
async def async_validate_condition_config(
|
||||
hass: HomeAssistant, config: ConfigType
|
||||
) -> ConfigType:
|
||||
"""Validate config."""
|
||||
condition = config[CONF_CONDITION]
|
||||
if condition in ("and", "or"):
|
||||
conditions = []
|
||||
for sub_cond in config["conditions"]:
|
||||
sub_cond = await async_validate_condition_config(hass, sub_cond)
|
||||
conditions.append(sub_cond)
|
||||
config["conditions"] = conditions
|
||||
|
||||
if condition == "device":
|
||||
config = cv.DEVICE_CONDITION_SCHEMA(config)
|
||||
platform = await async_get_device_automation_platform(
|
||||
hass, config[CONF_DOMAIN], "condition"
|
||||
)
|
||||
return cast(ConfigType, platform.CONDITION_SCHEMA(config)) # type: ignore
|
||||
|
||||
return config
|
||||
|
|
|
@ -96,7 +96,12 @@ async def async_validate_action_config(
|
|||
platform = await device_automation.async_get_device_automation_platform(
|
||||
hass, config[CONF_DOMAIN], "action"
|
||||
)
|
||||
config = platform.ACTION_SCHEMA(config)
|
||||
config = platform.ACTION_SCHEMA(config) # type: ignore
|
||||
if action_type == ACTION_CHECK_CONDITION and config[CONF_CONDITION] == "device":
|
||||
platform = await device_automation.async_get_device_automation_platform(
|
||||
hass, config[CONF_DOMAIN], "condition"
|
||||
)
|
||||
config = platform.CONDITION_SCHEMA(config) # type: ignore
|
||||
|
||||
return config
|
||||
|
||||
|
|
|
@ -4,10 +4,16 @@ import pytest
|
|||
from homeassistant.setup import async_setup_component
|
||||
import homeassistant.components.automation as automation
|
||||
from homeassistant.components.websocket_api.const import TYPE_RESULT
|
||||
from homeassistant.const import STATE_ON, STATE_OFF, CONF_PLATFORM
|
||||
from homeassistant.helpers import device_registry
|
||||
|
||||
|
||||
from tests.common import MockConfigEntry, mock_device_registry, mock_registry
|
||||
from tests.common import (
|
||||
MockConfigEntry,
|
||||
async_mock_service,
|
||||
mock_device_registry,
|
||||
mock_registry,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
@ -301,6 +307,31 @@ async def test_automation_with_integration_without_device_action(hass, caplog):
|
|||
)
|
||||
|
||||
|
||||
async def test_automation_with_integration_without_device_condition(hass, caplog):
|
||||
"""Test automation with integration without device condition support."""
|
||||
assert await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"alias": "hello",
|
||||
"trigger": {"platform": "event", "event_type": "test_event1"},
|
||||
"condition": {
|
||||
"condition": "device",
|
||||
"device_id": "none",
|
||||
"domain": "test",
|
||||
},
|
||||
"action": {"service": "test.automation", "entity_id": "hello.world"},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
assert (
|
||||
"Integration 'test' does not support device automation conditions"
|
||||
in caplog.text
|
||||
)
|
||||
|
||||
|
||||
async def test_automation_with_integration_without_device_trigger(hass, caplog):
|
||||
"""Test automation with integration without device trigger support."""
|
||||
assert await async_setup_component(
|
||||
|
@ -341,6 +372,179 @@ async def test_automation_with_bad_action(hass, caplog):
|
|||
assert "required key not provided" in caplog.text
|
||||
|
||||
|
||||
async def test_automation_with_bad_condition_action(hass, caplog):
|
||||
"""Test automation with bad device action."""
|
||||
assert await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"alias": "hello",
|
||||
"trigger": {"platform": "event", "event_type": "test_event1"},
|
||||
"action": {"condition": "device", "device_id": "", "domain": "light"},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
assert "required key not provided" in caplog.text
|
||||
|
||||
|
||||
async def test_automation_with_bad_condition(hass, caplog):
|
||||
"""Test automation with bad device condition."""
|
||||
assert await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"alias": "hello",
|
||||
"trigger": {"platform": "event", "event_type": "test_event1"},
|
||||
"condition": {"condition": "device", "domain": "light"},
|
||||
"action": {"service": "test.automation", "entity_id": "hello.world"},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
assert "required key not provided" in caplog.text
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def calls(hass):
|
||||
"""Track calls to a mock serivce."""
|
||||
return async_mock_service(hass, "test", "automation")
|
||||
|
||||
|
||||
async def test_automation_with_sub_condition(hass, calls):
|
||||
"""Test automation with device condition under and/or conditions."""
|
||||
DOMAIN = "light"
|
||||
platform = getattr(hass.components, f"test.{DOMAIN}")
|
||||
|
||||
platform.init()
|
||||
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
|
||||
|
||||
ent1, ent2, ent3 = platform.ENTITIES
|
||||
|
||||
assert await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: [
|
||||
{
|
||||
"trigger": {"platform": "event", "event_type": "test_event1"},
|
||||
"condition": [
|
||||
{
|
||||
"condition": "and",
|
||||
"conditions": [
|
||||
{
|
||||
"condition": "device",
|
||||
"domain": DOMAIN,
|
||||
"device_id": "",
|
||||
"entity_id": ent1.entity_id,
|
||||
"type": "is_on",
|
||||
},
|
||||
{
|
||||
"condition": "device",
|
||||
"domain": DOMAIN,
|
||||
"device_id": "",
|
||||
"entity_id": ent2.entity_id,
|
||||
"type": "is_on",
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
"action": {
|
||||
"service": "test.automation",
|
||||
"data_template": {
|
||||
"some": "and {{ trigger.%s }}"
|
||||
% "}} - {{ trigger.".join(("platform", "event.event_type"))
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"trigger": {"platform": "event", "event_type": "test_event1"},
|
||||
"condition": [
|
||||
{
|
||||
"condition": "or",
|
||||
"conditions": [
|
||||
{
|
||||
"condition": "device",
|
||||
"domain": DOMAIN,
|
||||
"device_id": "",
|
||||
"entity_id": ent1.entity_id,
|
||||
"type": "is_on",
|
||||
},
|
||||
{
|
||||
"condition": "device",
|
||||
"domain": DOMAIN,
|
||||
"device_id": "",
|
||||
"entity_id": ent2.entity_id,
|
||||
"type": "is_on",
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
"action": {
|
||||
"service": "test.automation",
|
||||
"data_template": {
|
||||
"some": "or {{ trigger.%s }}"
|
||||
% "}} - {{ trigger.".join(("platform", "event.event_type"))
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert hass.states.get(ent1.entity_id).state == STATE_ON
|
||||
assert hass.states.get(ent2.entity_id).state == STATE_OFF
|
||||
assert len(calls) == 0
|
||||
|
||||
hass.bus.async_fire("test_event1")
|
||||
await hass.async_block_till_done()
|
||||
assert len(calls) == 1
|
||||
assert calls[0].data["some"] == "or event - test_event1"
|
||||
|
||||
hass.states.async_set(ent1.entity_id, STATE_OFF)
|
||||
hass.bus.async_fire("test_event1")
|
||||
await hass.async_block_till_done()
|
||||
assert len(calls) == 1
|
||||
|
||||
hass.states.async_set(ent2.entity_id, STATE_ON)
|
||||
hass.bus.async_fire("test_event1")
|
||||
await hass.async_block_till_done()
|
||||
assert len(calls) == 2
|
||||
assert calls[1].data["some"] == "or event - test_event1"
|
||||
|
||||
hass.states.async_set(ent1.entity_id, STATE_ON)
|
||||
hass.bus.async_fire("test_event1")
|
||||
await hass.async_block_till_done()
|
||||
assert len(calls) == 4
|
||||
assert _same_lists(
|
||||
[calls[2].data["some"], calls[3].data["some"]],
|
||||
["or event - test_event1", "and event - test_event1"],
|
||||
)
|
||||
|
||||
|
||||
async def test_automation_with_bad_sub_condition(hass, caplog):
|
||||
"""Test automation with bad device condition under and/or conditions."""
|
||||
assert await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"alias": "hello",
|
||||
"trigger": {"platform": "event", "event_type": "test_event1"},
|
||||
"condition": {
|
||||
"condition": "and",
|
||||
"conditions": [{"condition": "device", "domain": "light"}],
|
||||
},
|
||||
"action": {"service": "test.automation", "entity_id": "hello.world"},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
assert "required key not provided" in caplog.text
|
||||
|
||||
|
||||
async def test_automation_with_bad_trigger(hass, caplog):
|
||||
"""Test automation with bad device trigger."""
|
||||
assert await async_setup_component(
|
||||
|
|
Loading…
Reference in New Issue