Add support for for to binary_sensor, light and switch device triggers (#26658)

* Add support for `for` to binary_sensor, light and switch device triggers

* Add WS API device_automation/trigger/capabilities
This commit is contained in:
Erik Montnemery 2019-10-02 22:14:52 +02:00 committed by GitHub
parent d8c6b281b8
commit 65ce3b49c1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 495 additions and 42 deletions

View File

@ -5,6 +5,7 @@ from homeassistant.components.device_automation import (
TRIGGER_BASE_SCHEMA, TRIGGER_BASE_SCHEMA,
async_get_device_automation_platform, async_get_device_automation_platform,
) )
from homeassistant.const import CONF_DOMAIN
# mypy: allow-untyped-defs, no-check-untyped-defs # mypy: allow-untyped-defs, no-check-untyped-defs
@ -14,11 +15,15 @@ TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA)
async def async_validate_trigger_config(hass, config): async def async_validate_trigger_config(hass, config):
"""Validate config.""" """Validate config."""
platform = await async_get_device_automation_platform(hass, config, "trigger") platform = await async_get_device_automation_platform(
hass, config[CONF_DOMAIN], "trigger"
)
return platform.TRIGGER_SCHEMA(config) return platform.TRIGGER_SCHEMA(config)
async def async_attach_trigger(hass, config, action, automation_info): async def async_attach_trigger(hass, config, action, automation_info):
"""Listen for trigger.""" """Listen for trigger."""
platform = await async_get_device_automation_platform(hass, config, "trigger") platform = await async_get_device_automation_platform(
hass, config[CONF_DOMAIN], "trigger"
)
return await platform.async_attach_trigger(hass, config, action, automation_info) return await platform.async_attach_trigger(hass, config, action, automation_info)

View File

@ -7,7 +7,7 @@ from homeassistant.components.device_automation.const import (
CONF_TURNED_OFF, CONF_TURNED_OFF,
CONF_TURNED_ON, CONF_TURNED_ON,
) )
from homeassistant.const import ATTR_DEVICE_CLASS, CONF_ENTITY_ID, CONF_TYPE from homeassistant.const import ATTR_DEVICE_CLASS, CONF_ENTITY_ID, CONF_FOR, CONF_TYPE
from homeassistant.helpers.entity_registry import async_entries_for_device from homeassistant.helpers.entity_registry import async_entries_for_device
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
@ -175,13 +175,13 @@ TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend(
{ {
vol.Required(CONF_ENTITY_ID): cv.entity_id, vol.Required(CONF_ENTITY_ID): cv.entity_id,
vol.Required(CONF_TYPE): vol.In(TURNED_OFF + TURNED_ON), vol.Required(CONF_TYPE): vol.In(TURNED_OFF + TURNED_ON),
vol.Optional(CONF_FOR): cv.positive_time_period_dict,
} }
) )
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."""
config = TRIGGER_SCHEMA(config)
trigger_type = config[CONF_TYPE] trigger_type = config[CONF_TYPE]
if trigger_type in TURNED_ON: if trigger_type in TURNED_ON:
from_state = "off" from_state = "off"
@ -195,6 +195,8 @@ async def async_attach_trigger(hass, config, action, automation_info):
state_automation.CONF_FROM: from_state, state_automation.CONF_FROM: from_state,
state_automation.CONF_TO: to_state, state_automation.CONF_TO: to_state,
} }
if "for" in config:
state_config["for"] = config["for"]
return await state_automation.async_attach_trigger( return await state_automation.async_attach_trigger(
hass, state_config, action, automation_info, platform_type="device" hass, state_config, action, automation_info, platform_type="device"
@ -236,3 +238,12 @@ async def async_get_triggers(hass, device_id):
) )
return triggers return triggers
async def async_get_trigger_capabilities(hass, trigger):
"""List trigger capabilities."""
return {
"extra_fields": vol.Schema(
{vol.Optional(CONF_FOR): cv.positive_time_period_dict}
)
}

View File

@ -206,8 +206,6 @@ def _get_deconz_event_from_device_id(hass, device_id):
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."""
config = TRIGGER_SCHEMA(config)
device_registry = await hass.helpers.device_registry.async_get_registry() device_registry = await hass.helpers.device_registry.async_get_registry()
device = device_registry.async_get(config[CONF_DEVICE_ID]) device = device_registry.async_get(config[CONF_DEVICE_ID])

