Add device automation condition (#26313)

* Add support for device conditions

* Lint

* Update test case

* Make and+or conditions async, adjust tests

* Cleanup tests

* Remove non callback versions of conditions, correct typing

* Correct typing

* Update light/strings.json

* Address review comments

* Make device automation lists simple lists, not dicts

* Add device_automation/const.py

* Use IS_ON/IS_OFF everywhere for conditions
This commit is contained in:
Erik Montnemery 2019-09-05 16:49:32 +02:00 committed by GitHub
parent c50faaef3c
commit f7dc537275
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 480 additions and 263 deletions

View File

@ -386,7 +386,7 @@ async def _async_process_config(hass, config, component):
action = _async_get_action(hass, config_block.get(CONF_ACTION, {}), name) action = _async_get_action(hass, config_block.get(CONF_ACTION, {}), name)
if CONF_CONDITION in config_block: if CONF_CONDITION in config_block:
cond_func = _async_process_if(hass, config, config_block) cond_func = await _async_process_if(hass, config, config_block)
if cond_func is None: if cond_func is None:
continue continue
@ -437,14 +437,14 @@ def _async_get_action(hass, config, name):
return action return action
def _async_process_if(hass, config, p_config): async def _async_process_if(hass, config, p_config):
"""Process if checks.""" """Process if checks."""
if_configs = p_config.get(CONF_CONDITION) if_configs = p_config.get(CONF_CONDITION)
checks = [] checks = []
for if_config in if_configs: for if_config in if_configs:
try: try:
checks.append(condition.async_from_config(if_config, False)) checks.append(await condition.async_from_config(hass, if_config, False))
except HomeAssistantError as ex: except HomeAssistantError as ex:
_LOGGER.warning("Invalid condition: %s", ex) _LOGGER.warning("Invalid condition: %s", ex)
return None return None

View File

@ -1,12 +1,16 @@
"""Helpers for device automations.""" """Helpers for device automations."""
import asyncio import asyncio
import logging import logging
from typing import Callable, cast
import voluptuous as vol import voluptuous as vol
from homeassistant.components import websocket_api from homeassistant.components import websocket_api
from homeassistant.core import split_entity_id from homeassistant.const import CONF_DOMAIN
from homeassistant.core import split_entity_id, HomeAssistant
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_registry import async_entries_for_device from homeassistant.helpers.entity_registry import async_entries_for_device
from homeassistant.helpers.typing import ConfigType
from homeassistant.loader import async_get_integration, IntegrationNotFound from homeassistant.loader import async_get_integration, IntegrationNotFound
DOMAIN = "device_automation" DOMAIN = "device_automation"
@ -16,14 +20,31 @@ _LOGGER = logging.getLogger(__name__)
async def async_setup(hass, config): async def async_setup(hass, config):
"""Set up device automation.""" """Set up device automation."""
hass.components.websocket_api.async_register_command(
websocket_device_automation_list_conditions
)
hass.components.websocket_api.async_register_command( hass.components.websocket_api.async_register_command(
websocket_device_automation_list_triggers websocket_device_automation_list_triggers
) )
return True return True
async def _async_get_device_automation_triggers(hass, domain, device_id): async def async_device_condition_from_config(
"""List device triggers.""" hass: HomeAssistant, config: ConfigType, config_validation: bool = True
) -> Callable[..., bool]:
"""Wrap action method with state based condition."""
if config_validation:
config = cv.DEVICE_CONDITION_SCHEMA(config)
integration = await async_get_integration(hass, config[CONF_DOMAIN])
platform = integration.get_platform("device_automation")
return cast(
Callable[..., bool],
platform.async_condition_from_config(config, config_validation), # type: ignore
)
async def _async_get_device_automations_from_domain(hass, domain, fname, device_id):
"""List device automations."""
integration = None integration = None
try: try:
integration = await async_get_integration(hass, domain) integration = await async_get_integration(hass, domain)
@ -37,19 +58,19 @@ async def _async_get_device_automation_triggers(hass, domain, device_id):
# The domain does not have device automations # The domain does not have device automations
return None return None
if hasattr(platform, "async_get_triggers"): if hasattr(platform, fname):
return await platform.async_get_triggers(hass, device_id) return await getattr(platform, fname)(hass, device_id)
async def async_get_device_automation_triggers(hass, device_id): async def _async_get_device_automations(hass, fname, device_id):
"""List device triggers.""" """List device automations."""
device_registry, entity_registry = await asyncio.gather( device_registry, entity_registry = await asyncio.gather(
hass.helpers.device_registry.async_get_registry(), hass.helpers.device_registry.async_get_registry(),
hass.helpers.entity_registry.async_get_registry(), hass.helpers.entity_registry.async_get_registry(),
) )
domains = set() domains = set()
triggers = [] automations = []
device = device_registry.async_get(device_id) device = device_registry.async_get(device_id)
for entry_id in device.config_entries: for entry_id in device.config_entries:
config_entry = hass.config_entries.async_get_entry(entry_id) config_entry = hass.config_entries.async_get_entry(entry_id)
@ -59,17 +80,33 @@ async def async_get_device_automation_triggers(hass, device_id):
for entity in entities: for entity in entities:
domains.add(split_entity_id(entity.entity_id)[0]) domains.add(split_entity_id(entity.entity_id)[0])
device_triggers = await asyncio.gather( device_automations = await asyncio.gather(
*( *(
_async_get_device_automation_triggers(hass, domain, device_id) _async_get_device_automations_from_domain(hass, domain, fname, device_id)
for domain in domains for domain in domains
) )
) )
for device_trigger in device_triggers: for device_automation in device_automations:
if device_trigger is not None: if device_automation is not None:
triggers.extend(device_trigger) automations.extend(device_automation)
return triggers return automations
@websocket_api.async_response
@websocket_api.websocket_command(
{
vol.Required("type"): "device_automation/condition/list",
vol.Required("device_id"): str,
}
)
async def websocket_device_automation_list_conditions(hass, connection, msg):
"""Handle request for device conditions."""
device_id = msg["device_id"]
conditions = await _async_get_device_automations(
hass, "async_get_conditions", device_id
)
connection.send_result(msg["id"], conditions)
@websocket_api.async_response @websocket_api.async_response
@ -82,5 +119,7 @@ async def async_get_device_automation_triggers(hass, device_id):
async def websocket_device_automation_list_triggers(hass, connection, msg): async def websocket_device_automation_list_triggers(hass, connection, msg):
"""Handle request for device triggers.""" """Handle request for device triggers."""
device_id = msg["device_id"] device_id = msg["device_id"]
triggers = await async_get_device_automation_triggers(hass, device_id) triggers = await _async_get_device_automations(
connection.send_result(msg["id"], {"triggers": triggers}) hass, "async_get_triggers", device_id
)
connection.send_result(msg["id"], triggers)

View File

@ -0,0 +1,5 @@
"""Constants for device automations."""
CONF_IS_OFF = "is_off"
CONF_IS_ON = "is_on"
CONF_TURN_OFF = "turn_off"
CONF_TURN_ON = "turn_on"

View File

@ -2,39 +2,70 @@
import voluptuous as vol import voluptuous as vol
import homeassistant.components.automation.state as state import homeassistant.components.automation.state as state
from homeassistant.components.device_automation.const import (
CONF_IS_OFF,
CONF_IS_ON,
CONF_TURN_OFF,
CONF_TURN_ON,
)
from homeassistant.core import split_entity_id from homeassistant.core import split_entity_id
from homeassistant.const import ( from homeassistant.const import (
CONF_CONDITION,
CONF_DEVICE_ID, CONF_DEVICE_ID,
CONF_DOMAIN, CONF_DOMAIN,
CONF_ENTITY_ID, CONF_ENTITY_ID,
CONF_PLATFORM, CONF_PLATFORM,
CONF_TYPE, CONF_TYPE,
) )
import homeassistant.helpers.config_validation as cv from homeassistant.helpers import condition, config_validation as cv
from homeassistant.helpers.entity_registry import async_entries_for_device from homeassistant.helpers.entity_registry import async_entries_for_device
from . import DOMAIN from . import DOMAIN
# mypy: allow-untyped-defs, no-check-untyped-defs # mypy: allow-untyped-defs, no-check-untyped-defs
CONF_TURN_OFF = "turn_off" ENTITY_CONDITIONS = [
CONF_TURN_ON = "turn_on" {
# True when light is turned off
CONF_CONDITION: "device",
CONF_DOMAIN: DOMAIN,
CONF_TYPE: CONF_IS_OFF,
},
{
# True when light is turned on
CONF_CONDITION: "device",
CONF_DOMAIN: DOMAIN,
CONF_TYPE: CONF_IS_ON,
},
]
ENTITY_TRIGGERS = [ ENTITY_TRIGGERS = [
{ {
# Trigger when light is turned on # Trigger when light is turned off
CONF_PLATFORM: "device", CONF_PLATFORM: "device",
CONF_DOMAIN: DOMAIN, CONF_DOMAIN: DOMAIN,
CONF_TYPE: CONF_TURN_OFF, CONF_TYPE: CONF_TURN_OFF,
}, },
{ {
# Trigger when light is turned off # Trigger when light is turned on
CONF_PLATFORM: "device", CONF_PLATFORM: "device",
CONF_DOMAIN: DOMAIN, CONF_DOMAIN: DOMAIN,
CONF_TYPE: CONF_TURN_ON, CONF_TYPE: CONF_TURN_ON,
}, },
] ]
CONDITION_SCHEMA = vol.All(
vol.Schema(
{
vol.Required(CONF_CONDITION): "device",
vol.Optional(CONF_DEVICE_ID): str,
vol.Required(CONF_DOMAIN): DOMAIN,
vol.Required(CONF_ENTITY_ID): cv.entity_id,
vol.Required(CONF_TYPE): vol.In([CONF_IS_OFF, CONF_IS_ON]),
}
)
)
TRIGGER_SCHEMA = vol.All( TRIGGER_SCHEMA = vol.All(
vol.Schema( vol.Schema(
{ {
@ -42,7 +73,7 @@ TRIGGER_SCHEMA = vol.All(
vol.Optional(CONF_DEVICE_ID): str, vol.Optional(CONF_DEVICE_ID): str,
vol.Required(CONF_DOMAIN): DOMAIN, vol.Required(CONF_DOMAIN): DOMAIN,
vol.Required(CONF_ENTITY_ID): cv.entity_id, vol.Required(CONF_ENTITY_ID): cv.entity_id,
vol.Required(CONF_TYPE): str, vol.Required(CONF_TYPE): vol.In([CONF_TURN_OFF, CONF_TURN_ON]),
} }
) )
) )
@ -52,9 +83,27 @@ def _is_domain(entity, domain):
return split_entity_id(entity.entity_id)[0] == domain return split_entity_id(entity.entity_id)[0] == domain
def async_condition_from_config(config, config_validation):
"""Evaluate state based on configuration."""
config = CONDITION_SCHEMA(config)
condition_type = config[CONF_TYPE]
if condition_type == CONF_IS_ON:
stat = "on"
else:
stat = "off"
state_config = {
condition.CONF_CONDITION: "state",
condition.CONF_ENTITY_ID: config[CONF_ENTITY_ID],
condition.CONF_STATE: stat,
}
return condition.state_from_config(state_config, config_validation)
async def async_attach_trigger(hass, config, action, automation_info): async def async_attach_trigger(hass, config, action, automation_info):
"""Listen for state changes based on configuration.""" """Listen for state changes based on configuration."""
trigger_type = config.get(CONF_TYPE) config = TRIGGER_SCHEMA(config)
trigger_type = config[CONF_TYPE]
if trigger_type == CONF_TURN_ON: if trigger_type == CONF_TURN_ON:
from_state = "off" from_state = "off"
to_state = "on" to_state = "on"
@ -75,17 +124,27 @@ async def async_trigger(hass, config, action, automation_info):
return await async_attach_trigger(hass, config, action, automation_info) return await async_attach_trigger(hass, config, action, automation_info)
async def async_get_triggers(hass, device_id): async def _async_get_automations(hass, device_id, automation_templates):
"""List device triggers.""" """List device automations."""
triggers = [] automations = []
entity_registry = await hass.helpers.entity_registry.async_get_registry() entity_registry = await hass.helpers.entity_registry.async_get_registry()
entities = async_entries_for_device(entity_registry, device_id) entities = async_entries_for_device(entity_registry, device_id)
domain_entities = [x for x in entities if _is_domain(x, DOMAIN)] domain_entities = [x for x in entities if _is_domain(x, DOMAIN)]
for entity in domain_entities: for entity in domain_entities:
for trigger in ENTITY_TRIGGERS: for automation in automation_templates:
trigger = dict(trigger) automation = dict(automation)
trigger.update(device_id=device_id, entity_id=entity.entity_id) automation.update(device_id=device_id, entity_id=entity.entity_id)
triggers.append(trigger) automations.append(automation)
return triggers return automations
async def async_get_conditions(hass, device_id):
"""List device conditions."""
return await _async_get_automations(hass, device_id, ENTITY_CONDITIONS)
async def async_get_triggers(hass, device_id):
"""List device triggers."""
return await _async_get_automations(hass, device_id, ENTITY_TRIGGERS)

View File

@ -1,5 +1,9 @@
{ {
"device_automation": { "device_automation": {
"condition_type": {
"is_on": "{name} is on",
"is_off": "{name} is off"
},
"trigger_type": { "trigger_type": {
"turn_on": "{name} turned on", "turn_on": "{name} turned on",
"turn_off": "{name} turned off" "turn_off": "{name} turned off"

View File

@ -1,4 +1,5 @@
"""Offer reusable conditions.""" """Offer reusable conditions."""
import asyncio
from datetime import datetime, timedelta from datetime import datetime, timedelta
import functools as ft import functools as ft
import logging import logging
@ -10,6 +11,9 @@ from homeassistant.helpers.typing import ConfigType, TemplateVarsType
from homeassistant.core import HomeAssistant, State from homeassistant.core import HomeAssistant, State
from homeassistant.components import zone as zone_cmp from homeassistant.components import zone as zone_cmp
from homeassistant.components.device_automation import ( # noqa: F401 pylint: disable=unused-import
async_device_condition_from_config as async_device_from_config,
)
from homeassistant.const import ( from homeassistant.const import (
ATTR_GPS_ACCURACY, ATTR_GPS_ACCURACY,
ATTR_LATITUDE, ATTR_LATITUDE,
@ -41,40 +45,9 @@ ASYNC_FROM_CONFIG_FORMAT = "async_{}_from_config"
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
# PyLint does not like the use of _threaded_factory
# pylint: disable=invalid-name
async def async_from_config(
def _threaded_factory( hass: HomeAssistant, config: ConfigType, config_validation: bool = True
async_factory: Callable[[ConfigType, bool], Callable[..., bool]]
) -> Callable[[ConfigType, bool], Callable[..., bool]]:
"""Create threaded versions of async factories."""
@ft.wraps(async_factory)
def factory(
config: ConfigType, config_validation: bool = True
) -> Callable[..., bool]:
"""Threaded factory."""
async_check = async_factory(config, config_validation)
def condition_if(
hass: HomeAssistant, variables: TemplateVarsType = None
) -> bool:
"""Validate condition."""
return cast(
bool,
run_callback_threadsafe(
hass.loop, async_check, hass, variables
).result(),
)
return condition_if
return factory
def async_from_config(
config: ConfigType, config_validation: bool = True
) -> Callable[..., bool]: ) -> Callable[..., bool]:
"""Turn a condition configuration into a method. """Turn a condition configuration into a method.
@ -95,29 +68,30 @@ def async_from_config(
) )
) )
# Check for partials to properly determine if coroutine function
check_factory = factory
while isinstance(check_factory, ft.partial):
check_factory = check_factory.func
if asyncio.iscoroutinefunction(check_factory):
return cast(Callable[..., bool], await factory(hass, config, config_validation))
return cast(Callable[..., bool], factory(config, config_validation)) return cast(Callable[..., bool], factory(config, config_validation))
from_config = _threaded_factory(async_from_config) async def async_and_from_config(
hass: HomeAssistant, config: ConfigType, config_validation: bool = True
def async_and_from_config(
config: ConfigType, config_validation: bool = True
) -> Callable[..., bool]: ) -> Callable[..., bool]:
"""Create multi condition matcher using 'AND'.""" """Create multi condition matcher using 'AND'."""
if config_validation: if config_validation:
config = cv.AND_CONDITION_SCHEMA(config) config = cv.AND_CONDITION_SCHEMA(config)
checks = None checks = [
await async_from_config(hass, entry, False) for entry in config["conditions"]
]
def if_and_condition( def if_and_condition(
hass: HomeAssistant, variables: TemplateVarsType = None hass: HomeAssistant, variables: TemplateVarsType = None
) -> bool: ) -> bool:
"""Test and condition.""" """Test and condition."""
nonlocal checks
if checks is None:
checks = [async_from_config(entry, False) for entry in config["conditions"]]
try: try:
for check in checks: for check in checks:
if not check(hass, variables): if not check(hass, variables):
@ -131,26 +105,20 @@ def async_and_from_config(
return if_and_condition return if_and_condition
and_from_config = _threaded_factory(async_and_from_config) async def async_or_from_config(
hass: HomeAssistant, config: ConfigType, config_validation: bool = True
def async_or_from_config(
config: ConfigType, config_validation: bool = True
) -> Callable[..., bool]: ) -> Callable[..., bool]:
"""Create multi condition matcher using 'OR'.""" """Create multi condition matcher using 'OR'."""
if config_validation: if config_validation:
config = cv.OR_CONDITION_SCHEMA(config) config = cv.OR_CONDITION_SCHEMA(config)
checks = None checks = [
await async_from_config(hass, entry, False) for entry in config["conditions"]
]
def if_or_condition( def if_or_condition(
hass: HomeAssistant, variables: TemplateVarsType = None hass: HomeAssistant, variables: TemplateVarsType = None
) -> bool: ) -> bool:
"""Test and condition.""" """Test and condition."""
nonlocal checks
if checks is None:
checks = [async_from_config(entry, False) for entry in config["conditions"]]
try: try:
for check in checks: for check in checks:
if check(hass, variables): if check(hass, variables):
@ -163,9 +131,6 @@ def async_or_from_config(
return if_or_condition return if_or_condition
or_from_config = _threaded_factory(async_or_from_config)
def numeric_state( def numeric_state(
hass: HomeAssistant, hass: HomeAssistant,
entity: Union[None, str, State], entity: Union[None, str, State],
@ -263,9 +228,6 @@ def async_numeric_state_from_config(
return if_numeric_state return if_numeric_state
numeric_state_from_config = _threaded_factory(async_numeric_state_from_config)
def state( def state(
hass: HomeAssistant, hass: HomeAssistant,
entity: Union[None, str, State], entity: Union[None, str, State],
@ -423,9 +385,6 @@ def async_template_from_config(
return template_if return template_if
template_from_config = _threaded_factory(async_template_from_config)
def time( def time(
before: Optional[dt_util.dt.time] = None, before: Optional[dt_util.dt.time] = None,
after: Optional[dt_util.dt.time] = None, after: Optional[dt_util.dt.time] = None,

View File

@ -24,10 +24,13 @@ from homeassistant.const import (
CONF_ALIAS, CONF_ALIAS,
CONF_BELOW, CONF_BELOW,
CONF_CONDITION, CONF_CONDITION,
CONF_DOMAIN,
CONF_ENTITY_ID, CONF_ENTITY_ID,
CONF_ENTITY_NAMESPACE, CONF_ENTITY_NAMESPACE,
CONF_FOR,
CONF_PLATFORM, CONF_PLATFORM,
CONF_SCAN_INTERVAL, CONF_SCAN_INTERVAL,
CONF_STATE,
CONF_UNIT_SYSTEM_IMPERIAL, CONF_UNIT_SYSTEM_IMPERIAL,
CONF_UNIT_SYSTEM_METRIC, CONF_UNIT_SYSTEM_METRIC,
CONF_VALUE_TEMPLATE, CONF_VALUE_TEMPLATE,
@ -746,8 +749,8 @@ STATE_CONDITION_SCHEMA = vol.All(
{ {
vol.Required(CONF_CONDITION): "state", vol.Required(CONF_CONDITION): "state",
vol.Required(CONF_ENTITY_ID): entity_id, vol.Required(CONF_ENTITY_ID): entity_id,
vol.Required("state"): str, vol.Required(CONF_STATE): str,
vol.Optional("for"): vol.All(time_period, positive_timedelta), vol.Optional(CONF_FOR): vol.All(time_period, positive_timedelta),
# To support use_trigger_value in automation # To support use_trigger_value in automation
# Deprecated 2016/04/25 # Deprecated 2016/04/25
vol.Optional("from"): str, vol.Optional("from"): str,
@ -823,6 +826,11 @@ OR_CONDITION_SCHEMA = vol.Schema(
} }
) )
DEVICE_CONDITION_SCHEMA = vol.Schema(
{vol.Required(CONF_CONDITION): "device", vol.Required(CONF_DOMAIN): str},
extra=vol.ALLOW_EXTRA,
)
CONDITION_SCHEMA: vol.Schema = vol.Any( CONDITION_SCHEMA: vol.Schema = vol.Any(
NUMERIC_STATE_CONDITION_SCHEMA, NUMERIC_STATE_CONDITION_SCHEMA,
STATE_CONDITION_SCHEMA, STATE_CONDITION_SCHEMA,
@ -832,6 +840,7 @@ CONDITION_SCHEMA: vol.Schema = vol.Any(
ZONE_CONDITION_SCHEMA, ZONE_CONDITION_SCHEMA,
AND_CONDITION_SCHEMA, AND_CONDITION_SCHEMA,
OR_CONDITION_SCHEMA, OR_CONDITION_SCHEMA,
DEVICE_CONDITION_SCHEMA,
) )
_SCRIPT_DELAY_SCHEMA = vol.Schema( _SCRIPT_DELAY_SCHEMA = vol.Schema(

View File

@ -338,7 +338,7 @@ class Script:
config_cache_key = frozenset((k, str(v)) for k, v in action.items()) config_cache_key = frozenset((k, str(v)) for k, v in action.items())
config = self._config_cache.get(config_cache_key) config = self._config_cache.get(config_cache_key)
if not config: if not config:
config = condition.async_from_config(action, False) config = await condition.async_from_config(self.hass, action, False)
self._config_cache[config_cache_key] = config self._config_cache[config_cache_key] = config
self.last_action = action.get(CONF_ALIAS, action[CONF_CONDITION]) self.last_action = action.get(CONF_ALIAS, action[CONF_CONDITION])

View File

@ -21,7 +21,7 @@ def entity_reg(hass):
return mock_registry(hass) return mock_registry(hass)
def _same_triggers(a, b): def _same_lists(a, b):
if len(a) != len(b): if len(a) != len(b):
return False return False
@ -31,6 +31,50 @@ def _same_triggers(a, b):
return True return True
async def test_websocket_get_conditions(hass, hass_ws_client, device_reg, entity_reg):
"""Test we get the expected conditions from a light through websocket."""
await async_setup_component(hass, "device_automation", {})
config_entry = MockConfigEntry(domain="test", data={})
config_entry.add_to_hass(hass)
device_entry = device_reg.async_get_or_create(
config_entry_id=config_entry.entry_id,
connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
)
entity_reg.async_get_or_create("light", "test", "5678", device_id=device_entry.id)
expected_conditions = [
{
"condition": "device",
"domain": "light",
"type": "is_off",
"device_id": device_entry.id,
"entity_id": "light.test_5678",
},
{
"condition": "device",
"domain": "light",
"type": "is_on",
"device_id": device_entry.id,
"entity_id": "light.test_5678",
},
]
client = await hass_ws_client(hass)
await client.send_json(
{
"id": 1,
"type": "device_automation/condition/list",
"device_id": device_entry.id,
}
)
msg = await client.receive_json()
assert msg["id"] == 1
assert msg["type"] == TYPE_RESULT
assert msg["success"]
conditions = msg["result"]
assert _same_lists(conditions, expected_conditions)
async def test_websocket_get_triggers(hass, hass_ws_client, device_reg, entity_reg): async def test_websocket_get_triggers(hass, hass_ws_client, device_reg, entity_reg):
"""Test we get the expected triggers from a light through websocket.""" """Test we get the expected triggers from a light through websocket."""
await async_setup_component(hass, "device_automation", {}) await async_setup_component(hass, "device_automation", {})
@ -71,5 +115,5 @@ async def test_websocket_get_triggers(hass, hass_ws_client, device_reg, entity_r
assert msg["id"] == 1 assert msg["id"] == 1
assert msg["type"] == TYPE_RESULT assert msg["type"] == TYPE_RESULT
assert msg["success"] assert msg["success"]
triggers = msg["result"]["triggers"] triggers = msg["result"]
assert _same_triggers(triggers, expected_triggers) assert _same_lists(triggers, expected_triggers)

View File

@ -6,11 +6,10 @@ from homeassistant.const import STATE_ON, STATE_OFF, CONF_PLATFORM
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
import homeassistant.components.automation as automation import homeassistant.components.automation as automation
from homeassistant.components.device_automation import ( from homeassistant.components.device_automation import (
async_get_device_automation_triggers, _async_get_device_automations as async_get_device_automations,
) )
from homeassistant.helpers import device_registry from homeassistant.helpers import device_registry
from tests.common import ( from tests.common import (
MockConfigEntry, MockConfigEntry,
async_mock_service, async_mock_service,
@ -37,7 +36,7 @@ def calls(hass):
return async_mock_service(hass, "test", "automation") return async_mock_service(hass, "test", "automation")
def _same_triggers(a, b): def _same_lists(a, b):
if len(a) != len(b): if len(a) != len(b):
return False return False
@ -47,6 +46,37 @@ def _same_triggers(a, b):
return True return True
async def test_get_conditions(hass, device_reg, entity_reg):
"""Test we get the expected conditions from a light."""
config_entry = MockConfigEntry(domain="test", data={})
config_entry.add_to_hass(hass)
device_entry = device_reg.async_get_or_create(
config_entry_id=config_entry.entry_id,
connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
)
entity_reg.async_get_or_create("light", "test", "5678", device_id=device_entry.id)
expected_conditions = [
{
"condition": "device",
"domain": "light",
"type": "is_off",
"device_id": device_entry.id,
"entity_id": "light.test_5678",
},
{
"condition": "device",
"domain": "light",
"type": "is_on",
"device_id": device_entry.id,
"entity_id": "light.test_5678",
},
]
conditions = await async_get_device_automations(
hass, "async_get_conditions", device_entry.id
)
assert _same_lists(conditions, expected_conditions)
async def test_get_triggers(hass, device_reg, entity_reg): async def test_get_triggers(hass, device_reg, entity_reg):
"""Test we get the expected triggers from a light.""" """Test we get the expected triggers from a light."""
config_entry = MockConfigEntry(domain="test", data={}) config_entry = MockConfigEntry(domain="test", data={})
@ -72,8 +102,10 @@ async def test_get_triggers(hass, device_reg, entity_reg):
"entity_id": "light.test_5678", "entity_id": "light.test_5678",
}, },
] ]
triggers = await async_get_device_automation_triggers(hass, device_entry.id) triggers = await async_get_device_automations(
assert _same_triggers(triggers, expected_triggers) hass, "async_get_triggers", device_entry.id
)
assert _same_lists(triggers, expected_triggers)
async def test_if_fires_on_state_change(hass, calls): async def test_if_fires_on_state_change(hass, calls):
@ -158,3 +190,76 @@ async def test_if_fires_on_state_change(hass, calls):
assert calls[1].data["some"] == "turn_on state - {} - off - on - None".format( assert calls[1].data["some"] == "turn_on state - {} - off - on - None".format(
dev1.entity_id dev1.entity_id
) )
async def test_if_state(hass, calls):
"""Test for turn_on and turn_off conditions."""
platform = getattr(hass.components, "test.light")
platform.init()
assert await async_setup_component(
hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}}
)
dev1, dev2, dev3 = platform.DEVICES
assert await async_setup_component(
hass,
automation.DOMAIN,
{
automation.DOMAIN: [
{
"trigger": {"platform": "event", "event_type": "test_event1"},
"condition": [
{
"condition": "device",
"domain": "light",
"entity_id": dev1.entity_id,
"type": "is_on",
}
],
"action": {
"service": "test.automation",
"data_template": {
"some": "is_on {{ trigger.%s }}"
% "}} - {{ trigger.".join(("platform", "event.event_type"))
},
},
},
{
"trigger": {"platform": "event", "event_type": "test_event2"},
"condition": [
{
"condition": "device",
"domain": "light",
"entity_id": dev1.entity_id,
"type": "is_off",
}
],
"action": {
"service": "test.automation",
"data_template": {
"some": "is_off {{ trigger.%s }}"
% "}} - {{ trigger.".join(("platform", "event.event_type"))
},
},
},
]
},
)
await hass.async_block_till_done()
assert hass.states.get(dev1.entity_id).state == STATE_ON
assert len(calls) == 0
hass.bus.async_fire("test_event1")
hass.bus.async_fire("test_event2")
await hass.async_block_till_done()
assert len(calls) == 1
assert calls[0].data["some"] == "is_on event - test_event1"
hass.states.async_set(dev1.entity_id, STATE_OFF)
hass.bus.async_fire("test_event1")
hass.bus.async_fire("test_event2")
await hass.async_block_till_done()
assert len(calls) == 2
assert calls[1].data["some"] == "is_off event - test_event2"

View File

@ -4,182 +4,175 @@ from unittest.mock import patch
from homeassistant.helpers import condition from homeassistant.helpers import condition
from homeassistant.util import dt from homeassistant.util import dt
from tests.common import get_test_home_assistant
async def test_and_condition(hass):
"""Test the 'and' condition."""
test = await condition.async_from_config(
hass,
{
"condition": "and",
"conditions": [
{
"condition": "state",
"entity_id": "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)
class TestConditionHelper: async def test_and_condition_with_template(hass):
"""Test condition helpers.""" """Test the 'and' condition."""
test = await condition.async_from_config(
hass,
{
"condition": "and",
"conditions": [
{
"condition": "template",
"value_template": '{{ states.sensor.temperature.state == "100" }}',
},
{
"condition": "numeric_state",
"entity_id": "sensor.temperature",
"below": 110,
},
],
},
)
def setup_method(self, method): hass.states.async_set("sensor.temperature", 120)
"""Set up things to be run when tests are started.""" assert not test(hass)
self.hass = get_test_home_assistant()
def teardown_method(self, method): hass.states.async_set("sensor.temperature", 105)
"""Stop everything that was started.""" assert not test(hass)
self.hass.stop()
def test_and_condition(self): hass.states.async_set("sensor.temperature", 100)
"""Test the 'and' condition.""" assert test(hass)
test = condition.from_config(
{
"condition": "and",
"conditions": [
{
"condition": "state",
"entity_id": "sensor.temperature",
"state": "100",
},
{
"condition": "numeric_state",
"entity_id": "sensor.temperature",
"below": 110,
},
],
}
)
self.hass.states.set("sensor.temperature", 120)
assert not test(self.hass)
self.hass.states.set("sensor.temperature", 105) async def test_or_condition(hass):
assert not test(self.hass) """Test the 'or' condition."""
test = await condition.async_from_config(
hass,
{
"condition": "or",
"conditions": [
{
"condition": "state",
"entity_id": "sensor.temperature",
"state": "100",
},
{
"condition": "numeric_state",
"entity_id": "sensor.temperature",
"below": 110,
},
],
},
)
self.hass.states.set("sensor.temperature", 100) hass.states.async_set("sensor.temperature", 120)
assert test(self.hass) assert not test(hass)
def test_and_condition_with_template(self): hass.states.async_set("sensor.temperature", 105)
"""Test the 'and' condition.""" assert test(hass)
test = condition.from_config(
{
"condition": "and",
"conditions": [
{
"condition": "template",
"value_template": '{{ states.sensor.temperature.state == "100" }}',
},
{
"condition": "numeric_state",
"entity_id": "sensor.temperature",
"below": 110,
},
],
}
)
self.hass.states.set("sensor.temperature", 120) hass.states.async_set("sensor.temperature", 100)
assert not test(self.hass) assert test(hass)
self.hass.states.set("sensor.temperature", 105)
assert not test(self.hass)
self.hass.states.set("sensor.temperature", 100) async def test_or_condition_with_template(hass):
assert test(self.hass) """Test the 'or' condition."""
test = await condition.async_from_config(
hass,
{
"condition": "or",
"conditions": [
{
"condition": "template",
"value_template": '{{ states.sensor.temperature.state == "100" }}',
},
{
"condition": "numeric_state",
"entity_id": "sensor.temperature",
"below": 110,
},
],
},
)
def test_or_condition(self): hass.states.async_set("sensor.temperature", 120)
"""Test the 'or' condition.""" assert not test(hass)
test = condition.from_config(
{
"condition": "or",
"conditions": [
{
"condition": "state",
"entity_id": "sensor.temperature",
"state": "100",
},
{
"condition": "numeric_state",
"entity_id": "sensor.temperature",
"below": 110,
},
],
}
)
self.hass.states.set("sensor.temperature", 120) hass.states.async_set("sensor.temperature", 105)
assert not test(self.hass) assert test(hass)
self.hass.states.set("sensor.temperature", 105) hass.states.async_set("sensor.temperature", 100)
assert test(self.hass) assert test(hass)
self.hass.states.set("sensor.temperature", 100)
assert test(self.hass)
def test_or_condition_with_template(self): async def test_time_window(hass):
"""Test the 'or' condition.""" """Test time condition windows."""
test = condition.from_config( sixam = dt.parse_time("06:00:00")
{ sixpm = dt.parse_time("18:00:00")
"condition": "or",
"conditions": [
{
"condition": "template",
"value_template": '{{ states.sensor.temperature.state == "100" }}',
},
{
"condition": "numeric_state",
"entity_id": "sensor.temperature",
"below": 110,
},
],
}
)
self.hass.states.set("sensor.temperature", 120) with patch(
assert not test(self.hass) "homeassistant.helpers.condition.dt_util.now",
return_value=dt.now().replace(hour=3),
):
assert not condition.time(after=sixam, before=sixpm)
assert condition.time(after=sixpm, before=sixam)
self.hass.states.set("sensor.temperature", 105) with patch(
assert test(self.hass) "homeassistant.helpers.condition.dt_util.now",
return_value=dt.now().replace(hour=9),
):
assert condition.time(after=sixam, before=sixpm)
assert not condition.time(after=sixpm, before=sixam)
self.hass.states.set("sensor.temperature", 100) with patch(
assert test(self.hass) "homeassistant.helpers.condition.dt_util.now",
return_value=dt.now().replace(hour=15),
):
assert condition.time(after=sixam, before=sixpm)
assert not condition.time(after=sixpm, before=sixam)
def test_time_window(self): with patch(
"""Test time condition windows.""" "homeassistant.helpers.condition.dt_util.now",
sixam = dt.parse_time("06:00:00") return_value=dt.now().replace(hour=21),
sixpm = dt.parse_time("18:00:00") ):
assert not condition.time(after=sixam, before=sixpm)
assert condition.time(after=sixpm, before=sixam)
with patch(
"homeassistant.helpers.condition.dt_util.now",
return_value=dt.now().replace(hour=3),
):
assert not condition.time(after=sixam, before=sixpm)
assert condition.time(after=sixpm, before=sixam)
with patch( async def test_if_numeric_state_not_raise_on_unavailable(hass):
"homeassistant.helpers.condition.dt_util.now", """Test numeric_state doesn't raise on unavailable/unknown state."""
return_value=dt.now().replace(hour=9), test = await condition.async_from_config(
): hass,
assert condition.time(after=sixam, before=sixpm) {"condition": "numeric_state", "entity_id": "sensor.temperature", "below": 42},
assert not condition.time(after=sixpm, before=sixam) )
with patch( with patch("homeassistant.helpers.condition._LOGGER.warning") as logwarn:
"homeassistant.helpers.condition.dt_util.now", hass.states.async_set("sensor.temperature", "unavailable")
return_value=dt.now().replace(hour=15), assert not test(hass)
): assert len(logwarn.mock_calls) == 0
assert condition.time(after=sixam, before=sixpm)
assert not condition.time(after=sixpm, before=sixam)
with patch( hass.states.async_set("sensor.temperature", "unknown")
"homeassistant.helpers.condition.dt_util.now", assert not test(hass)
return_value=dt.now().replace(hour=21), assert len(logwarn.mock_calls) == 0
):
assert not condition.time(after=sixam, before=sixpm)
assert condition.time(after=sixpm, before=sixam)
def test_if_numeric_state_not_raise_on_unavailable(self):
"""Test numeric_state doesn't raise on unavailable/unknown state."""
test = condition.from_config(
{
"condition": "numeric_state",
"entity_id": "sensor.temperature",
"below": 42,
}
)
with patch("homeassistant.helpers.condition._LOGGER.warning") as logwarn:
self.hass.states.set("sensor.temperature", "unavailable")
assert not test(self.hass)
assert len(logwarn.mock_calls) == 0
self.hass.states.set("sensor.temperature", "unknown")
assert not test(self.hass)
assert len(logwarn.mock_calls) == 0