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,
async_get_device_automation_platform,
)
from homeassistant.const import CONF_DOMAIN
# 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):
"""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)
async def async_attach_trigger(hass, config, action, automation_info):
"""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)

View File

@ -7,7 +7,7 @@ from homeassistant.components.device_automation.const import (
CONF_TURNED_OFF,
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 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_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):
"""Listen for state changes based on configuration."""
config = TRIGGER_SCHEMA(config)
trigger_type = config[CONF_TYPE]
if trigger_type in TURNED_ON:
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_TO: to_state,
}
if "for" in config:
state_config["for"] = config["for"]
return await state_automation.async_attach_trigger(
hass, state_config, action, automation_info, platform_type="device"
@ -236,3 +238,12 @@ async def async_get_triggers(hass, device_id):
)
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):
"""Listen for state changes based on configuration."""
config = TRIGGER_SCHEMA(config)
device_registry = await hass.helpers.device_registry.async_get_registry()
device = device_registry.async_get(config[CONF_DEVICE_ID])

View File

@ -4,9 +4,11 @@ import logging
from typing import Any, List, MutableMapping
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.helpers import config_validation as cv
from homeassistant.helpers.entity_registry import async_entries_for_device
from homeassistant.loader import async_get_integration, IntegrationNotFound
@ -29,9 +31,18 @@ TRIGGER_BASE_SCHEMA = vol.Schema(
)
TYPES = {
"trigger": ("device_trigger", "async_get_triggers"),
"condition": ("device_condition", "async_get_conditions"),
"action": ("device_action", "async_get_actions"),
# platform name, get automations function, get capabilities function
"trigger": (
"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(
websocket_device_automation_list_triggers
)
hass.components.websocket_api.async_register_command(
websocket_device_automation_get_trigger_capabilities
)
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.
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:
integration = await async_get_integration(hass, config[CONF_DOMAIN])
integration = await async_get_integration(hass, domain)
platform = integration.get_platform(platform_name)
except IntegrationNotFound:
raise InvalidDeviceAutomationConfig(
f"Integration '{config[CONF_DOMAIN]}' not found"
)
raise InvalidDeviceAutomationConfig(f"Integration '{domain}' not found")
except ImportError:
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
@ -74,20 +86,14 @@ async def _async_get_device_automations_from_domain(
hass, domain, automation_type, device_id
):
"""List device automations."""
integration = None
try:
integration = await async_get_integration(hass, domain)
except IntegrationNotFound:
_LOGGER.warning("Integration %s not found", domain)
platform = await async_get_device_automation_platform(
hass, domain, automation_type
)
except InvalidDeviceAutomationConfig:
return None
platform_name, function_name = TYPES[automation_type]
try:
platform = integration.get_platform(platform_name)
except ImportError:
# The domain does not have device automations
return None
function_name = TYPES[automation_type][1]
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
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.websocket_command(
{
@ -165,3 +200,19 @@ async def websocket_device_automation_list_triggers(hass, connection, msg):
device_id = msg["device_id"]
triggers = await _async_get_device_automations(hass, "trigger", device_id)
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_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 import condition, config_validation as cv, service
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_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,
) -> None:
"""Change state based on configuration."""
config = ACTION_SCHEMA(config)
action_type = config[CONF_TYPE]
if action_type == CONF_TURN_ON:
action = "turn_on"
@ -149,6 +155,8 @@ async def async_attach_trigger(
state.CONF_FROM: from_state,
state.CONF_TO: to_state,
}
if "for" in config:
state_config["for"] = config["for"]
return await state.async_attach_trigger(
hass, state_config, action, automation_info, platform_type="device"
@ -203,3 +211,12 @@ async def async_get_triggers(
) -> List[dict]:
"""List device triggers."""
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,
) -> None:
"""Change state based on configuration."""
config = ACTION_SCHEMA(config)
await toggle_entity.async_call_action_from_config(
hass, config, variables, context, DOMAIN
)

View File

@ -22,7 +22,6 @@ async def async_attach_trigger(
automation_info: dict,
) -> CALLBACK_TYPE:
"""Listen for state changes based on configuration."""
config = TRIGGER_SCHEMA(config)
return await toggle_entity.async_attach_trigger(
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]:
"""List device triggers."""
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,
) -> None:
"""Change state based on configuration."""
config = ACTION_SCHEMA(config)
await toggle_entity.async_call_action_from_config(
hass, config, variables, context, DOMAIN
)

View File

@ -22,7 +22,6 @@ async def async_attach_trigger(
automation_info: dict,
) -> CALLBACK_TYPE:
"""Listen for state changes based on configuration."""
config = TRIGGER_SCHEMA(config)
return await toggle_entity.async_attach_trigger(
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]:
"""List device triggers."""
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,
) -> None:
"""Perform an action based on configuration."""
config = ACTION_SCHEMA(config)
await ZHA_ACTION_TYPES[DEVICE_ACTION_TYPES[config[CONF_TYPE]]](
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):
"""Listen for state changes based on configuration."""
config = TRIGGER_SCHEMA(config)
trigger = (config[CONF_TYPE], config[CONF_SUBTYPE])
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 uuid import UUID
import voluptuous as vol
from pkg_resources import parse_version
import voluptuous as vol
import voluptuous_serialize
import homeassistant.util.dt as dt_util
from homeassistant.const import (
@ -374,6 +375,9 @@ def positive_timedelta(value: timedelta) -> timedelta:
return value
positive_time_period_dict = vol.All(time_period_dict, positive_timedelta)
def remove_falsy(value: List[T]) -> List[T]:
"""Remove falsy values from a list."""
return [v for v in value if v]
@ -690,6 +694,14 @@ def key_dependency(key, dependency):
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
PLATFORM_SCHEMA = vol.Schema(
{

View File

@ -10,7 +10,12 @@ import voluptuous as vol
import homeassistant.components.device_automation as device_automation
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.helpers import (
service,
@ -89,7 +94,7 @@ async def async_validate_action_config(
if action_type == ACTION_DEVICE_AUTOMATION:
platform = await device_automation.async_get_device_automation_platform(
hass, config, "action"
hass, config[CONF_DOMAIN], "action"
)
config = platform.ACTION_SCHEMA(config)
@ -346,7 +351,7 @@ class Script:
self.last_action = action.get(CONF_ALIAS, "device automation")
self._log("Executing step %s" % self.last_action)
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(
self.hass, action, variables, context

View File

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

View File

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

View File

@ -50,7 +50,7 @@ REQUIRES = [
"requests==2.22.0",
"ruamel.yaml==0.15.100",
"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))

View File

@ -56,6 +56,7 @@ from homeassistant.util.unit_system import METRIC_SYSTEM
from homeassistant.util.async_ import run_callback_threadsafe
from homeassistant.components.device_automation import ( # noqa
_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

View File

@ -1,4 +1,5 @@
"""The test for binary_sensor device automation."""
from datetime import timedelta
import pytest
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
import homeassistant.components.automation as automation
from homeassistant.helpers import device_registry
import homeassistant.util.dt as dt_util
from tests.common import (
MockConfigEntry,
async_fire_time_changed,
async_mock_service,
mock_device_registry,
mock_registry,
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
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):
"""Test for on and off triggers firing."""
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(
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)
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):
"""Test device automation with non existing integration."""
assert await async_setup_component(

View File

@ -1,4 +1,5 @@
"""The test for light device automation."""
from datetime import timedelta
import pytest
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
import homeassistant.components.automation as automation
from homeassistant.helpers import device_registry
import homeassistant.util.dt as dt_util
from tests.common import (
MockConfigEntry,
async_fire_time_changed,
async_mock_service,
mock_device_registry,
mock_registry,
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
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):
"""Test for turn_on and turn_off triggers firing."""
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(
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."""
from datetime import timedelta
import pytest
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
import homeassistant.components.automation as automation
from homeassistant.helpers import device_registry
import homeassistant.util.dt as dt_util
from tests.common import (
MockConfigEntry,
async_fire_time_changed,
async_mock_service,
mock_device_registry,
mock_registry,
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
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):
"""Test for turn_on and turn_off triggers firing."""
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(
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
)