View File

@ -4,9 +4,11 @@ import logging
from typing import Any, List, MutableMapping from typing import Any, List, MutableMapping
import voluptuous as vol import voluptuous as vol
import voluptuous_serialize
from homeassistant.const import CONF_PLATFORM, CONF_DOMAIN, CONF_DEVICE_ID from homeassistant.const import CONF_PLATFORM, CONF_DOMAIN, CONF_DEVICE_ID
from homeassistant.components import websocket_api from homeassistant.components import websocket_api
from homeassistant.helpers import 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.loader import async_get_integration, IntegrationNotFound from homeassistant.loader import async_get_integration, IntegrationNotFound
@ -29,9 +31,18 @@ TRIGGER_BASE_SCHEMA = vol.Schema(
) )
TYPES = { TYPES = {
"trigger": ("device_trigger", "async_get_triggers"), # platform name, get automations function, get capabilities function
"condition": ("device_condition", "async_get_conditions"), "trigger": (
"action": ("device_action", "async_get_actions"), "device_trigger",
"async_get_triggers",
"async_get_trigger_capabilities",
),
"condition": (
"device_condition",
"async_get_conditions",
"async_get_condition_capabilities",
),
"action": ("device_action", "async_get_actions", "async_get_action_capabilities"),
} }
@ -46,25 +57,26 @@ async def async_setup(hass, config):
hass.components.websocket_api.async_register_command( hass.components.websocket_api.async_register_command(
websocket_device_automation_list_triggers websocket_device_automation_list_triggers
) )
hass.components.websocket_api.async_register_command(
websocket_device_automation_get_trigger_capabilities
)
return True return True
async def async_get_device_automation_platform(hass, config, automation_type): async def async_get_device_automation_platform(hass, domain, automation_type):
"""Load device automation platform for integration. """Load device automation platform for integration.
Throws InvalidDeviceAutomationConfig if the integration is not found or does not support device automation. Throws InvalidDeviceAutomationConfig if the integration is not found or does not support device automation.
""" """
platform_name, _ = TYPES[automation_type] platform_name = TYPES[automation_type][0]
try: try:
integration = await async_get_integration(hass, config[CONF_DOMAIN]) integration = await async_get_integration(hass, domain)
platform = integration.get_platform(platform_name) platform = integration.get_platform(platform_name)
except IntegrationNotFound: except IntegrationNotFound:
raise InvalidDeviceAutomationConfig( raise InvalidDeviceAutomationConfig(f"Integration '{domain}' not found")
f"Integration '{config[CONF_DOMAIN]}' not found"
)
except ImportError: except ImportError:
raise InvalidDeviceAutomationConfig( raise InvalidDeviceAutomationConfig(
f"Integration '{config[CONF_DOMAIN]}' does not support device automation {automation_type}s" f"Integration '{domain}' does not support device automation {automation_type}s"
) )
return platform return platform
@ -74,20 +86,14 @@ async def _async_get_device_automations_from_domain(
hass, domain, automation_type, device_id hass, domain, automation_type, device_id
): ):
"""List device automations.""" """List device automations."""
integration = None
try: try:
integration = await async_get_integration(hass, domain) platform = await async_get_device_automation_platform(
except IntegrationNotFound: hass, domain, automation_type
_LOGGER.warning("Integration %s not found", domain) )
except InvalidDeviceAutomationConfig:
return None return None
platform_name, function_name = TYPES[automation_type] function_name = TYPES[automation_type][1]
try:
platform = integration.get_platform(platform_name)
except ImportError:
# The domain does not have device automations
return None
return await getattr(platform, function_name)(hass, device_id) return await getattr(platform, function_name)(hass, device_id)
@ -125,6 +131,35 @@ async def _async_get_device_automations(hass, automation_type, device_id):
return automations return automations
async def _async_get_device_automation_capabilities(hass, automation_type, automation):
"""List device automations."""
try:
platform = await async_get_device_automation_platform(
hass, automation[CONF_DOMAIN], automation_type
)
except InvalidDeviceAutomationConfig:
return {}
function_name = TYPES[automation_type][2]
if not hasattr(platform, function_name):
# The device automation has no capabilities
return {}
capabilities = await getattr(platform, function_name)(hass, automation)
capabilities = capabilities.copy()
extra_fields = capabilities.get("extra_fields")
if extra_fields is None:
capabilities["extra_fields"] = []
else:
capabilities["extra_fields"] = voluptuous_serialize.convert(
extra_fields, custom_serializer=cv.custom_serializer
)
return capabilities
@websocket_api.async_response @websocket_api.async_response
@websocket_api.websocket_command( @websocket_api.websocket_command(
{ {
@ -165,3 +200,19 @@ async def websocket_device_automation_list_triggers(hass, connection, msg):
device_id = msg["device_id"] device_id = msg["device_id"]
triggers = await _async_get_device_automations(hass, "trigger", device_id) triggers = await _async_get_device_automations(hass, "trigger", device_id)
connection.send_result(msg["id"], triggers) connection.send_result(msg["id"], triggers)
@websocket_api.async_response
@websocket_api.websocket_command(
{
vol.Required("type"): "device_automation/trigger/capabilities",
vol.Required("trigger"): dict,
}
)
async def websocket_device_automation_get_trigger_capabilities(hass, connection, msg):
"""Handle request for device trigger capabilities."""
trigger = msg["trigger"]
capabilities = await _async_get_device_automation_capabilities(
hass, "trigger", trigger
)
connection.send_result(msg["id"], capabilities)

View File

@ -13,7 +13,13 @@ from homeassistant.components.device_automation.const import (
CONF_TURNED_OFF, CONF_TURNED_OFF,
CONF_TURNED_ON, CONF_TURNED_ON,
) )
from homeassistant.const import CONF_CONDITION, CONF_ENTITY_ID, CONF_PLATFORM, CONF_TYPE from homeassistant.const import (
CONF_CONDITION,
CONF_ENTITY_ID,
CONF_FOR,
CONF_PLATFORM,
CONF_TYPE,
)
from homeassistant.helpers.entity_registry import async_entries_for_device from homeassistant.helpers.entity_registry import async_entries_for_device
from homeassistant.helpers import condition, config_validation as cv, service from homeassistant.helpers import condition, config_validation as cv, service
from homeassistant.helpers.typing import ConfigType, TemplateVarsType from homeassistant.helpers.typing import ConfigType, TemplateVarsType
@ -81,6 +87,7 @@ TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend(
{ {
vol.Required(CONF_ENTITY_ID): cv.entity_id, vol.Required(CONF_ENTITY_ID): cv.entity_id,
vol.Required(CONF_TYPE): vol.In([CONF_TURNED_OFF, CONF_TURNED_ON]), vol.Required(CONF_TYPE): vol.In([CONF_TURNED_OFF, CONF_TURNED_ON]),
vol.Optional(CONF_FOR): cv.positive_time_period_dict,
} }
) )
@ -93,7 +100,6 @@ async def async_call_action_from_config(
domain: str, domain: str,
) -> None: ) -> None:
"""Change state based on configuration.""" """Change state based on configuration."""
config = ACTION_SCHEMA(config)
action_type = config[CONF_TYPE] action_type = config[CONF_TYPE]
if action_type == CONF_TURN_ON: if action_type == CONF_TURN_ON:
action = "turn_on" action = "turn_on"
@ -149,6 +155,8 @@ async def async_attach_trigger(
state.CONF_FROM: from_state, state.CONF_FROM: from_state,
state.CONF_TO: to_state, state.CONF_TO: to_state,
} }
if "for" in config:
state_config["for"] = config["for"]
return await state.async_attach_trigger( return await state.async_attach_trigger(
hass, state_config, action, automation_info, platform_type="device" hass, state_config, action, automation_info, platform_type="device"
@ -203,3 +211,12 @@ async def async_get_triggers(
) -> List[dict]: ) -> List[dict]:
"""List device triggers.""" """List device triggers."""
return await _async_get_automations(hass, device_id, ENTITY_TRIGGERS, domain) return await _async_get_automations(hass, device_id, ENTITY_TRIGGERS, domain)
async def async_get_trigger_capabilities(hass: HomeAssistant, trigger: dict) -> dict:
"""List trigger capabilities."""
return {
"extra_fields": vol.Schema(
{vol.Optional(CONF_FOR): cv.positive_time_period_dict}
)
}

View File

@ -19,7 +19,6 @@ async def async_call_action_from_config(
context: Context, context: Context,
) -> None: ) -> None:
"""Change state based on configuration.""" """Change state based on configuration."""
config = ACTION_SCHEMA(config)
await toggle_entity.async_call_action_from_config( await toggle_entity.async_call_action_from_config(
hass, config, variables, context, DOMAIN hass, config, variables, context, DOMAIN
) )

