diff --git a/homeassistant/components/automation/device.py b/homeassistant/components/automation/device.py index eb3e5a95c9c17e..dc65008c3fb268 100644 --- a/homeassistant/components/automation/device.py +++ b/homeassistant/components/automation/device.py @@ -5,6 +5,7 @@ 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 @@ 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) diff --git a/homeassistant/components/binary_sensor/device_trigger.py b/homeassistant/components/binary_sensor/device_trigger.py index 2211b3001045ed..c4d2efcb63b2bb 100644 --- a/homeassistant/components/binary_sensor/device_trigger.py +++ b/homeassistant/components/binary_sensor/device_trigger.py @@ -7,7 +7,7 @@ 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 @@ { 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} + ) + } diff --git a/homeassistant/components/deconz/device_trigger.py b/homeassistant/components/deconz/device_trigger.py index 5339eff055e89f..badbe8b8651278 100644 --- a/homeassistant/components/deconz/device_trigger.py +++ b/homeassistant/components/deconz/device_trigger.py @@ -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]) diff --git a/homeassistant/components/device_automation/__init__.py b/homeassistant/components/device_automation/__init__.py index 23e320fe153b60..a7e04f874b4d83 100644 --- a/homeassistant/components/device_automation/__init__.py +++ b/homeassistant/components/device_automation/__init__.py @@ -4,9 +4,11 @@ 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 @@ ) 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) diff --git a/homeassistant/components/device_automation/toggle_entity.py b/homeassistant/components/device_automation/toggle_entity.py index ef1b605f4d68e0..7c68be83ba30ec 100644 --- a/homeassistant/components/device_automation/toggle_entity.py +++ b/homeassistant/components/device_automation/toggle_entity.py @@ -13,7 +13,13 @@ 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 @@ { 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} + ) + } diff --git a/homeassistant/components/light/device_action.py b/homeassistant/components/light/device_action.py index ea37b8e9470534..9d8ef6bceafb32 100644 --- a/homeassistant/components/light/device_action.py +++ b/homeassistant/components/light/device_action.py @@ -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 ) diff --git a/homeassistant/components/light/device_trigger.py b/homeassistant/components/light/device_trigger.py index f2a82afdc2d41c..5bd5d83e1c02ee 100644 --- a/homeassistant/components/light/device_trigger.py +++ b/homeassistant/components/light/device_trigger.py @@ -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) diff --git a/homeassistant/components/switch/device_action.py b/homeassistant/components/switch/device_action.py index ca91cc70512be5..a65c1acc5124b8 100644 --- a/homeassistant/components/switch/device_action.py +++ b/homeassistant/components/switch/device_action.py @@ -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 ) diff --git a/homeassistant/components/switch/device_trigger.py b/homeassistant/components/switch/device_trigger.py index 9be294d5460c99..22a016e49b9bc2 100644 --- a/homeassistant/components/switch/device_trigger.py +++ b/homeassistant/components/switch/device_trigger.py @@ -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) diff --git a/homeassistant/components/zha/device_action.py b/homeassistant/components/zha/device_action.py index 27e78507bfb317..460676a75a0045 100644 --- a/homeassistant/components/zha/device_action.py +++ b/homeassistant/components/zha/device_action.py @@ -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 ) diff --git a/homeassistant/components/zha/device_trigger.py b/homeassistant/components/zha/device_trigger.py index 331dc3d32968d3..c1ea3c2b761698 100644 --- a/homeassistant/components/zha/device_trigger.py +++ b/homeassistant/components/zha/device_trigger.py @@ -23,7 +23,6 @@ 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]) diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index d0aeb4f4968bc6..2d1bb89d23a56e 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -15,8 +15,9 @@ 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 validator(value): 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( { diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index e383f1013ab204..d9b3df8c01b717 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -10,7 +10,12 @@ 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 @@ async def _async_device_automation(self, action, variables, context): 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 diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 684285a1cf10b3..c500ddca85d4c5 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -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 diff --git a/requirements_all.txt b/requirements_all.txt index a0f64eeec74b9d..4a2a2cf45fcf76 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -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 diff --git a/setup.py b/setup.py index d842ae39ae1b77..23a8a808f4376f 100755 --- a/setup.py +++ b/setup.py @@ -50,7 +50,7 @@ "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)) diff --git a/tests/common.py b/tests/common.py index 1982e80dfe949e..bd611e04c37a6e 100644 --- a/tests/common.py +++ b/tests/common.py @@ -56,6 +56,7 @@ 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 diff --git a/tests/components/binary_sensor/test_device_trigger.py b/tests/components/binary_sensor/test_device_trigger.py index 5be354c78fc5cf..9bab1ff1f363e9 100644 --- a/tests/components/binary_sensor/test_device_trigger.py +++ b/tests/components/binary_sensor/test_device_trigger.py @@ -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.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 + ) diff --git a/tests/components/device_automation/test_init.py b/tests/components/device_automation/test_init.py index acfa853d596f27..8a92f69e57493a 100644 --- a/tests/components/device_automation/test_init.py +++ b/tests/components/device_automation/test_init.py @@ -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( diff --git a/tests/components/light/test_device_trigger.py b/tests/components/light/test_device_trigger.py index 9b540c7aa15af7..a6437ef9ee0a64 100644 --- a/tests/components/light/test_device_trigger.py +++ b/tests/components/light/test_device_trigger.py @@ -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.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 + ) diff --git a/tests/components/switch/test_device_trigger.py b/tests/components/switch/test_device_trigger.py index 43af9fe3df34b2..31fb6d30f60994 100644 --- a/tests/components/switch/test_device_trigger.py +++ b/tests/components/switch/test_device_trigger.py @@ -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.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 + )