View File

@ -22,7 +22,6 @@ async def async_attach_trigger(
automation_info: dict, automation_info: dict,
) -> CALLBACK_TYPE: ) -> CALLBACK_TYPE:
"""Listen for state changes based on configuration.""" """Listen for state changes based on configuration."""
config = TRIGGER_SCHEMA(config)
return await toggle_entity.async_attach_trigger( return await toggle_entity.async_attach_trigger(
hass, config, action, automation_info hass, config, action, automation_info
) )
@ -31,3 +30,8 @@ async def async_attach_trigger(
async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]: async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]:
"""List device triggers.""" """List device triggers."""
return await toggle_entity.async_get_triggers(hass, device_id, DOMAIN) return await toggle_entity.async_get_triggers(hass, device_id, DOMAIN)
async def async_get_trigger_capabilities(hass: HomeAssistant, trigger: dict) -> dict:
"""List trigger capabilities."""
return await toggle_entity.async_get_trigger_capabilities(hass, trigger)

View File

@ -19,7 +19,6 @@ async def async_call_action_from_config(
context: Context, context: Context,
) -> None: ) -> None:
"""Change state based on configuration.""" """Change state based on configuration."""
config = ACTION_SCHEMA(config)
await toggle_entity.async_call_action_from_config( await toggle_entity.async_call_action_from_config(
hass, config, variables, context, DOMAIN hass, config, variables, context, DOMAIN
) )

View File

@ -22,7 +22,6 @@ async def async_attach_trigger(
automation_info: dict, automation_info: dict,
) -> CALLBACK_TYPE: ) -> CALLBACK_TYPE:
"""Listen for state changes based on configuration.""" """Listen for state changes based on configuration."""
config = TRIGGER_SCHEMA(config)
return await toggle_entity.async_attach_trigger( return await toggle_entity.async_attach_trigger(
hass, config, action, automation_info hass, config, action, automation_info
) )
@ -31,3 +30,8 @@ async def async_attach_trigger(
async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]: async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]:
"""List device triggers.""" """List device triggers."""
return await toggle_entity.async_get_triggers(hass, device_id, DOMAIN) return await toggle_entity.async_get_triggers(hass, device_id, DOMAIN)
async def async_get_trigger_capabilities(hass: HomeAssistant, trigger: dict) -> dict:
"""List trigger capabilities."""
return await toggle_entity.async_get_trigger_capabilities(hass, trigger)

View File

@ -49,7 +49,6 @@ async def async_call_action_from_config(
context: Context, context: Context,
) -> None: ) -> None:
"""Perform an action based on configuration.""" """Perform an action based on configuration."""
config = ACTION_SCHEMA(config)
await ZHA_ACTION_TYPES[DEVICE_ACTION_TYPES[config[CONF_TYPE]]]( await ZHA_ACTION_TYPES[DEVICE_ACTION_TYPES[config[CONF_TYPE]]](
hass, config, variables, context hass, config, variables, context
) )

View File

@ -23,7 +23,6 @@ TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend(
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."""
config = TRIGGER_SCHEMA(config)
trigger = (config[CONF_TYPE], config[CONF_SUBTYPE]) trigger = (config[CONF_TYPE], config[CONF_SUBTYPE])
zha_device = await async_get_zha_device(hass, config[CONF_DEVICE_ID]) zha_device = await async_get_zha_device(hass, config[CONF_DEVICE_ID])

View File

@ -15,8 +15,9 @@ from typing import Any, Union, TypeVar, Callable, List, Dict, Optional
from urllib.parse import urlparse from urllib.parse import urlparse
from uuid import UUID from uuid import UUID
import voluptuous as vol
from pkg_resources import parse_version from pkg_resources import parse_version
import voluptuous as vol
import voluptuous_serialize
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
from homeassistant.const import ( from homeassistant.const import (
@ -374,6 +375,9 @@ def positive_timedelta(value: timedelta) -> timedelta:
return value return value
positive_time_period_dict = vol.All(time_period_dict, positive_timedelta)
def remove_falsy(value: List[T]) -> List[T]: def remove_falsy(value: List[T]) -> List[T]:
"""Remove falsy values from a list.""" """Remove falsy values from a list."""
return [v for v in value if v] return [v for v in value if v]
@ -690,6 +694,14 @@ def key_dependency(key, dependency):
return validator return validator
def custom_serializer(schema):
"""Serialize additional types for voluptuous_serialize."""
if schema is positive_time_period_dict:
return {"type": "positive_time_period_dict"}
return voluptuous_serialize.UNSUPPORTED
# Schemas # Schemas
PLATFORM_SCHEMA = vol.Schema( PLATFORM_SCHEMA = vol.Schema(
{ {

View File

@ -10,7 +10,12 @@ import voluptuous as vol
import homeassistant.components.device_automation as device_automation import homeassistant.components.device_automation as device_automation
from homeassistant.core import HomeAssistant, Context, callback, CALLBACK_TYPE from homeassistant.core import HomeAssistant, Context, callback, CALLBACK_TYPE
from homeassistant.const import CONF_CONDITION, CONF_DEVICE_ID, CONF_TIMEOUT from homeassistant.const import (
CONF_CONDITION,
CONF_DEVICE_ID,
CONF_DOMAIN,
CONF_TIMEOUT,
)
from homeassistant import exceptions from homeassistant import exceptions
from homeassistant.helpers import ( from homeassistant.helpers import (
service, service,
@ -89,7 +94,7 @@ async def async_validate_action_config(
if action_type == ACTION_DEVICE_AUTOMATION: if action_type == ACTION_DEVICE_AUTOMATION:
platform = await device_automation.async_get_device_automation_platform( platform = await device_automation.async_get_device_automation_platform(
hass, config, "action" hass, config[CONF_DOMAIN], "action"
) )
config = platform.ACTION_SCHEMA(config) config = platform.ACTION_SCHEMA(config)
@ -346,7 +351,7 @@ class Script:
self.last_action = action.get(CONF_ALIAS, "device automation") self.last_action = action.get(CONF_ALIAS, "device automation")
self._log("Executing step %s" % self.last_action) self._log("Executing step %s" % self.last_action)
platform = await device_automation.async_get_device_automation_platform( platform = await device_automation.async_get_device_automation_platform(
self.hass, action, "action" self.hass, action[CONF_DOMAIN], "action"
) )
await platform.async_call_action_from_config( await platform.async_call_action_from_config(
self.hass, action, variables, context self.hass, action, variables, context

View File

@ -22,7 +22,7 @@ pyyaml==5.1.2
requests==2.22.0 requests==2.22.0
ruamel.yaml==0.15.100 ruamel.yaml==0.15.100
sqlalchemy==1.3.8 sqlalchemy==1.3.8
voluptuous-serialize==2.2.0 voluptuous-serialize==2.3.0
voluptuous==0.11.7 voluptuous==0.11.7
zeroconf==0.23.0 zeroconf==0.23.0

View File

@ -17,7 +17,7 @@ pyyaml==5.1.2
requests==2.22.0 requests==2.22.0
ruamel.yaml==0.15.100 ruamel.yaml==0.15.100
voluptuous==0.11.7 voluptuous==0.11.7
voluptuous-serialize==2.2.0 voluptuous-serialize==2.3.0
# homeassistant.components.nuimo_controller # homeassistant.components.nuimo_controller
--only-binary=all nuimo==0.1.0 --only-binary=all nuimo==0.1.0

View File

@ -50,7 +50,7 @@ REQUIRES = [
"requests==2.22.0", "requests==2.22.0",
"ruamel.yaml==0.15.100", "ruamel.yaml==0.15.100",
"voluptuous==0.11.7", "voluptuous==0.11.7",
"voluptuous-serialize==2.2.0", "voluptuous-serialize==2.3.0",
] ]
MIN_PY_VERSION = ".".join(map(str, hass_const.REQUIRED_PYTHON_VER)) MIN_PY_VERSION = ".".join(map(str, hass_const.REQUIRED_PYTHON_VER))

View File

@ -56,6 +56,7 @@ from homeassistant.util.unit_system import METRIC_SYSTEM
from homeassistant.util.async_ import run_callback_threadsafe from homeassistant.util.async_ import run_callback_threadsafe
from homeassistant.components.device_automation import ( # noqa from homeassistant.components.device_automation import ( # noqa
_async_get_device_automations as async_get_device_automations, _async_get_device_automations as async_get_device_automations,
_async_get_device_automation_capabilities as async_get_device_automation_capabilities,
) )
_TEST_INSTANCE_PORT = SERVER_PORT _TEST_INSTANCE_PORT = SERVER_PORT

View File

@ -1,4 +1,5 @@
"""The test for binary_sensor device automation.""" """The test for binary_sensor device automation."""
from datetime import timedelta
import pytest import pytest
from homeassistant.components.binary_sensor import DOMAIN, DEVICE_CLASSES from homeassistant.components.binary_sensor import DOMAIN, DEVICE_CLASSES
@ -7,13 +8,16 @@ 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.helpers import device_registry from homeassistant.helpers import device_registry
import homeassistant.util.dt as dt_util
from tests.common import ( from tests.common import (
MockConfigEntry, MockConfigEntry,
async_fire_time_changed,
async_mock_service, async_mock_service,
mock_device_registry, mock_device_registry,
mock_registry, mock_registry,
async_get_device_automations, async_get_device_automations,
async_get_device_automation_capabilities,
) )
@ -71,6 +75,28 @@ async def test_get_triggers(hass, device_reg, entity_reg):
assert triggers == expected_triggers assert triggers == expected_triggers
async def test_get_trigger_capabilities(hass, device_reg, entity_reg):
"""Test we get the expected capabilities from a binary_sensor trigger."""
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(DOMAIN, "test", "5678", device_id=device_entry.id)
expected_capabilities = {
"extra_fields": [
{"name": "for", "optional": True, "type": "positive_time_period_dict"}
]
}
triggers = await async_get_device_automations(hass, "trigger", device_entry.id)
for trigger in triggers:
capabilities = await async_get_device_automation_capabilities(
hass, "trigger", trigger
)
assert capabilities == expected_capabilities
async def test_if_fires_on_state_change(hass, calls): async def test_if_fires_on_state_change(hass, calls):
"""Test for on and off triggers firing.""" """Test for on and off triggers firing."""
platform = getattr(hass.components, f"test.{DOMAIN}") platform = getattr(hass.components, f"test.{DOMAIN}")
@ -152,3 +178,61 @@ async def test_if_fires_on_state_change(hass, calls):
assert calls[1].data["some"] == "bat_low device - {} - off - on - None".format( assert calls[1].data["some"] == "bat_low device - {} - off - on - None".format(
sensor1.entity_id sensor1.entity_id
) )
async def test_if_fires_on_state_change_with_for(hass, calls):
"""Test for triggers firing with delay."""
platform = getattr(hass.components, f"test.{DOMAIN}")
platform.init()
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
sensor1 = platform.ENTITIES["battery"]
assert await async_setup_component(
hass,
automation.DOMAIN,
{
automation.DOMAIN: [
{
"trigger": {
"platform": "device",
"domain": DOMAIN,
"device_id": "",
"entity_id": sensor1.entity_id,
"type": "turned_off",
"for": {"seconds": 5},
},
"action": {
"service": "test.automation",
"data_template": {
"some": "turn_off {{ trigger.%s }}"
% "}} - {{ trigger.".join(
(
"platform",
"entity_id",
"from_state.state",
"to_state.state",
"for",
)
)
},
},
}
]
},
)
await hass.async_block_till_done()
assert hass.states.get(sensor1.entity_id).state == STATE_ON
assert len(calls) == 0
hass.states.async_set(sensor1.entity_id, STATE_OFF)
await hass.async_block_till_done()
assert len(calls) == 0
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10))
await hass.async_block_till_done()
assert len(calls) == 1
await hass.async_block_till_done()
assert calls[0].data["some"] == "turn_off device - {} - on - off - 0:00:05".format(
sensor1.entity_id
)

View File

@ -164,6 +164,103 @@ async def test_websocket_get_triggers(hass, hass_ws_client, device_reg, entity_r
assert _same_lists(triggers, expected_triggers) assert _same_lists(triggers, expected_triggers)
async def test_websocket_get_trigger_capabilities(
hass, hass_ws_client, device_reg, entity_reg
):
"""Test we get the expected trigger capabilities for 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_capabilities = {
"extra_fields": [
{"name": "for", "optional": True, "type": "positive_time_period_dict"}
]
}
client = await hass_ws_client(hass)
await client.send_json(
{
"id": 1,
"type": "device_automation/trigger/list",
"device_id": device_entry.id,
}
)
msg = await client.receive_json()
assert msg["id"] == 1
assert msg["type"] == TYPE_RESULT
assert msg["success"]
triggers = msg["result"]
id = 2
for trigger in triggers:
await client.send_json(
{
"id": id,
"type": "device_automation/trigger/capabilities",
"trigger": trigger,
}
)
msg = await client.receive_json()
assert msg["id"] == id
assert msg["type"] == TYPE_RESULT
assert msg["success"]
capabilities = msg["result"]
assert capabilities == expected_capabilities
id = id + 1
async def test_websocket_get_bad_trigger_capabilities(
hass, hass_ws_client, device_reg, entity_reg
):
"""Test we get no trigger capabilities for a non existing domain."""
await async_setup_component(hass, "device_automation", {})
expected_capabilities = {}
client = await hass_ws_client(hass)
await client.send_json(
{
"id": 1,
"type": "device_automation/trigger/capabilities",
"trigger": {"domain": "beer"},
}
)
msg = await client.receive_json()
assert msg["id"] == 1
assert msg["type"] == TYPE_RESULT
assert msg["success"]
capabilities = msg["result"]
assert capabilities == expected_capabilities
async def test_websocket_get_no_trigger_capabilities(
hass, hass_ws_client, device_reg, entity_reg
):
"""Test we get no trigger capabilities for a domain with no device trigger capabilities."""
await async_setup_component(hass, "device_automation", {})
expected_capabilities = {}
client = await hass_ws_client(hass)
await client.send_json(
{
"id": 1,
"type": "device_automation/trigger/capabilities",
"trigger": {"domain": "deconz"},
}
)
msg = await client.receive_json()
assert msg["id"] == 1
assert msg["type"] == TYPE_RESULT
assert msg["success"]
capabilities = msg["result"]
assert capabilities == expected_capabilities
async def test_automation_with_non_existing_integration(hass, caplog): async def test_automation_with_non_existing_integration(hass, caplog):
"""Test device automation with non existing integration.""" """Test device automation with non existing integration."""
assert await async_setup_component( assert await async_setup_component(

View File

@ -1,4 +1,5 @@
"""The test for light device automation.""" """The test for light device automation."""
from datetime import timedelta
import pytest import pytest
from homeassistant.components.light import DOMAIN from homeassistant.components.light import DOMAIN
@ -6,13 +7,16 @@ 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.helpers import device_registry from homeassistant.helpers import device_registry
import homeassistant.util.dt as dt_util
from tests.common import ( from tests.common import (
MockConfigEntry, MockConfigEntry,
async_fire_time_changed,
async_mock_service, async_mock_service,
mock_device_registry, mock_device_registry,
mock_registry, mock_registry,
async_get_device_automations, async_get_device_automations,
async_get_device_automation_capabilities,
) )
@ -63,6 +67,28 @@ async def test_get_triggers(hass, device_reg, entity_reg):
assert triggers == expected_triggers assert triggers == expected_triggers
async def test_get_trigger_capabilities(hass, device_reg, entity_reg):
"""Test we get the expected capabilities from a light trigger."""
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(DOMAIN, "test", "5678", device_id=device_entry.id)
expected_capabilities = {
"extra_fields": [
{"name": "for", "optional": True, "type": "positive_time_period_dict"}
]
}
triggers = await async_get_device_automations(hass, "trigger", device_entry.id)
for trigger in triggers:
capabilities = await async_get_device_automation_capabilities(
hass, "trigger", trigger
)
assert capabilities == expected_capabilities
async def test_if_fires_on_state_change(hass, calls): async def test_if_fires_on_state_change(hass, calls):
"""Test for turn_on and turn_off triggers firing.""" """Test for turn_on and turn_off triggers firing."""
platform = getattr(hass.components, f"test.{DOMAIN}") platform = getattr(hass.components, f"test.{DOMAIN}")
@ -145,3 +171,61 @@ async def test_if_fires_on_state_change(hass, calls):
assert calls[1].data["some"] == "turn_on device - {} - off - on - None".format( assert calls[1].data["some"] == "turn_on device - {} - off - on - None".format(
ent1.entity_id ent1.entity_id
) )
async def test_if_fires_on_state_change_with_for(hass, calls):
"""Test for triggers firing with delay."""
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": "device",
"domain": DOMAIN,
"device_id": "",
"entity_id": ent1.entity_id,
"type": "turned_off",
"for": {"seconds": 5},
},
"action": {
"service": "test.automation",
"data_template": {
"some": "turn_off {{ trigger.%s }}"
% "}} - {{ trigger.".join(
(
"platform",
"entity_id",
"from_state.state",
"to_state.state",
"for",
)
)
},
},
}
]
},
)
await hass.async_block_till_done()
assert hass.states.get(ent1.entity_id).state == STATE_ON
assert len(calls) == 0
hass.states.async_set(ent1.entity_id, STATE_OFF)
await hass.async_block_till_done()
assert len(calls) == 0
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10))
await hass.async_block_till_done()
assert len(calls) == 1
await hass.async_block_till_done()
assert calls[0].data["some"] == "turn_off device - {} - on - off - 0:00:05".format(
ent1.entity_id
)

View File

@ -1,4 +1,5 @@
"""The test for switch device automation.""" """The test for switch device automation."""
from datetime import timedelta
import pytest import pytest
from homeassistant.components.switch import DOMAIN from homeassistant.components.switch import DOMAIN
@ -6,13 +7,16 @@ 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.helpers import device_registry from homeassistant.helpers import device_registry
import homeassistant.util.dt as dt_util
from tests.common import ( from tests.common import (
MockConfigEntry, MockConfigEntry,
async_fire_time_changed,
async_mock_service, async_mock_service,
mock_device_registry, mock_device_registry,
mock_registry, mock_registry,
async_get_device_automations, async_get_device_automations,
async_get_device_automation_capabilities,
) )
@ -63,6 +67,28 @@ async def test_get_triggers(hass, device_reg, entity_reg):
assert triggers == expected_triggers assert triggers == expected_triggers
async def test_get_trigger_capabilities(hass, device_reg, entity_reg):
"""Test we get the expected capabilities from a switch trigger."""
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(DOMAIN, "test", "5678", device_id=device_entry.id)
expected_capabilities = {
"extra_fields": [
{"name": "for", "optional": True, "type": "positive_time_period_dict"}
]
}
triggers = await async_get_device_automations(hass, "trigger", device_entry.id)
for trigger in triggers:
capabilities = await async_get_device_automation_capabilities(
hass, "trigger", trigger
)
assert capabilities == expected_capabilities
async def test_if_fires_on_state_change(hass, calls): async def test_if_fires_on_state_change(hass, calls):
"""Test for turn_on and turn_off triggers firing.""" """Test for turn_on and turn_off triggers firing."""
platform = getattr(hass.components, f"test.{DOMAIN}") platform = getattr(hass.components, f"test.{DOMAIN}")
@ -145,3 +171,61 @@ async def test_if_fires_on_state_change(hass, calls):
assert calls[1].data["some"] == "turn_on device - {} - off - on - None".format( assert calls[1].data["some"] == "turn_on device - {} - off - on - None".format(
ent1.entity_id ent1.entity_id
) )
async def test_if_fires_on_state_change_with_for(hass, calls):
"""Test for triggers firing with delay."""
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": "device",
"domain": DOMAIN,
"device_id": "",
"entity_id": ent1.entity_id,
"type": "turned_off",
"for": {"seconds": 5},
},
"action": {
"service": "test.automation",
"data_template": {
"some": "turn_off {{ trigger.%s }}"
% "}} - {{ trigger.".join(
(
"platform",
"entity_id",
"from_state.state",
"to_state.state",
"for",
)
)
},
},
}
]
},
)
await hass.async_block_till_done()
assert hass.states.get(ent1.entity_id).state == STATE_ON
assert len(calls) == 0
hass.states.async_set(ent1.entity_id, STATE_OFF)
await hass.async_block_till_done()
assert len(calls) == 0
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10))
await hass.async_block_till_done()
assert len(calls) == 1
await hass.async_block_till_done()
assert calls[0].data["some"] == "turn_off device - {} - on - off - 0:00:05".format(
ent1.entity_id
)