diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index f0529f126f1..f669d415854 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -3,7 +3,7 @@ import asyncio from functools import partial import importlib import logging -from typing import Any +from typing import Any, Awaitable, Callable import voluptuous as vol @@ -23,7 +23,7 @@ from homeassistant.const import ( SERVICE_TURN_ON, STATE_ON, ) -from homeassistant.core import Context, CoreState +from homeassistant.core import Context, CoreState, HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import condition, extract_domain_configs, script import homeassistant.helpers.config_validation as cv @@ -31,6 +31,7 @@ from homeassistant.helpers.config_validation import ENTITY_SERVICE_SCHEMA from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.helpers.typing import TemplateVarsType from homeassistant.loader import bind_hass from homeassistant.util.dt import parse_datetime, utcnow @@ -67,6 +68,8 @@ SERVICE_TRIGGER = "trigger" _LOGGER = logging.getLogger(__name__) +AutomationActionType = Callable[[HomeAssistant, TemplateVarsType], Awaitable[None]] + def _platform_validator(config): """Validate it is a valid platform.""" @@ -474,7 +477,7 @@ async def _async_process_trigger(hass, config, trigger_configs, name, action): platform = importlib.import_module(".{}".format(conf[CONF_PLATFORM]), __name__) try: - remove = await platform.async_trigger(hass, conf, action, info) + remove = await platform.async_attach_trigger(hass, conf, action, info) except InvalidDeviceAutomationConfig: remove = False diff --git a/homeassistant/components/automation/device.py b/homeassistant/components/automation/device.py index b090484ab67..fe2d65edef6 100644 --- a/homeassistant/components/automation/device.py +++ b/homeassistant/components/automation/device.py @@ -13,8 +13,8 @@ TRIGGER_SCHEMA = vol.Schema( ) -async def async_trigger(hass, config, action, automation_info): +async def async_attach_trigger(hass, config, action, automation_info): """Listen for trigger.""" integration = await async_get_integration(hass, config[CONF_DOMAIN]) - platform = integration.get_platform("device_automation") - return await platform.async_trigger(hass, config, action, automation_info) + platform = integration.get_platform("device_trigger") + return await platform.async_attach_trigger(hass, config, action, automation_info) diff --git a/homeassistant/components/automation/event.py b/homeassistant/components/automation/event.py index d372aedd1d7..26dacac974d 100644 --- a/homeassistant/components/automation/event.py +++ b/homeassistant/components/automation/event.py @@ -24,7 +24,9 @@ TRIGGER_SCHEMA = vol.Schema( ) -async def async_trigger(hass, config, action, automation_info): +async def async_attach_trigger( + hass, config, action, automation_info, *, platform_type="event" +): """Listen for events based on configuration.""" event_type = config.get(CONF_EVENT_TYPE) event_data_schema = ( @@ -47,7 +49,7 @@ async def async_trigger(hass, config, action, automation_info): hass.async_run_job( action( - {"trigger": {"platform": "event", "event": event}}, + {"trigger": {"platform": platform_type, "event": event}}, context=event.context, ) ) diff --git a/homeassistant/components/automation/geo_location.py b/homeassistant/components/automation/geo_location.py index 3f2aa1c00d7..0ef0884d329 100644 --- a/homeassistant/components/automation/geo_location.py +++ b/homeassistant/components/automation/geo_location.py @@ -37,7 +37,7 @@ def source_match(state, source): return state and state.attributes.get("source") == source -async def async_trigger(hass, config, action, automation_info): +async def async_attach_trigger(hass, config, action, automation_info): """Listen for state changes based on configuration.""" source = config.get(CONF_SOURCE).lower() zone_entity_id = config.get(CONF_ZONE) diff --git a/homeassistant/components/automation/homeassistant.py b/homeassistant/components/automation/homeassistant.py index bd1da7e7e1f..e4eb029d5aa 100644 --- a/homeassistant/components/automation/homeassistant.py +++ b/homeassistant/components/automation/homeassistant.py @@ -21,7 +21,7 @@ TRIGGER_SCHEMA = vol.Schema( ) -async def async_trigger(hass, config, action, automation_info): +async def async_attach_trigger(hass, config, action, automation_info): """Listen for events based on configuration.""" event = config.get(CONF_EVENT) diff --git a/homeassistant/components/automation/litejet.py b/homeassistant/components/automation/litejet.py index 7bc4c937765..9512db8261d 100644 --- a/homeassistant/components/automation/litejet.py +++ b/homeassistant/components/automation/litejet.py @@ -32,7 +32,7 @@ TRIGGER_SCHEMA = vol.Schema( ) -async def async_trigger(hass, config, action, automation_info): +async def async_attach_trigger(hass, config, action, automation_info): """Listen for events based on configuration.""" number = config.get(CONF_NUMBER) held_more_than = config.get(CONF_HELD_MORE_THAN) diff --git a/homeassistant/components/automation/mqtt.py b/homeassistant/components/automation/mqtt.py index fd9a778dbfc..135a421f72e 100644 --- a/homeassistant/components/automation/mqtt.py +++ b/homeassistant/components/automation/mqtt.py @@ -25,7 +25,7 @@ TRIGGER_SCHEMA = vol.Schema( ) -async def async_trigger(hass, config, action, automation_info): +async def async_attach_trigger(hass, config, action, automation_info): """Listen for state changes based on configuration.""" topic = config[CONF_TOPIC] payload = config.get(CONF_PAYLOAD) diff --git a/homeassistant/components/automation/numeric_state.py b/homeassistant/components/automation/numeric_state.py index b33d724d770..9dd4657291d 100644 --- a/homeassistant/components/automation/numeric_state.py +++ b/homeassistant/components/automation/numeric_state.py @@ -40,7 +40,7 @@ TRIGGER_SCHEMA = vol.All( _LOGGER = logging.getLogger(__name__) -async def async_trigger(hass, config, action, automation_info): +async def async_attach_trigger(hass, config, action, automation_info): """Listen for state changes based on configuration.""" entity_id = config.get(CONF_ENTITY_ID) below = config.get(CONF_BELOW) diff --git a/homeassistant/components/automation/state.py b/homeassistant/components/automation/state.py index 5fbe97185a7..184b9ea302b 100644 --- a/homeassistant/components/automation/state.py +++ b/homeassistant/components/automation/state.py @@ -37,7 +37,9 @@ TRIGGER_SCHEMA = vol.All( ) -async def async_trigger(hass, config, action, automation_info): +async def async_attach_trigger( + hass, config, action, automation_info, *, platform_type="state" +): """Listen for state changes based on configuration.""" entity_id = config.get(CONF_ENTITY_ID) from_state = config.get(CONF_FROM, MATCH_ALL) @@ -59,7 +61,7 @@ async def async_trigger(hass, config, action, automation_info): action( { "trigger": { - "platform": "state", + "platform": platform_type, "entity_id": entity, "from_state": from_s, "to_state": to_s, diff --git a/homeassistant/components/automation/sun.py b/homeassistant/components/automation/sun.py index 7cbbe56f326..66892784a54 100644 --- a/homeassistant/components/automation/sun.py +++ b/homeassistant/components/automation/sun.py @@ -28,7 +28,7 @@ TRIGGER_SCHEMA = vol.Schema( ) -async def async_trigger(hass, config, action, automation_info): +async def async_attach_trigger(hass, config, action, automation_info): """Listen for events based on configuration.""" event = config.get(CONF_EVENT) offset = config.get(CONF_OFFSET) diff --git a/homeassistant/components/automation/template.py b/homeassistant/components/automation/template.py index c83d660912c..f2b4134de42 100644 --- a/homeassistant/components/automation/template.py +++ b/homeassistant/components/automation/template.py @@ -28,7 +28,7 @@ TRIGGER_SCHEMA = IF_ACTION_SCHEMA = vol.Schema( ) -async def async_trigger(hass, config, action, automation_info): +async def async_attach_trigger(hass, config, action, automation_info): """Listen for state changes based on configuration.""" value_template = config.get(CONF_VALUE_TEMPLATE) value_template.hass = hass diff --git a/homeassistant/components/automation/time.py b/homeassistant/components/automation/time.py index 3942d0efadb..231bc346e14 100644 --- a/homeassistant/components/automation/time.py +++ b/homeassistant/components/automation/time.py @@ -18,7 +18,7 @@ TRIGGER_SCHEMA = vol.Schema( ) -async def async_trigger(hass, config, action, automation_info): +async def async_attach_trigger(hass, config, action, automation_info): """Listen for state changes based on configuration.""" at_time = config.get(CONF_AT) hours, minutes, seconds = at_time.hour, at_time.minute, at_time.second diff --git a/homeassistant/components/automation/time_pattern.py b/homeassistant/components/automation/time_pattern.py index f749a308bf7..ee092916112 100644 --- a/homeassistant/components/automation/time_pattern.py +++ b/homeassistant/components/automation/time_pattern.py @@ -30,7 +30,7 @@ TRIGGER_SCHEMA = vol.All( ) -async def async_trigger(hass, config, action, automation_info): +async def async_attach_trigger(hass, config, action, automation_info): """Listen for state changes based on configuration.""" hours = config.get(CONF_HOURS) minutes = config.get(CONF_MINUTES) diff --git a/homeassistant/components/automation/webhook.py b/homeassistant/components/automation/webhook.py index 706afbe9042..bbcf9bd9ddc 100644 --- a/homeassistant/components/automation/webhook.py +++ b/homeassistant/components/automation/webhook.py @@ -36,7 +36,7 @@ async def _handle_webhook(action, hass, webhook_id, request): hass.async_run_job(action, {"trigger": result}) -async def async_trigger(hass, config, action, automation_info): +async def async_attach_trigger(hass, config, action, automation_info): """Trigger based on incoming webhooks.""" webhook_id = config.get(CONF_WEBHOOK_ID) hass.components.webhook.async_register( diff --git a/homeassistant/components/automation/zone.py b/homeassistant/components/automation/zone.py index 35b11006024..535ef298a2a 100644 --- a/homeassistant/components/automation/zone.py +++ b/homeassistant/components/automation/zone.py @@ -31,7 +31,7 @@ TRIGGER_SCHEMA = vol.Schema( ) -async def async_trigger(hass, config, action, automation_info): +async def async_attach_trigger(hass, config, action, automation_info): """Listen for state changes based on configuration.""" entity_id = config.get(CONF_ENTITY_ID) zone_entity_id = config.get(CONF_ZONE) diff --git a/homeassistant/components/binary_sensor/device_automation.py b/homeassistant/components/binary_sensor/device_automation.py deleted file mode 100644 index c609c2eb5da..00000000000 --- a/homeassistant/components/binary_sensor/device_automation.py +++ /dev/null @@ -1,423 +0,0 @@ -"""Provides device automations for lights.""" -import voluptuous as vol - -import homeassistant.components.automation.state as state -from homeassistant.components.device_automation.const import ( - CONF_IS_OFF, - CONF_IS_ON, - CONF_TURNED_OFF, - CONF_TURNED_ON, -) -from homeassistant.const import ( - ATTR_DEVICE_CLASS, - CONF_CONDITION, - CONF_DEVICE_ID, - CONF_DOMAIN, - CONF_ENTITY_ID, - CONF_PLATFORM, - CONF_TYPE, -) -from homeassistant.core import split_entity_id -from homeassistant.helpers.entity_registry import async_entries_for_device -from homeassistant.helpers import condition, config_validation as cv - -from . import ( - DOMAIN, - DEVICE_CLASS_BATTERY, - DEVICE_CLASS_COLD, - DEVICE_CLASS_CONNECTIVITY, - DEVICE_CLASS_DOOR, - DEVICE_CLASS_GARAGE_DOOR, - DEVICE_CLASS_GAS, - DEVICE_CLASS_HEAT, - DEVICE_CLASS_LIGHT, - DEVICE_CLASS_LOCK, - DEVICE_CLASS_MOISTURE, - DEVICE_CLASS_MOTION, - DEVICE_CLASS_MOVING, - DEVICE_CLASS_OCCUPANCY, - DEVICE_CLASS_OPENING, - DEVICE_CLASS_PLUG, - DEVICE_CLASS_POWER, - DEVICE_CLASS_PRESENCE, - DEVICE_CLASS_PROBLEM, - DEVICE_CLASS_SAFETY, - DEVICE_CLASS_SMOKE, - DEVICE_CLASS_SOUND, - DEVICE_CLASS_VIBRATION, - DEVICE_CLASS_WINDOW, -) - - -# mypy: allow-untyped-defs, no-check-untyped-defs - -DEVICE_CLASS_NONE = "none" - -CONF_IS_BAT_LOW = "is_bat_low" -CONF_IS_NOT_BAT_LOW = "is_not_bat_low" -CONF_IS_COLD = "is_cold" -CONF_IS_NOT_COLD = "is_not_cold" -CONF_IS_CONNECTED = "is_connected" -CONF_IS_NOT_CONNECTED = "is_not_connected" -CONF_IS_GAS = "is_gas" -CONF_IS_NO_GAS = "is_no_gas" -CONF_IS_HOT = "is_hot" -CONF_IS_NOT_HOT = "is_not_hot" -CONF_IS_LIGHT = "is_light" -CONF_IS_NO_LIGHT = "is_no_light" -CONF_IS_LOCKED = "is_locked" -CONF_IS_NOT_LOCKED = "is_not_locked" -CONF_IS_MOIST = "is_moist" -CONF_IS_NOT_MOIST = "is_not_moist" -CONF_IS_MOTION = "is_motion" -CONF_IS_NO_MOTION = "is_no_motion" -CONF_IS_MOVING = "is_moving" -CONF_IS_NOT_MOVING = "is_not_moving" -CONF_IS_OCCUPIED = "is_occupied" -CONF_IS_NOT_OCCUPIED = "is_not_occupied" -CONF_IS_PLUGGED_IN = "is_plugged_in" -CONF_IS_NOT_PLUGGED_IN = "is_not_plugged_in" -CONF_IS_POWERED = "is_powered" -CONF_IS_NOT_POWERED = "is_not_powered" -CONF_IS_PRESENT = "is_present" -CONF_IS_NOT_PRESENT = "is_not_present" -CONF_IS_PROBLEM = "is_problem" -CONF_IS_NO_PROBLEM = "is_no_problem" -CONF_IS_UNSAFE = "is_unsafe" -CONF_IS_NOT_UNSAFE = "is_not_unsafe" -CONF_IS_SMOKE = "is_smoke" -CONF_IS_NO_SMOKE = "is_no_smoke" -CONF_IS_SOUND = "is_sound" -CONF_IS_NO_SOUND = "is_no_sound" -CONF_IS_VIBRATION = "is_vibration" -CONF_IS_NO_VIBRATION = "is_no_vibration" -CONF_IS_OPEN = "is_open" -CONF_IS_NOT_OPEN = "is_not_open" - -CONF_BAT_LOW = "bat_low" -CONF_NOT_BAT_LOW = "not_bat_low" -CONF_COLD = "cold" -CONF_NOT_COLD = "not_cold" -CONF_CONNECTED = "connected" -CONF_NOT_CONNECTED = "not_connected" -CONF_GAS = "gas" -CONF_NO_GAS = "no_gas" -CONF_HOT = "hot" -CONF_NOT_HOT = "not_hot" -CONF_LIGHT = "light" -CONF_NO_LIGHT = "no_light" -CONF_LOCKED = "locked" -CONF_NOT_LOCKED = "not_locked" -CONF_MOIST = "moist" -CONF_NOT_MOIST = "not_moist" -CONF_MOTION = "motion" -CONF_NO_MOTION = "no_motion" -CONF_MOVING = "moving" -CONF_NOT_MOVING = "not_moving" -CONF_OCCUPIED = "occupied" -CONF_NOT_OCCUPIED = "not_occupied" -CONF_PLUGGED_IN = "plugged_in" -CONF_NOT_PLUGGED_IN = "not_plugged_in" -CONF_POWERED = "powered" -CONF_NOT_POWERED = "not_powered" -CONF_PRESENT = "present" -CONF_NOT_PRESENT = "not_present" -CONF_PROBLEM = "problem" -CONF_NO_PROBLEM = "no_problem" -CONF_UNSAFE = "unsafe" -CONF_NOT_UNSAFE = "not_unsafe" -CONF_SMOKE = "smoke" -CONF_NO_SMOKE = "no_smoke" -CONF_SOUND = "sound" -CONF_NO_SOUND = "no_sound" -CONF_VIBRATION = "vibration" -CONF_NO_VIBRATION = "no_vibration" -CONF_OPEN = "open" -CONF_NOT_OPEN = "not_open" - -IS_ON = [ - CONF_IS_BAT_LOW, - CONF_IS_COLD, - CONF_IS_CONNECTED, - CONF_IS_GAS, - CONF_IS_HOT, - CONF_IS_LIGHT, - CONF_IS_LOCKED, - CONF_IS_MOIST, - CONF_IS_MOTION, - CONF_IS_MOVING, - CONF_IS_OCCUPIED, - CONF_IS_OPEN, - CONF_IS_PLUGGED_IN, - CONF_IS_POWERED, - CONF_IS_PRESENT, - CONF_IS_PROBLEM, - CONF_IS_SMOKE, - CONF_IS_SOUND, - CONF_IS_UNSAFE, - CONF_IS_VIBRATION, - CONF_IS_ON, -] - -IS_OFF = [ - CONF_IS_NOT_BAT_LOW, - CONF_IS_NOT_COLD, - CONF_IS_NOT_CONNECTED, - CONF_IS_NOT_HOT, - CONF_IS_NOT_LOCKED, - CONF_IS_NOT_MOIST, - CONF_IS_NOT_MOVING, - CONF_IS_NOT_OCCUPIED, - CONF_IS_NOT_OPEN, - CONF_IS_NOT_PLUGGED_IN, - CONF_IS_NOT_POWERED, - CONF_IS_NOT_PRESENT, - CONF_IS_NOT_UNSAFE, - CONF_IS_NO_GAS, - CONF_IS_NO_LIGHT, - CONF_IS_NO_MOTION, - CONF_IS_NO_PROBLEM, - CONF_IS_NO_SMOKE, - CONF_IS_NO_SOUND, - CONF_IS_NO_VIBRATION, - CONF_IS_OFF, -] - -TURNED_ON = [ - CONF_BAT_LOW, - CONF_COLD, - CONF_CONNECTED, - CONF_GAS, - CONF_HOT, - CONF_LIGHT, - CONF_LOCKED, - CONF_MOIST, - CONF_MOTION, - CONF_MOVING, - CONF_OCCUPIED, - CONF_OPEN, - CONF_PLUGGED_IN, - CONF_POWERED, - CONF_PRESENT, - CONF_PROBLEM, - CONF_SMOKE, - CONF_SOUND, - CONF_UNSAFE, - CONF_VIBRATION, - CONF_TURNED_ON, -] - -TURNED_OFF = [ - CONF_NOT_BAT_LOW, - CONF_NOT_COLD, - CONF_NOT_CONNECTED, - CONF_NOT_HOT, - CONF_NOT_LOCKED, - CONF_NOT_MOIST, - CONF_NOT_MOVING, - CONF_NOT_OCCUPIED, - CONF_NOT_OPEN, - CONF_NOT_PLUGGED_IN, - CONF_NOT_POWERED, - CONF_NOT_PRESENT, - CONF_NOT_UNSAFE, - CONF_NO_GAS, - CONF_NO_LIGHT, - CONF_NO_MOTION, - CONF_NO_PROBLEM, - CONF_NO_SMOKE, - CONF_NO_SOUND, - CONF_NO_VIBRATION, - CONF_TURNED_OFF, -] - -ENTITY_CONDITIONS = { - DEVICE_CLASS_BATTERY: [ - {CONF_TYPE: CONF_IS_BAT_LOW}, - {CONF_TYPE: CONF_IS_NOT_BAT_LOW}, - ], - DEVICE_CLASS_COLD: [{CONF_TYPE: CONF_IS_COLD}, {CONF_TYPE: CONF_IS_NOT_COLD}], - DEVICE_CLASS_CONNECTIVITY: [ - {CONF_TYPE: CONF_IS_CONNECTED}, - {CONF_TYPE: CONF_IS_NOT_CONNECTED}, - ], - DEVICE_CLASS_DOOR: [{CONF_TYPE: CONF_IS_OPEN}, {CONF_TYPE: CONF_IS_NOT_OPEN}], - DEVICE_CLASS_GARAGE_DOOR: [ - {CONF_TYPE: CONF_IS_OPEN}, - {CONF_TYPE: CONF_IS_NOT_OPEN}, - ], - DEVICE_CLASS_GAS: [{CONF_TYPE: CONF_IS_GAS}, {CONF_TYPE: CONF_IS_NO_GAS}], - DEVICE_CLASS_HEAT: [{CONF_TYPE: CONF_IS_HOT}, {CONF_TYPE: CONF_IS_NOT_HOT}], - DEVICE_CLASS_LIGHT: [{CONF_TYPE: CONF_IS_LIGHT}, {CONF_TYPE: CONF_IS_NO_LIGHT}], - DEVICE_CLASS_LOCK: [{CONF_TYPE: CONF_IS_LOCKED}, {CONF_TYPE: CONF_IS_NOT_LOCKED}], - DEVICE_CLASS_MOISTURE: [{CONF_TYPE: CONF_IS_MOIST}, {CONF_TYPE: CONF_IS_NOT_MOIST}], - DEVICE_CLASS_MOTION: [{CONF_TYPE: CONF_IS_MOTION}, {CONF_TYPE: CONF_IS_NO_MOTION}], - DEVICE_CLASS_MOVING: [{CONF_TYPE: CONF_IS_MOVING}, {CONF_TYPE: CONF_IS_NOT_MOVING}], - DEVICE_CLASS_OCCUPANCY: [ - {CONF_TYPE: CONF_IS_OCCUPIED}, - {CONF_TYPE: CONF_IS_NOT_OCCUPIED}, - ], - DEVICE_CLASS_OPENING: [{CONF_TYPE: CONF_IS_OPEN}, {CONF_TYPE: CONF_IS_NOT_OPEN}], - DEVICE_CLASS_PLUG: [ - {CONF_TYPE: CONF_IS_PLUGGED_IN}, - {CONF_TYPE: CONF_IS_NOT_PLUGGED_IN}, - ], - DEVICE_CLASS_POWER: [ - {CONF_TYPE: CONF_IS_POWERED}, - {CONF_TYPE: CONF_IS_NOT_POWERED}, - ], - DEVICE_CLASS_PRESENCE: [ - {CONF_TYPE: CONF_IS_PRESENT}, - {CONF_TYPE: CONF_IS_NOT_PRESENT}, - ], - DEVICE_CLASS_PROBLEM: [ - {CONF_TYPE: CONF_IS_PROBLEM}, - {CONF_TYPE: CONF_IS_NO_PROBLEM}, - ], - DEVICE_CLASS_SAFETY: [{CONF_TYPE: CONF_IS_UNSAFE}, {CONF_TYPE: CONF_IS_NOT_UNSAFE}], - DEVICE_CLASS_SMOKE: [{CONF_TYPE: CONF_IS_SMOKE}, {CONF_TYPE: CONF_IS_NO_SMOKE}], - DEVICE_CLASS_SOUND: [{CONF_TYPE: CONF_IS_SOUND}, {CONF_TYPE: CONF_IS_NO_SOUND}], - DEVICE_CLASS_VIBRATION: [ - {CONF_TYPE: CONF_IS_VIBRATION}, - {CONF_TYPE: CONF_IS_NO_VIBRATION}, - ], - DEVICE_CLASS_WINDOW: [{CONF_TYPE: CONF_IS_OPEN}, {CONF_TYPE: CONF_IS_NOT_OPEN}], - DEVICE_CLASS_NONE: [{CONF_TYPE: CONF_IS_ON}, {CONF_TYPE: CONF_IS_OFF}], -} - -ENTITY_TRIGGERS = { - DEVICE_CLASS_BATTERY: [{CONF_TYPE: CONF_BAT_LOW}, {CONF_TYPE: CONF_NOT_BAT_LOW}], - DEVICE_CLASS_COLD: [{CONF_TYPE: CONF_COLD}, {CONF_TYPE: CONF_NOT_COLD}], - DEVICE_CLASS_CONNECTIVITY: [ - {CONF_TYPE: CONF_CONNECTED}, - {CONF_TYPE: CONF_NOT_CONNECTED}, - ], - DEVICE_CLASS_DOOR: [{CONF_TYPE: CONF_OPEN}, {CONF_TYPE: CONF_NOT_OPEN}], - DEVICE_CLASS_GARAGE_DOOR: [{CONF_TYPE: CONF_OPEN}, {CONF_TYPE: CONF_NOT_OPEN}], - DEVICE_CLASS_GAS: [{CONF_TYPE: CONF_GAS}, {CONF_TYPE: CONF_NO_GAS}], - DEVICE_CLASS_HEAT: [{CONF_TYPE: CONF_HOT}, {CONF_TYPE: CONF_NOT_HOT}], - DEVICE_CLASS_LIGHT: [{CONF_TYPE: CONF_LIGHT}, {CONF_TYPE: CONF_NO_LIGHT}], - DEVICE_CLASS_LOCK: [{CONF_TYPE: CONF_LOCKED}, {CONF_TYPE: CONF_NOT_LOCKED}], - DEVICE_CLASS_MOISTURE: [{CONF_TYPE: CONF_MOIST}, {CONF_TYPE: CONF_NOT_MOIST}], - DEVICE_CLASS_MOTION: [{CONF_TYPE: CONF_MOTION}, {CONF_TYPE: CONF_NO_MOTION}], - DEVICE_CLASS_MOVING: [{CONF_TYPE: CONF_MOVING}, {CONF_TYPE: CONF_NOT_MOVING}], - DEVICE_CLASS_OCCUPANCY: [ - {CONF_TYPE: CONF_OCCUPIED}, - {CONF_TYPE: CONF_NOT_OCCUPIED}, - ], - DEVICE_CLASS_OPENING: [{CONF_TYPE: CONF_OPEN}, {CONF_TYPE: CONF_NOT_OPEN}], - DEVICE_CLASS_PLUG: [{CONF_TYPE: CONF_PLUGGED_IN}, {CONF_TYPE: CONF_NOT_PLUGGED_IN}], - DEVICE_CLASS_POWER: [{CONF_TYPE: CONF_POWERED}, {CONF_TYPE: CONF_NOT_POWERED}], - DEVICE_CLASS_PRESENCE: [{CONF_TYPE: CONF_PRESENT}, {CONF_TYPE: CONF_NOT_PRESENT}], - DEVICE_CLASS_PROBLEM: [{CONF_TYPE: CONF_PROBLEM}, {CONF_TYPE: CONF_NO_PROBLEM}], - DEVICE_CLASS_SAFETY: [{CONF_TYPE: CONF_UNSAFE}, {CONF_TYPE: CONF_NOT_UNSAFE}], - DEVICE_CLASS_SMOKE: [{CONF_TYPE: CONF_SMOKE}, {CONF_TYPE: CONF_NO_SMOKE}], - DEVICE_CLASS_SOUND: [{CONF_TYPE: CONF_SOUND}, {CONF_TYPE: CONF_NO_SOUND}], - DEVICE_CLASS_VIBRATION: [ - {CONF_TYPE: CONF_VIBRATION}, - {CONF_TYPE: CONF_NO_VIBRATION}, - ], - DEVICE_CLASS_WINDOW: [{CONF_TYPE: CONF_OPEN}, {CONF_TYPE: CONF_NOT_OPEN}], - DEVICE_CLASS_NONE: [{CONF_TYPE: CONF_TURNED_ON}, {CONF_TYPE: CONF_TURNED_OFF}], -} - -CONDITION_SCHEMA = vol.Schema( - { - vol.Required(CONF_CONDITION): "device", - vol.Required(CONF_DEVICE_ID): str, - vol.Required(CONF_DOMAIN): DOMAIN, - vol.Required(CONF_ENTITY_ID): cv.entity_id, - vol.Required(CONF_TYPE): vol.In(IS_OFF + IS_ON), - } -) - -TRIGGER_SCHEMA = vol.Schema( - { - vol.Required(CONF_PLATFORM): "device", - vol.Required(CONF_DEVICE_ID): str, - vol.Required(CONF_DOMAIN): DOMAIN, - vol.Required(CONF_ENTITY_ID): cv.entity_id, - vol.Required(CONF_TYPE): vol.In(TURNED_OFF + TURNED_ON), - } -) - - -def async_condition_from_config(config, config_validation): - """Evaluate state based on configuration.""" - config = CONDITION_SCHEMA(config) - condition_type = config[CONF_TYPE] - if condition_type in IS_ON: - stat = "on" - else: - stat = "off" - state_config = { - condition.CONF_CONDITION: "state", - condition.CONF_ENTITY_ID: config[CONF_ENTITY_ID], - condition.CONF_STATE: stat, - } - - return condition.state_from_config(state_config, config_validation) - - -async def async_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" - to_state = "on" - else: - from_state = "on" - to_state = "off" - state_config = { - state.CONF_ENTITY_ID: config[CONF_ENTITY_ID], - state.CONF_FROM: from_state, - state.CONF_TO: to_state, - } - - return await state.async_trigger(hass, state_config, action, automation_info) - - -def _is_domain(entity, domain): - return split_entity_id(entity.entity_id)[0] == domain - - -async def _async_get_automations(hass, device_id, automation_templates, domain): - """List device automations.""" - automations = [] - entity_registry = await hass.helpers.entity_registry.async_get_registry() - - entities = async_entries_for_device(entity_registry, device_id) - domain_entities = [x for x in entities if _is_domain(x, domain)] - for entity in domain_entities: - device_class = DEVICE_CLASS_NONE - entity_id = entity.entity_id - entity = hass.states.get(entity_id) - if entity and ATTR_DEVICE_CLASS in entity.attributes: - device_class = entity.attributes[ATTR_DEVICE_CLASS] - automation_template = automation_templates[device_class] - - for automation in automation_template: - automation = dict(automation) - automation.update(device_id=device_id, entity_id=entity_id, domain=domain) - automations.append(automation) - - return automations - - -async def async_get_conditions(hass, device_id): - """List device conditions.""" - automations = await _async_get_automations( - hass, device_id, ENTITY_CONDITIONS, DOMAIN - ) - for automation in automations: - automation.update(condition="device") - return automations - - -async def async_get_triggers(hass, device_id): - """List device triggers.""" - automations = await _async_get_automations(hass, device_id, ENTITY_TRIGGERS, DOMAIN) - for automation in automations: - automation.update(platform="device") - return automations diff --git a/homeassistant/components/binary_sensor/device_condition.py b/homeassistant/components/binary_sensor/device_condition.py new file mode 100644 index 00000000000..70b79becb8b --- /dev/null +++ b/homeassistant/components/binary_sensor/device_condition.py @@ -0,0 +1,247 @@ +"""Implemenet device conditions for binary sensor.""" +from typing import List +import voluptuous as vol + +from homeassistant.core import HomeAssistant +from homeassistant.components.device_automation.const import CONF_IS_OFF, CONF_IS_ON +from homeassistant.const import ATTR_DEVICE_CLASS, CONF_ENTITY_ID, CONF_TYPE +from homeassistant.helpers import condition, config_validation as cv +from homeassistant.helpers.entity_registry import ( + async_entries_for_device, + async_get_registry, +) +from homeassistant.helpers.typing import ConfigType + +from . import ( + DOMAIN, + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_COLD, + DEVICE_CLASS_CONNECTIVITY, + DEVICE_CLASS_DOOR, + DEVICE_CLASS_GARAGE_DOOR, + DEVICE_CLASS_GAS, + DEVICE_CLASS_HEAT, + DEVICE_CLASS_LIGHT, + DEVICE_CLASS_LOCK, + DEVICE_CLASS_MOISTURE, + DEVICE_CLASS_MOTION, + DEVICE_CLASS_MOVING, + DEVICE_CLASS_OCCUPANCY, + DEVICE_CLASS_OPENING, + DEVICE_CLASS_PLUG, + DEVICE_CLASS_POWER, + DEVICE_CLASS_PRESENCE, + DEVICE_CLASS_PROBLEM, + DEVICE_CLASS_SAFETY, + DEVICE_CLASS_SMOKE, + DEVICE_CLASS_SOUND, + DEVICE_CLASS_VIBRATION, + DEVICE_CLASS_WINDOW, +) + +DEVICE_CLASS_NONE = "none" + +CONF_IS_BAT_LOW = "is_bat_low" +CONF_IS_NOT_BAT_LOW = "is_not_bat_low" +CONF_IS_COLD = "is_cold" +CONF_IS_NOT_COLD = "is_not_cold" +CONF_IS_CONNECTED = "is_connected" +CONF_IS_NOT_CONNECTED = "is_not_connected" +CONF_IS_GAS = "is_gas" +CONF_IS_NO_GAS = "is_no_gas" +CONF_IS_HOT = "is_hot" +CONF_IS_NOT_HOT = "is_not_hot" +CONF_IS_LIGHT = "is_light" +CONF_IS_NO_LIGHT = "is_no_light" +CONF_IS_LOCKED = "is_locked" +CONF_IS_NOT_LOCKED = "is_not_locked" +CONF_IS_MOIST = "is_moist" +CONF_IS_NOT_MOIST = "is_not_moist" +CONF_IS_MOTION = "is_motion" +CONF_IS_NO_MOTION = "is_no_motion" +CONF_IS_MOVING = "is_moving" +CONF_IS_NOT_MOVING = "is_not_moving" +CONF_IS_OCCUPIED = "is_occupied" +CONF_IS_NOT_OCCUPIED = "is_not_occupied" +CONF_IS_PLUGGED_IN = "is_plugged_in" +CONF_IS_NOT_PLUGGED_IN = "is_not_plugged_in" +CONF_IS_POWERED = "is_powered" +CONF_IS_NOT_POWERED = "is_not_powered" +CONF_IS_PRESENT = "is_present" +CONF_IS_NOT_PRESENT = "is_not_present" +CONF_IS_PROBLEM = "is_problem" +CONF_IS_NO_PROBLEM = "is_no_problem" +CONF_IS_UNSAFE = "is_unsafe" +CONF_IS_NOT_UNSAFE = "is_not_unsafe" +CONF_IS_SMOKE = "is_smoke" +CONF_IS_NO_SMOKE = "is_no_smoke" +CONF_IS_SOUND = "is_sound" +CONF_IS_NO_SOUND = "is_no_sound" +CONF_IS_VIBRATION = "is_vibration" +CONF_IS_NO_VIBRATION = "is_no_vibration" +CONF_IS_OPEN = "is_open" +CONF_IS_NOT_OPEN = "is_not_open" + +IS_ON = [ + CONF_IS_BAT_LOW, + CONF_IS_COLD, + CONF_IS_CONNECTED, + CONF_IS_GAS, + CONF_IS_HOT, + CONF_IS_LIGHT, + CONF_IS_LOCKED, + CONF_IS_MOIST, + CONF_IS_MOTION, + CONF_IS_MOVING, + CONF_IS_OCCUPIED, + CONF_IS_OPEN, + CONF_IS_PLUGGED_IN, + CONF_IS_POWERED, + CONF_IS_PRESENT, + CONF_IS_PROBLEM, + CONF_IS_SMOKE, + CONF_IS_SOUND, + CONF_IS_UNSAFE, + CONF_IS_VIBRATION, + CONF_IS_ON, +] + +IS_OFF = [ + CONF_IS_NOT_BAT_LOW, + CONF_IS_NOT_COLD, + CONF_IS_NOT_CONNECTED, + CONF_IS_NOT_HOT, + CONF_IS_NOT_LOCKED, + CONF_IS_NOT_MOIST, + CONF_IS_NOT_MOVING, + CONF_IS_NOT_OCCUPIED, + CONF_IS_NOT_OPEN, + CONF_IS_NOT_PLUGGED_IN, + CONF_IS_NOT_POWERED, + CONF_IS_NOT_PRESENT, + CONF_IS_NOT_UNSAFE, + CONF_IS_NO_GAS, + CONF_IS_NO_LIGHT, + CONF_IS_NO_MOTION, + CONF_IS_NO_PROBLEM, + CONF_IS_NO_SMOKE, + CONF_IS_NO_SOUND, + CONF_IS_NO_VIBRATION, + CONF_IS_OFF, +] + +ENTITY_CONDITIONS = { + DEVICE_CLASS_BATTERY: [ + {CONF_TYPE: CONF_IS_BAT_LOW}, + {CONF_TYPE: CONF_IS_NOT_BAT_LOW}, + ], + DEVICE_CLASS_COLD: [{CONF_TYPE: CONF_IS_COLD}, {CONF_TYPE: CONF_IS_NOT_COLD}], + DEVICE_CLASS_CONNECTIVITY: [ + {CONF_TYPE: CONF_IS_CONNECTED}, + {CONF_TYPE: CONF_IS_NOT_CONNECTED}, + ], + DEVICE_CLASS_DOOR: [{CONF_TYPE: CONF_IS_OPEN}, {CONF_TYPE: CONF_IS_NOT_OPEN}], + DEVICE_CLASS_GARAGE_DOOR: [ + {CONF_TYPE: CONF_IS_OPEN}, + {CONF_TYPE: CONF_IS_NOT_OPEN}, + ], + DEVICE_CLASS_GAS: [{CONF_TYPE: CONF_IS_GAS}, {CONF_TYPE: CONF_IS_NO_GAS}], + DEVICE_CLASS_HEAT: [{CONF_TYPE: CONF_IS_HOT}, {CONF_TYPE: CONF_IS_NOT_HOT}], + DEVICE_CLASS_LIGHT: [{CONF_TYPE: CONF_IS_LIGHT}, {CONF_TYPE: CONF_IS_NO_LIGHT}], + DEVICE_CLASS_LOCK: [{CONF_TYPE: CONF_IS_LOCKED}, {CONF_TYPE: CONF_IS_NOT_LOCKED}], + DEVICE_CLASS_MOISTURE: [{CONF_TYPE: CONF_IS_MOIST}, {CONF_TYPE: CONF_IS_NOT_MOIST}], + DEVICE_CLASS_MOTION: [{CONF_TYPE: CONF_IS_MOTION}, {CONF_TYPE: CONF_IS_NO_MOTION}], + DEVICE_CLASS_MOVING: [{CONF_TYPE: CONF_IS_MOVING}, {CONF_TYPE: CONF_IS_NOT_MOVING}], + DEVICE_CLASS_OCCUPANCY: [ + {CONF_TYPE: CONF_IS_OCCUPIED}, + {CONF_TYPE: CONF_IS_NOT_OCCUPIED}, + ], + DEVICE_CLASS_OPENING: [{CONF_TYPE: CONF_IS_OPEN}, {CONF_TYPE: CONF_IS_NOT_OPEN}], + DEVICE_CLASS_PLUG: [ + {CONF_TYPE: CONF_IS_PLUGGED_IN}, + {CONF_TYPE: CONF_IS_NOT_PLUGGED_IN}, + ], + DEVICE_CLASS_POWER: [ + {CONF_TYPE: CONF_IS_POWERED}, + {CONF_TYPE: CONF_IS_NOT_POWERED}, + ], + DEVICE_CLASS_PRESENCE: [ + {CONF_TYPE: CONF_IS_PRESENT}, + {CONF_TYPE: CONF_IS_NOT_PRESENT}, + ], + DEVICE_CLASS_PROBLEM: [ + {CONF_TYPE: CONF_IS_PROBLEM}, + {CONF_TYPE: CONF_IS_NO_PROBLEM}, + ], + DEVICE_CLASS_SAFETY: [{CONF_TYPE: CONF_IS_UNSAFE}, {CONF_TYPE: CONF_IS_NOT_UNSAFE}], + DEVICE_CLASS_SMOKE: [{CONF_TYPE: CONF_IS_SMOKE}, {CONF_TYPE: CONF_IS_NO_SMOKE}], + DEVICE_CLASS_SOUND: [{CONF_TYPE: CONF_IS_SOUND}, {CONF_TYPE: CONF_IS_NO_SOUND}], + DEVICE_CLASS_VIBRATION: [ + {CONF_TYPE: CONF_IS_VIBRATION}, + {CONF_TYPE: CONF_IS_NO_VIBRATION}, + ], + DEVICE_CLASS_WINDOW: [{CONF_TYPE: CONF_IS_OPEN}, {CONF_TYPE: CONF_IS_NOT_OPEN}], + DEVICE_CLASS_NONE: [{CONF_TYPE: CONF_IS_ON}, {CONF_TYPE: CONF_IS_OFF}], +} + +CONDITION_SCHEMA = cv.DEVICE_CONDITION_BASE_SCHEMA.extend( + { + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_TYPE): vol.In(IS_OFF + IS_ON), + } +) + + +async def async_get_conditions(hass: HomeAssistant, device_id: str) -> List[dict]: + """List device conditions.""" + conditions: List[dict] = [] + entity_registry = await async_get_registry(hass) + entries = [ + entry + for entry in async_entries_for_device(entity_registry, device_id) + if entry.domain == DOMAIN + ] + + for entry in entries: + device_class = DEVICE_CLASS_NONE + state = hass.states.get(entry.entity_id) + if state and ATTR_DEVICE_CLASS in state.attributes: + device_class = state.attributes[ATTR_DEVICE_CLASS] + + templates = ENTITY_CONDITIONS.get( + device_class, ENTITY_CONDITIONS[DEVICE_CLASS_NONE] + ) + + conditions.extend( + ( + { + **template, + "condition": "device", + "device_id": device_id, + "entity_id": entry.entity_id, + "domain": DOMAIN, + } + for template in templates + ) + ) + + return conditions + + +def async_condition_from_config( + config: ConfigType, config_validation: bool +) -> condition.ConditionCheckerType: + """Evaluate state based on configuration.""" + config = CONDITION_SCHEMA(config) + condition_type = config[CONF_TYPE] + if condition_type in IS_ON: + stat = "on" + else: + stat = "off" + state_config = { + condition.CONF_CONDITION: "state", + condition.CONF_ENTITY_ID: config[CONF_ENTITY_ID], + condition.CONF_STATE: stat, + } + + return condition.state_from_config(state_config, config_validation) diff --git a/homeassistant/components/binary_sensor/device_trigger.py b/homeassistant/components/binary_sensor/device_trigger.py new file mode 100644 index 00000000000..2211b300104 --- /dev/null +++ b/homeassistant/components/binary_sensor/device_trigger.py @@ -0,0 +1,238 @@ +"""Provides device triggers for binary sensors.""" +import voluptuous as vol + +from homeassistant.components.automation import state as state_automation +from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA +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.helpers.entity_registry import async_entries_for_device +from homeassistant.helpers import config_validation as cv + +from . import ( + DOMAIN, + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_COLD, + DEVICE_CLASS_CONNECTIVITY, + DEVICE_CLASS_DOOR, + DEVICE_CLASS_GARAGE_DOOR, + DEVICE_CLASS_GAS, + DEVICE_CLASS_HEAT, + DEVICE_CLASS_LIGHT, + DEVICE_CLASS_LOCK, + DEVICE_CLASS_MOISTURE, + DEVICE_CLASS_MOTION, + DEVICE_CLASS_MOVING, + DEVICE_CLASS_OCCUPANCY, + DEVICE_CLASS_OPENING, + DEVICE_CLASS_PLUG, + DEVICE_CLASS_POWER, + DEVICE_CLASS_PRESENCE, + DEVICE_CLASS_PROBLEM, + DEVICE_CLASS_SAFETY, + DEVICE_CLASS_SMOKE, + DEVICE_CLASS_SOUND, + DEVICE_CLASS_VIBRATION, + DEVICE_CLASS_WINDOW, +) + + +# mypy: allow-untyped-defs, no-check-untyped-defs + +DEVICE_CLASS_NONE = "none" + +CONF_BAT_LOW = "bat_low" +CONF_NOT_BAT_LOW = "not_bat_low" +CONF_COLD = "cold" +CONF_NOT_COLD = "not_cold" +CONF_CONNECTED = "connected" +CONF_NOT_CONNECTED = "not_connected" +CONF_GAS = "gas" +CONF_NO_GAS = "no_gas" +CONF_HOT = "hot" +CONF_NOT_HOT = "not_hot" +CONF_LIGHT = "light" +CONF_NO_LIGHT = "no_light" +CONF_LOCKED = "locked" +CONF_NOT_LOCKED = "not_locked" +CONF_MOIST = "moist" +CONF_NOT_MOIST = "not_moist" +CONF_MOTION = "motion" +CONF_NO_MOTION = "no_motion" +CONF_MOVING = "moving" +CONF_NOT_MOVING = "not_moving" +CONF_OCCUPIED = "occupied" +CONF_NOT_OCCUPIED = "not_occupied" +CONF_PLUGGED_IN = "plugged_in" +CONF_NOT_PLUGGED_IN = "not_plugged_in" +CONF_POWERED = "powered" +CONF_NOT_POWERED = "not_powered" +CONF_PRESENT = "present" +CONF_NOT_PRESENT = "not_present" +CONF_PROBLEM = "problem" +CONF_NO_PROBLEM = "no_problem" +CONF_UNSAFE = "unsafe" +CONF_NOT_UNSAFE = "not_unsafe" +CONF_SMOKE = "smoke" +CONF_NO_SMOKE = "no_smoke" +CONF_SOUND = "sound" +CONF_NO_SOUND = "no_sound" +CONF_VIBRATION = "vibration" +CONF_NO_VIBRATION = "no_vibration" +CONF_OPEN = "open" +CONF_NOT_OPEN = "not_open" + + +TURNED_ON = [ + CONF_BAT_LOW, + CONF_COLD, + CONF_CONNECTED, + CONF_GAS, + CONF_HOT, + CONF_LIGHT, + CONF_LOCKED, + CONF_MOIST, + CONF_MOTION, + CONF_MOVING, + CONF_OCCUPIED, + CONF_OPEN, + CONF_PLUGGED_IN, + CONF_POWERED, + CONF_PRESENT, + CONF_PROBLEM, + CONF_SMOKE, + CONF_SOUND, + CONF_UNSAFE, + CONF_VIBRATION, + CONF_TURNED_ON, +] + +TURNED_OFF = [ + CONF_NOT_BAT_LOW, + CONF_NOT_COLD, + CONF_NOT_CONNECTED, + CONF_NOT_HOT, + CONF_NOT_LOCKED, + CONF_NOT_MOIST, + CONF_NOT_MOVING, + CONF_NOT_OCCUPIED, + CONF_NOT_OPEN, + CONF_NOT_PLUGGED_IN, + CONF_NOT_POWERED, + CONF_NOT_PRESENT, + CONF_NOT_UNSAFE, + CONF_NO_GAS, + CONF_NO_LIGHT, + CONF_NO_MOTION, + CONF_NO_PROBLEM, + CONF_NO_SMOKE, + CONF_NO_SOUND, + CONF_NO_VIBRATION, + CONF_TURNED_OFF, +] + + +ENTITY_TRIGGERS = { + DEVICE_CLASS_BATTERY: [{CONF_TYPE: CONF_BAT_LOW}, {CONF_TYPE: CONF_NOT_BAT_LOW}], + DEVICE_CLASS_COLD: [{CONF_TYPE: CONF_COLD}, {CONF_TYPE: CONF_NOT_COLD}], + DEVICE_CLASS_CONNECTIVITY: [ + {CONF_TYPE: CONF_CONNECTED}, + {CONF_TYPE: CONF_NOT_CONNECTED}, + ], + DEVICE_CLASS_DOOR: [{CONF_TYPE: CONF_OPEN}, {CONF_TYPE: CONF_NOT_OPEN}], + DEVICE_CLASS_GARAGE_DOOR: [{CONF_TYPE: CONF_OPEN}, {CONF_TYPE: CONF_NOT_OPEN}], + DEVICE_CLASS_GAS: [{CONF_TYPE: CONF_GAS}, {CONF_TYPE: CONF_NO_GAS}], + DEVICE_CLASS_HEAT: [{CONF_TYPE: CONF_HOT}, {CONF_TYPE: CONF_NOT_HOT}], + DEVICE_CLASS_LIGHT: [{CONF_TYPE: CONF_LIGHT}, {CONF_TYPE: CONF_NO_LIGHT}], + DEVICE_CLASS_LOCK: [{CONF_TYPE: CONF_LOCKED}, {CONF_TYPE: CONF_NOT_LOCKED}], + DEVICE_CLASS_MOISTURE: [{CONF_TYPE: CONF_MOIST}, {CONF_TYPE: CONF_NOT_MOIST}], + DEVICE_CLASS_MOTION: [{CONF_TYPE: CONF_MOTION}, {CONF_TYPE: CONF_NO_MOTION}], + DEVICE_CLASS_MOVING: [{CONF_TYPE: CONF_MOVING}, {CONF_TYPE: CONF_NOT_MOVING}], + DEVICE_CLASS_OCCUPANCY: [ + {CONF_TYPE: CONF_OCCUPIED}, + {CONF_TYPE: CONF_NOT_OCCUPIED}, + ], + DEVICE_CLASS_OPENING: [{CONF_TYPE: CONF_OPEN}, {CONF_TYPE: CONF_NOT_OPEN}], + DEVICE_CLASS_PLUG: [{CONF_TYPE: CONF_PLUGGED_IN}, {CONF_TYPE: CONF_NOT_PLUGGED_IN}], + DEVICE_CLASS_POWER: [{CONF_TYPE: CONF_POWERED}, {CONF_TYPE: CONF_NOT_POWERED}], + DEVICE_CLASS_PRESENCE: [{CONF_TYPE: CONF_PRESENT}, {CONF_TYPE: CONF_NOT_PRESENT}], + DEVICE_CLASS_PROBLEM: [{CONF_TYPE: CONF_PROBLEM}, {CONF_TYPE: CONF_NO_PROBLEM}], + DEVICE_CLASS_SAFETY: [{CONF_TYPE: CONF_UNSAFE}, {CONF_TYPE: CONF_NOT_UNSAFE}], + DEVICE_CLASS_SMOKE: [{CONF_TYPE: CONF_SMOKE}, {CONF_TYPE: CONF_NO_SMOKE}], + DEVICE_CLASS_SOUND: [{CONF_TYPE: CONF_SOUND}, {CONF_TYPE: CONF_NO_SOUND}], + DEVICE_CLASS_VIBRATION: [ + {CONF_TYPE: CONF_VIBRATION}, + {CONF_TYPE: CONF_NO_VIBRATION}, + ], + DEVICE_CLASS_WINDOW: [{CONF_TYPE: CONF_OPEN}, {CONF_TYPE: CONF_NOT_OPEN}], + DEVICE_CLASS_NONE: [{CONF_TYPE: CONF_TURNED_ON}, {CONF_TYPE: CONF_TURNED_OFF}], +} + + +TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend( + { + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_TYPE): vol.In(TURNED_OFF + TURNED_ON), + } +) + + +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" + to_state = "on" + else: + from_state = "on" + to_state = "off" + + state_config = { + state_automation.CONF_ENTITY_ID: config[CONF_ENTITY_ID], + state_automation.CONF_FROM: from_state, + state_automation.CONF_TO: to_state, + } + + return await state_automation.async_attach_trigger( + hass, state_config, action, automation_info, platform_type="device" + ) + + +async def async_get_triggers(hass, device_id): + """List device triggers.""" + triggers = [] + entity_registry = await hass.helpers.entity_registry.async_get_registry() + + entries = [ + entry + for entry in async_entries_for_device(entity_registry, device_id) + if entry.domain == DOMAIN + ] + + for entry in entries: + device_class = None + state = hass.states.get(entry.entity_id) + if state: + device_class = state.attributes.get(ATTR_DEVICE_CLASS) + + templates = ENTITY_TRIGGERS.get( + device_class, ENTITY_TRIGGERS[DEVICE_CLASS_NONE] + ) + + triggers.extend( + ( + { + **automation, + "platform": "device", + "device_id": device_id, + "entity_id": entry.entity_id, + "domain": DOMAIN, + } + for automation in templates + ) + ) + + return triggers diff --git a/homeassistant/components/deconz/device_automation.py b/homeassistant/components/deconz/device_trigger.py similarity index 94% rename from homeassistant/components/deconz/device_automation.py rename to homeassistant/components/deconz/device_trigger.py index 28f36b8f431..77efc78562a 100644 --- a/homeassistant/components/deconz/device_automation.py +++ b/homeassistant/components/deconz/device_trigger.py @@ -3,6 +3,7 @@ import voluptuous as vol import homeassistant.components.automation.event as event +from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA from homeassistant.components.device_automation.exceptions import ( InvalidDeviceAutomationConfig, ) @@ -171,16 +172,8 @@ REMOTES = { AQARA_SQUARE_SWITCH_MODEL: AQARA_SQUARE_SWITCH, } -TRIGGER_SCHEMA = vol.All( - vol.Schema( - { - vol.Required(CONF_DEVICE_ID): str, - vol.Required(CONF_DOMAIN): DOMAIN, - vol.Required(CONF_PLATFORM): "device", - vol.Required(CONF_TYPE): str, - vol.Required(CONF_SUBTYPE): str, - } - ) +TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend( + {vol.Required(CONF_TYPE): str, vol.Required(CONF_SUBTYPE): str} ) @@ -198,7 +191,7 @@ def _get_deconz_event_from_device_id(hass, device_id): return None -async def async_trigger(hass, config, action, automation_info): +async def async_attach_trigger(hass, config, action, automation_info): """Listen for state changes based on configuration.""" config = TRIGGER_SCHEMA(config) @@ -223,7 +216,9 @@ async def async_trigger(hass, config, action, automation_info): event.CONF_EVENT_DATA: {CONF_UNIQUE_ID: event_id, CONF_EVENT: trigger}, } - return await event.async_trigger(hass, state_config, action, automation_info) + return await event.async_attach_trigger( + hass, state_config, action, automation_info, platform_type="device" + ) async def async_get_triggers(hass, device_id): diff --git a/homeassistant/components/device_automation/__init__.py b/homeassistant/components/device_automation/__init__.py index 9508dd9c849..b444abd5238 100644 --- a/homeassistant/components/device_automation/__init__.py +++ b/homeassistant/components/device_automation/__init__.py @@ -1,16 +1,12 @@ """Helpers for device automations.""" import asyncio import logging -from typing import Callable, cast import voluptuous as vol +from homeassistant.const import CONF_PLATFORM, CONF_DOMAIN, CONF_DEVICE_ID from homeassistant.components import websocket_api -from homeassistant.const import CONF_DOMAIN -from homeassistant.core import split_entity_id, HomeAssistant -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_registry import async_entries_for_device -from homeassistant.helpers.typing import ConfigType from homeassistant.loader import async_get_integration, IntegrationNotFound DOMAIN = "device_automation" @@ -18,6 +14,21 @@ DOMAIN = "device_automation" _LOGGER = logging.getLogger(__name__) +TRIGGER_BASE_SCHEMA = vol.Schema( + { + vol.Required(CONF_PLATFORM): "device", + vol.Required(CONF_DOMAIN): str, + vol.Required(CONF_DEVICE_ID): str, + } +) + +TYPES = { + "trigger": ("device_trigger", "async_get_triggers"), + "condition": ("device_condition", "async_get_conditions"), + "action": ("device_action", "async_get_actions"), +} + + async def async_setup(hass, config): """Set up device automation.""" hass.components.websocket_api.async_register_command( @@ -32,21 +43,9 @@ async def async_setup(hass, config): return True -async def async_device_condition_from_config( - hass: HomeAssistant, config: ConfigType, config_validation: bool = True -) -> Callable[..., bool]: - """Wrap action method with state based condition.""" - if config_validation: - config = cv.DEVICE_CONDITION_SCHEMA(config) - integration = await async_get_integration(hass, config[CONF_DOMAIN]) - platform = integration.get_platform("device_automation") - return cast( - Callable[..., bool], - platform.async_condition_from_config(config, config_validation), # type: ignore - ) - - -async def _async_get_device_automations_from_domain(hass, domain, fname, device_id): +async def _async_get_device_automations_from_domain( + hass, domain, automation_type, device_id +): """List device automations.""" integration = None try: @@ -55,17 +54,18 @@ async def _async_get_device_automations_from_domain(hass, domain, fname, device_ _LOGGER.warning("Integration %s not found", domain) return None + platform_name, function_name = TYPES[automation_type] + try: - platform = integration.get_platform("device_automation") + platform = integration.get_platform(platform_name) except ImportError: # The domain does not have device automations return None - if hasattr(platform, fname): - return await getattr(platform, fname)(hass, device_id) + return await getattr(platform, function_name)(hass, device_id) -async def _async_get_device_automations(hass, fname, device_id): +async def _async_get_device_automations(hass, automation_type, device_id): """List device automations.""" device_registry, entity_registry = await asyncio.gather( hass.helpers.device_registry.async_get_registry(), @@ -79,13 +79,15 @@ async def _async_get_device_automations(hass, fname, device_id): config_entry = hass.config_entries.async_get_entry(entry_id) domains.add(config_entry.domain) - entities = async_entries_for_device(entity_registry, device_id) - for entity in entities: - domains.add(split_entity_id(entity.entity_id)[0]) + entity_entries = async_entries_for_device(entity_registry, device_id) + for entity_entry in entity_entries: + domains.add(entity_entry.domain) device_automations = await asyncio.gather( *( - _async_get_device_automations_from_domain(hass, domain, fname, device_id) + _async_get_device_automations_from_domain( + hass, domain, automation_type, device_id + ) for domain in domains ) ) @@ -106,7 +108,7 @@ async def _async_get_device_automations(hass, fname, device_id): async def websocket_device_automation_list_actions(hass, connection, msg): """Handle request for device actions.""" device_id = msg["device_id"] - actions = await _async_get_device_automations(hass, "async_get_actions", device_id) + actions = await _async_get_device_automations(hass, "action", device_id) connection.send_result(msg["id"], actions) @@ -120,9 +122,7 @@ async def websocket_device_automation_list_actions(hass, connection, msg): async def websocket_device_automation_list_conditions(hass, connection, msg): """Handle request for device conditions.""" device_id = msg["device_id"] - conditions = await _async_get_device_automations( - hass, "async_get_conditions", device_id - ) + conditions = await _async_get_device_automations(hass, "condition", device_id) connection.send_result(msg["id"], conditions) @@ -136,7 +136,5 @@ async def websocket_device_automation_list_conditions(hass, connection, msg): async def websocket_device_automation_list_triggers(hass, connection, msg): """Handle request for device triggers.""" device_id = msg["device_id"] - triggers = await _async_get_device_automations( - hass, "async_get_triggers", device_id - ) + triggers = await _async_get_device_automations(hass, "trigger", device_id) connection.send_result(msg["id"], triggers) diff --git a/homeassistant/components/device_automation/toggle_entity.py b/homeassistant/components/device_automation/toggle_entity.py index 1593e70771a..b7cadd1349a 100644 --- a/homeassistant/components/device_automation/toggle_entity.py +++ b/homeassistant/components/device_automation/toggle_entity.py @@ -1,7 +1,9 @@ """Device automation helpers for toggle entity.""" +from typing import List import voluptuous as vol -import homeassistant.components.automation.state as state +from homeassistant.core import Context, HomeAssistant, CALLBACK_TYPE +from homeassistant.components.automation import state, AutomationActionType from homeassistant.components.device_automation.const import ( CONF_IS_OFF, CONF_IS_ON, @@ -11,17 +13,11 @@ from homeassistant.components.device_automation.const import ( CONF_TURNED_OFF, CONF_TURNED_ON, ) -from homeassistant.core import split_entity_id -from homeassistant.const import ( - CONF_CONDITION, - CONF_DEVICE_ID, - CONF_DOMAIN, - CONF_ENTITY_ID, - CONF_PLATFORM, - CONF_TYPE, -) +from homeassistant.const import CONF_CONDITION, CONF_ENTITY_ID, 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 +from . import TRIGGER_BASE_SCHEMA ENTITY_ACTIONS = [ { @@ -64,41 +60,35 @@ ENTITY_TRIGGERS = [ }, ] -ACTION_SCHEMA = vol.Schema( +ACTION_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend( { - vol.Required(CONF_DEVICE_ID): str, - vol.Required(CONF_DOMAIN): str, vol.Required(CONF_ENTITY_ID): cv.entity_id, vol.Required(CONF_TYPE): vol.In([CONF_TOGGLE, CONF_TURN_OFF, CONF_TURN_ON]), } ) -CONDITION_SCHEMA = vol.Schema( +CONDITION_SCHEMA = cv.DEVICE_CONDITION_BASE_SCHEMA.extend( { - vol.Required(CONF_CONDITION): "device", - vol.Required(CONF_DEVICE_ID): str, - vol.Required(CONF_DOMAIN): str, vol.Required(CONF_ENTITY_ID): cv.entity_id, vol.Required(CONF_TYPE): vol.In([CONF_IS_OFF, CONF_IS_ON]), } ) -TRIGGER_SCHEMA = vol.Schema( +TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend( { - vol.Required(CONF_PLATFORM): "device", - vol.Required(CONF_DEVICE_ID): str, - vol.Required(CONF_DOMAIN): str, vol.Required(CONF_ENTITY_ID): cv.entity_id, vol.Required(CONF_TYPE): vol.In([CONF_TURNED_OFF, CONF_TURNED_ON]), } ) -def _is_domain(entity, domain): - return split_entity_id(entity.entity_id)[0] == domain - - -async def async_call_action_from_config(hass, config, variables, context, domain): +async def async_call_action_from_config( + hass: HomeAssistant, + config: ConfigType, + variables: TemplateVarsType, + context: Context, + domain: str, +): """Change state based on configuration.""" config = ACTION_SCHEMA(config) action_type = config[CONF_TYPE] @@ -119,7 +109,9 @@ async def async_call_action_from_config(hass, config, variables, context, domain ) -def async_condition_from_config(config, config_validation): +def async_condition_from_config( + config: ConfigType, config_validation: bool +) -> condition.ConditionCheckerType: """Evaluate state based on configuration.""" condition_type = config[CONF_TYPE] if condition_type == CONF_IS_ON: @@ -135,7 +127,12 @@ def async_condition_from_config(config, config_validation): return condition.state_from_config(state_config, config_validation) -async def async_attach_trigger(hass, config, action, automation_info): +async def async_attach_trigger( + hass: HomeAssistant, + config: ConfigType, + action: AutomationActionType, + automation_info: dict, +) -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" trigger_type = config[CONF_TYPE] if trigger_type == CONF_TURNED_ON: @@ -150,37 +147,56 @@ async def async_attach_trigger(hass, config, action, automation_info): state.CONF_TO: to_state, } - return await state.async_trigger(hass, state_config, action, automation_info) + return await state.async_attach_trigger( + hass, state_config, action, automation_info, platform_type="device" + ) -async def _async_get_automations(hass, device_id, automation_templates, domain): +async def _async_get_automations( + hass: HomeAssistant, device_id: str, automation_templates: List[dict], domain: str +) -> List[dict]: """List device automations.""" automations = [] entity_registry = await hass.helpers.entity_registry.async_get_registry() - entities = async_entries_for_device(entity_registry, device_id) - domain_entities = [x for x in entities if _is_domain(x, domain)] - for entity in domain_entities: - for automation in automation_templates: - automation = dict(automation) - automation.update( - device_id=device_id, entity_id=entity.entity_id, domain=domain + entries = [ + entry + for entry in async_entries_for_device(entity_registry, device_id) + if entry.domain == domain + ] + + for entry in entries: + automations.extend( + ( + { + **template, + "device_id": device_id, + "entity_id": entry.entity_id, + "domain": domain, + } + for template in automation_templates ) - automations.append(automation) + ) return automations -async def async_get_actions(hass, device_id, domain): +async def async_get_actions( + hass: HomeAssistant, device_id: str, domain: str +) -> List[dict]: """List device actions.""" return await _async_get_automations(hass, device_id, ENTITY_ACTIONS, domain) -async def async_get_conditions(hass, device_id, domain): +async def async_get_conditions( + hass: HomeAssistant, device_id: str, domain: str +) -> List[dict]: """List device conditions.""" return await _async_get_automations(hass, device_id, ENTITY_CONDITIONS, domain) -async def async_get_triggers(hass, device_id, domain): +async def async_get_triggers( + hass: HomeAssistant, device_id: str, domain: str +) -> List[dict]: """List device triggers.""" return await _async_get_automations(hass, device_id, ENTITY_TRIGGERS, domain) diff --git a/homeassistant/components/light/device_action.py b/homeassistant/components/light/device_action.py new file mode 100644 index 00000000000..ea37b8e9470 --- /dev/null +++ b/homeassistant/components/light/device_action.py @@ -0,0 +1,30 @@ +"""Provides device actions for lights.""" +from typing import List +import voluptuous as vol + +from homeassistant.core import HomeAssistant, Context +from homeassistant.components.device_automation import toggle_entity +from homeassistant.const import CONF_DOMAIN +from homeassistant.helpers.typing import TemplateVarsType, ConfigType +from . import DOMAIN + + +ACTION_SCHEMA = toggle_entity.ACTION_SCHEMA.extend({vol.Required(CONF_DOMAIN): DOMAIN}) + + +async def async_call_action_from_config( + hass: HomeAssistant, + config: ConfigType, + variables: TemplateVarsType, + 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 + ) + + +async def async_get_actions(hass: HomeAssistant, device_id: str) -> List[dict]: + """List device actions.""" + return await toggle_entity.async_get_actions(hass, device_id, DOMAIN) diff --git a/homeassistant/components/light/device_automation.py b/homeassistant/components/light/device_automation.py deleted file mode 100644 index 61292d47449..00000000000 --- a/homeassistant/components/light/device_automation.py +++ /dev/null @@ -1,56 +0,0 @@ -"""Provides device automations for lights.""" -import voluptuous as vol - -from homeassistant.components.device_automation import toggle_entity -from homeassistant.const import CONF_DOMAIN -from . import DOMAIN - - -# mypy: allow-untyped-defs, no-check-untyped-defs - -ACTION_SCHEMA = toggle_entity.ACTION_SCHEMA.extend({vol.Required(CONF_DOMAIN): DOMAIN}) - -CONDITION_SCHEMA = toggle_entity.CONDITION_SCHEMA.extend( - {vol.Required(CONF_DOMAIN): DOMAIN} -) - -TRIGGER_SCHEMA = toggle_entity.TRIGGER_SCHEMA.extend( - {vol.Required(CONF_DOMAIN): DOMAIN} -) - - -async def async_call_action_from_config(hass, config, variables, context): - """Change state based on configuration.""" - config = ACTION_SCHEMA(config) - await toggle_entity.async_call_action_from_config( - hass, config, variables, context, DOMAIN - ) - - -def async_condition_from_config(config, config_validation): - """Evaluate state based on configuration.""" - config = CONDITION_SCHEMA(config) - return toggle_entity.async_condition_from_config(config, config_validation) - - -async def async_trigger(hass, config, action, automation_info): - """Listen for state changes based on configuration.""" - config = TRIGGER_SCHEMA(config) - return await toggle_entity.async_attach_trigger( - hass, config, action, automation_info - ) - - -async def async_get_actions(hass, device_id): - """List device actions.""" - return await toggle_entity.async_get_actions(hass, device_id, DOMAIN) - - -async def async_get_conditions(hass, device_id): - """List device conditions.""" - return await toggle_entity.async_get_conditions(hass, device_id, DOMAIN) - - -async def async_get_triggers(hass, device_id): - """List device triggers.""" - return await toggle_entity.async_get_triggers(hass, device_id, DOMAIN) diff --git a/homeassistant/components/light/device_condition.py b/homeassistant/components/light/device_condition.py new file mode 100644 index 00000000000..a69ca7ab8f2 --- /dev/null +++ b/homeassistant/components/light/device_condition.py @@ -0,0 +1,28 @@ +"""Provides device conditions for lights.""" +from typing import List +import voluptuous as vol + +from homeassistant.core import HomeAssistant +from homeassistant.components.device_automation import toggle_entity +from homeassistant.const import CONF_DOMAIN +from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.condition import ConditionCheckerType +from . import DOMAIN + + +CONDITION_SCHEMA = toggle_entity.CONDITION_SCHEMA.extend( + {vol.Required(CONF_DOMAIN): DOMAIN} +) + + +def async_condition_from_config( + config: ConfigType, config_validation: bool +) -> ConditionCheckerType: + """Evaluate state based on configuration.""" + config = CONDITION_SCHEMA(config) + return toggle_entity.async_condition_from_config(config, config_validation) + + +async def async_get_conditions(hass: HomeAssistant, device_id: str) -> List[dict]: + """List device conditions.""" + return await toggle_entity.async_get_conditions(hass, device_id, DOMAIN) diff --git a/homeassistant/components/light/device_trigger.py b/homeassistant/components/light/device_trigger.py new file mode 100644 index 00000000000..f2a82afdc2d --- /dev/null +++ b/homeassistant/components/light/device_trigger.py @@ -0,0 +1,33 @@ +"""Provides device trigger for lights.""" +from typing import List +import voluptuous as vol + +from homeassistant.core import HomeAssistant, CALLBACK_TYPE +from homeassistant.components.automation import AutomationActionType +from homeassistant.components.device_automation import toggle_entity +from homeassistant.const import CONF_DOMAIN +from homeassistant.helpers.typing import ConfigType +from . import DOMAIN + + +TRIGGER_SCHEMA = toggle_entity.TRIGGER_SCHEMA.extend( + {vol.Required(CONF_DOMAIN): DOMAIN} +) + + +async def async_attach_trigger( + hass: HomeAssistant, + config: ConfigType, + action: AutomationActionType, + 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 + ) + + +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) diff --git a/homeassistant/components/switch/device_action.py b/homeassistant/components/switch/device_action.py new file mode 100644 index 00000000000..ca91cc70512 --- /dev/null +++ b/homeassistant/components/switch/device_action.py @@ -0,0 +1,30 @@ +"""Provides device actions for switches.""" +from typing import List +import voluptuous as vol + +from homeassistant.core import HomeAssistant, Context +from homeassistant.components.device_automation import toggle_entity +from homeassistant.const import CONF_DOMAIN +from homeassistant.helpers.typing import TemplateVarsType, ConfigType +from . import DOMAIN + + +ACTION_SCHEMA = toggle_entity.ACTION_SCHEMA.extend({vol.Required(CONF_DOMAIN): DOMAIN}) + + +async def async_call_action_from_config( + hass: HomeAssistant, + config: ConfigType, + variables: TemplateVarsType, + 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 + ) + + +async def async_get_actions(hass: HomeAssistant, device_id: str) -> List[dict]: + """List device actions.""" + return await toggle_entity.async_get_actions(hass, device_id, DOMAIN) diff --git a/homeassistant/components/switch/device_automation.py b/homeassistant/components/switch/device_automation.py deleted file mode 100644 index 61292d47449..00000000000 --- a/homeassistant/components/switch/device_automation.py +++ /dev/null @@ -1,56 +0,0 @@ -"""Provides device automations for lights.""" -import voluptuous as vol - -from homeassistant.components.device_automation import toggle_entity -from homeassistant.const import CONF_DOMAIN -from . import DOMAIN - - -# mypy: allow-untyped-defs, no-check-untyped-defs - -ACTION_SCHEMA = toggle_entity.ACTION_SCHEMA.extend({vol.Required(CONF_DOMAIN): DOMAIN}) - -CONDITION_SCHEMA = toggle_entity.CONDITION_SCHEMA.extend( - {vol.Required(CONF_DOMAIN): DOMAIN} -) - -TRIGGER_SCHEMA = toggle_entity.TRIGGER_SCHEMA.extend( - {vol.Required(CONF_DOMAIN): DOMAIN} -) - - -async def async_call_action_from_config(hass, config, variables, context): - """Change state based on configuration.""" - config = ACTION_SCHEMA(config) - await toggle_entity.async_call_action_from_config( - hass, config, variables, context, DOMAIN - ) - - -def async_condition_from_config(config, config_validation): - """Evaluate state based on configuration.""" - config = CONDITION_SCHEMA(config) - return toggle_entity.async_condition_from_config(config, config_validation) - - -async def async_trigger(hass, config, action, automation_info): - """Listen for state changes based on configuration.""" - config = TRIGGER_SCHEMA(config) - return await toggle_entity.async_attach_trigger( - hass, config, action, automation_info - ) - - -async def async_get_actions(hass, device_id): - """List device actions.""" - return await toggle_entity.async_get_actions(hass, device_id, DOMAIN) - - -async def async_get_conditions(hass, device_id): - """List device conditions.""" - return await toggle_entity.async_get_conditions(hass, device_id, DOMAIN) - - -async def async_get_triggers(hass, device_id): - """List device triggers.""" - return await toggle_entity.async_get_triggers(hass, device_id, DOMAIN) diff --git a/homeassistant/components/switch/device_condition.py b/homeassistant/components/switch/device_condition.py new file mode 100644 index 00000000000..032c765bf59 --- /dev/null +++ b/homeassistant/components/switch/device_condition.py @@ -0,0 +1,28 @@ +"""Provides device conditions for switches.""" +from typing import List +import voluptuous as vol + +from homeassistant.core import HomeAssistant +from homeassistant.components.device_automation import toggle_entity +from homeassistant.const import CONF_DOMAIN +from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.condition import ConditionCheckerType +from . import DOMAIN + + +CONDITION_SCHEMA = toggle_entity.CONDITION_SCHEMA.extend( + {vol.Required(CONF_DOMAIN): DOMAIN} +) + + +def async_condition_from_config( + config: ConfigType, config_validation: bool +) -> ConditionCheckerType: + """Evaluate state based on configuration.""" + config = CONDITION_SCHEMA(config) + return toggle_entity.async_condition_from_config(config, config_validation) + + +async def async_get_conditions(hass: HomeAssistant, device_id: str) -> List[dict]: + """List device conditions.""" + return await toggle_entity.async_get_conditions(hass, device_id, DOMAIN) diff --git a/homeassistant/components/switch/device_trigger.py b/homeassistant/components/switch/device_trigger.py new file mode 100644 index 00000000000..9be294d5460 --- /dev/null +++ b/homeassistant/components/switch/device_trigger.py @@ -0,0 +1,33 @@ +"""Provides device triggers for switches.""" +from typing import List +import voluptuous as vol + +from homeassistant.core import HomeAssistant, CALLBACK_TYPE +from homeassistant.components.automation import AutomationActionType +from homeassistant.components.device_automation import toggle_entity +from homeassistant.const import CONF_DOMAIN +from homeassistant.helpers.typing import ConfigType +from . import DOMAIN + + +TRIGGER_SCHEMA = toggle_entity.TRIGGER_SCHEMA.extend( + {vol.Required(CONF_DOMAIN): DOMAIN} +) + + +async def async_attach_trigger( + hass: HomeAssistant, + config: ConfigType, + action: AutomationActionType, + 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 + ) + + +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) diff --git a/homeassistant/components/zha/device_automation.py b/homeassistant/components/zha/device_trigger.py similarity index 84% rename from homeassistant/components/zha/device_automation.py rename to homeassistant/components/zha/device_trigger.py index 6a96ce5aa3e..46e3beafcae 100644 --- a/homeassistant/components/zha/device_automation.py +++ b/homeassistant/components/zha/device_trigger.py @@ -6,6 +6,7 @@ from homeassistant.components.device_automation.exceptions import ( InvalidDeviceAutomationConfig, ) from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE +from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA from . import DOMAIN from .core.const import DATA_ZHA, DATA_ZHA_GATEWAY @@ -16,20 +17,12 @@ DEVICE = "device" DEVICE_IEEE = "device_ieee" ZHA_EVENT = "zha_event" -TRIGGER_SCHEMA = vol.All( - vol.Schema( - { - vol.Required(CONF_DEVICE_ID): str, - vol.Required(CONF_DOMAIN): DOMAIN, - vol.Required(CONF_PLATFORM): DEVICE, - vol.Required(CONF_TYPE): str, - vol.Required(CONF_SUBTYPE): str, - } - ) +TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend( + {vol.Required(CONF_TYPE): str, vol.Required(CONF_SUBTYPE): str} ) -async def async_trigger(hass, config, action, automation_info): +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]) @@ -48,7 +41,9 @@ async def async_trigger(hass, config, action, automation_info): event.CONF_EVENT_DATA: {DEVICE_IEEE: str(zha_device.ieee), **trigger}, } - return await event.async_trigger(hass, state_config, action, automation_info) + return await event.async_attach_trigger( + hass, state_config, action, automation_info, platform_type="device" + ) async def async_get_triggers(hass, device_id): diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py index 133251e779d..afb8c3934a7 100644 --- a/homeassistant/helpers/condition.py +++ b/homeassistant/helpers/condition.py @@ -8,16 +8,14 @@ from typing import Callable, Container, Optional, Union, cast from homeassistant.helpers.template import Template from homeassistant.helpers.typing import ConfigType, TemplateVarsType - +from homeassistant.loader import async_get_integration from homeassistant.core import HomeAssistant, State from homeassistant.components import zone as zone_cmp -from homeassistant.components.device_automation import ( # noqa: F401 pylint: disable=unused-import - async_device_condition_from_config as async_device_from_config, -) from homeassistant.const import ( ATTR_GPS_ACCURACY, ATTR_LATITUDE, ATTR_LONGITUDE, + CONF_DOMAIN, CONF_ENTITY_ID, CONF_VALUE_TEMPLATE, CONF_CONDITION, @@ -45,10 +43,12 @@ ASYNC_FROM_CONFIG_FORMAT = "async_{}_from_config" _LOGGER = logging.getLogger(__name__) +ConditionCheckerType = Callable[[HomeAssistant, TemplateVarsType], bool] + async def async_from_config( hass: HomeAssistant, config: ConfigType, config_validation: bool = True -) -> Callable[..., bool]: +) -> ConditionCheckerType: """Turn a condition configuration into a method. Should be run on the event loop. @@ -74,13 +74,15 @@ async def async_from_config( check_factory = check_factory.func if asyncio.iscoroutinefunction(check_factory): - return cast(Callable[..., bool], await factory(hass, config, config_validation)) - return cast(Callable[..., bool], factory(config, config_validation)) + return cast( + ConditionCheckerType, await factory(hass, config, config_validation) + ) + return cast(ConditionCheckerType, factory(config, config_validation)) async def async_and_from_config( hass: HomeAssistant, config: ConfigType, config_validation: bool = True -) -> Callable[..., bool]: +) -> ConditionCheckerType: """Create multi condition matcher using 'AND'.""" if config_validation: config = cv.AND_CONDITION_SCHEMA(config) @@ -107,7 +109,7 @@ async def async_and_from_config( async def async_or_from_config( hass: HomeAssistant, config: ConfigType, config_validation: bool = True -) -> Callable[..., bool]: +) -> ConditionCheckerType: """Create multi condition matcher using 'OR'.""" if config_validation: config = cv.OR_CONDITION_SCHEMA(config) @@ -205,7 +207,7 @@ def async_numeric_state( def async_numeric_state_from_config( config: ConfigType, config_validation: bool = True -) -> Callable[..., bool]: +) -> ConditionCheckerType: """Wrap action method with state based condition.""" if config_validation: config = cv.NUMERIC_STATE_CONDITION_SCHEMA(config) @@ -255,7 +257,7 @@ def state( def state_from_config( config: ConfigType, config_validation: bool = True -) -> Callable[..., bool]: +) -> ConditionCheckerType: """Wrap action method with state based condition.""" if config_validation: config = cv.STATE_CONDITION_SCHEMA(config) @@ -327,7 +329,7 @@ def sun( def sun_from_config( config: ConfigType, config_validation: bool = True -) -> Callable[..., bool]: +) -> ConditionCheckerType: """Wrap action method with sun based condition.""" if config_validation: config = cv.SUN_CONDITION_SCHEMA(config) @@ -370,7 +372,7 @@ def async_template( def async_template_from_config( config: ConfigType, config_validation: bool = True -) -> Callable[..., bool]: +) -> ConditionCheckerType: """Wrap action method with state based condition.""" if config_validation: config = cv.TEMPLATE_CONDITION_SCHEMA(config) @@ -427,7 +429,7 @@ def time( def time_from_config( config: ConfigType, config_validation: bool = True -) -> Callable[..., bool]: +) -> ConditionCheckerType: """Wrap action method with time based condition.""" if config_validation: config = cv.TIME_CONDITION_SCHEMA(config) @@ -476,7 +478,7 @@ def zone( def zone_from_config( config: ConfigType, config_validation: bool = True -) -> Callable[..., bool]: +) -> ConditionCheckerType: """Wrap action method with zone based condition.""" if config_validation: config = cv.ZONE_CONDITION_SCHEMA(config) @@ -488,3 +490,17 @@ def zone_from_config( return zone(hass, zone_entity_id, entity_id) return if_in_zone + + +async def async_device_from_config( + hass: HomeAssistant, config: ConfigType, config_validation: bool = True +) -> ConditionCheckerType: + """Test a device condition.""" + if config_validation: + config = cv.DEVICE_CONDITION_SCHEMA(config) + integration = await async_get_integration(hass, config[CONF_DOMAIN]) + platform = integration.get_platform("device_condition") + return cast( + ConditionCheckerType, + platform.async_condition_from_config(config, config_validation), # type: ignore + ) diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 952fa41c42c..113f2437ce8 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -827,11 +827,16 @@ OR_CONDITION_SCHEMA = vol.Schema( } ) -DEVICE_CONDITION_SCHEMA = vol.Schema( - {vol.Required(CONF_CONDITION): "device", vol.Required(CONF_DOMAIN): str}, - extra=vol.ALLOW_EXTRA, +DEVICE_CONDITION_BASE_SCHEMA = vol.Schema( + { + vol.Required(CONF_CONDITION): "device", + vol.Required(CONF_DEVICE_ID): str, + vol.Required(CONF_DOMAIN): str, + } ) +DEVICE_CONDITION_SCHEMA = DEVICE_CONDITION_BASE_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA) + CONDITION_SCHEMA: vol.Schema = vol.Any( NUMERIC_STATE_CONDITION_SCHEMA, STATE_CONDITION_SCHEMA, @@ -862,11 +867,12 @@ _SCRIPT_WAIT_TEMPLATE_SCHEMA = vol.Schema( } ) -DEVICE_ACTION_SCHEMA = vol.Schema( - {vol.Required(CONF_DEVICE_ID): string, vol.Required(CONF_DOMAIN): str}, - extra=vol.ALLOW_EXTRA, +DEVICE_ACTION_BASE_SCHEMA = vol.Schema( + {vol.Required(CONF_DEVICE_ID): string, vol.Required(CONF_DOMAIN): str} ) +DEVICE_ACTION_SCHEMA = DEVICE_ACTION_BASE_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA) + SCRIPT_SCHEMA = vol.All( ensure_list, [ diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 23728b65109..14ff873d4d1 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -336,7 +336,7 @@ class Script: self.last_action = action.get(CONF_ALIAS, "device automation") self._log("Executing step %s" % self.last_action) integration = await async_get_integration(self.hass, action[CONF_DOMAIN]) - platform = integration.get_platform("device_automation") + platform = integration.get_platform("device_action") await platform.async_call_action_from_config( self.hass, action, variables, context ) diff --git a/tests/common.py b/tests/common.py index fda5c743222..bc39b1f5e0b 100644 --- a/tests/common.py +++ b/tests/common.py @@ -54,7 +54,9 @@ from homeassistant.helpers.json import JSONEncoder from homeassistant.setup import async_setup_component, setup_component from homeassistant.util.unit_system import METRIC_SYSTEM from homeassistant.util.async_ import run_callback_threadsafe, run_coroutine_threadsafe - +from homeassistant.components.device_automation import ( # noqa + _async_get_device_automations as async_get_device_automations, +) _TEST_INSTANCE_PORT = SERVER_PORT _LOGGER = logging.getLogger(__name__) diff --git a/tests/components/binary_sensor/test_device_automation.py b/tests/components/binary_sensor/test_device_automation.py deleted file mode 100644 index 91124d47f4e..00000000000 --- a/tests/components/binary_sensor/test_device_automation.py +++ /dev/null @@ -1,309 +0,0 @@ -"""The test for binary_sensor device automation.""" -import pytest - -from homeassistant.components.binary_sensor import DOMAIN, DEVICE_CLASSES -from homeassistant.components.binary_sensor.device_automation import ( - ENTITY_CONDITIONS, - ENTITY_TRIGGERS, -) -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.components.device_automation import ( - _async_get_device_automations as async_get_device_automations, -) -from homeassistant.helpers import device_registry - -from tests.common import ( - MockConfigEntry, - async_mock_service, - mock_device_registry, - mock_registry, -) - - -@pytest.fixture -def device_reg(hass): - """Return an empty, loaded, registry.""" - return mock_device_registry(hass) - - -@pytest.fixture -def entity_reg(hass): - """Return an empty, loaded, registry.""" - return mock_registry(hass) - - -@pytest.fixture -def calls(hass): - """Track calls to a mock serivce.""" - return async_mock_service(hass, "test", "automation") - - -def _same_lists(a, b): - if len(a) != len(b): - return False - - for d in a: - if d not in b: - return False - return True - - -async def test_get_actions(hass, device_reg, entity_reg): - """Test we get the expected actions from a binary_sensor.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - platform.init() - - 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", - platform.ENTITIES["battery"].unique_id, - device_id=device_entry.id, - ) - - assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) - - expected_actions = [] - actions = await async_get_device_automations( - hass, "async_get_actions", device_entry.id - ) - assert _same_lists(actions, expected_actions) - - -async def test_get_conditions(hass, device_reg, entity_reg): - """Test we get the expected conditions from a binary_sensor.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - platform.init() - - 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")}, - ) - for device_class in DEVICE_CLASSES: - entity_reg.async_get_or_create( - DOMAIN, - "test", - platform.ENTITIES[device_class].unique_id, - device_id=device_entry.id, - ) - - assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) - - expected_conditions = [ - { - "condition": "device", - "domain": DOMAIN, - "type": condition["type"], - "device_id": device_entry.id, - "entity_id": platform.ENTITIES[device_class].entity_id, - } - for device_class in DEVICE_CLASSES - for condition in ENTITY_CONDITIONS[device_class] - ] - conditions = await async_get_device_automations( - hass, "async_get_conditions", device_entry.id - ) - assert _same_lists(conditions, expected_conditions) - - -async def test_get_triggers(hass, device_reg, entity_reg): - """Test we get the expected triggers from a binary_sensor.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - platform.init() - - 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")}, - ) - for device_class in DEVICE_CLASSES: - entity_reg.async_get_or_create( - DOMAIN, - "test", - platform.ENTITIES[device_class].unique_id, - device_id=device_entry.id, - ) - - assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) - - expected_triggers = [ - { - "platform": "device", - "domain": DOMAIN, - "type": trigger["type"], - "device_id": device_entry.id, - "entity_id": platform.ENTITIES[device_class].entity_id, - } - for device_class in DEVICE_CLASSES - for trigger in ENTITY_TRIGGERS[device_class] - ] - triggers = await async_get_device_automations( - hass, "async_get_triggers", device_entry.id - ) - assert _same_lists(triggers, expected_triggers) - - -async def test_if_fires_on_state_change(hass, calls): - """Test for on and off triggers firing.""" - 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": "bat_low", - }, - "action": { - "service": "test.automation", - "data_template": { - "some": "bat_low {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) - ) - }, - }, - }, - { - "trigger": { - "platform": "device", - "domain": DOMAIN, - "device_id": "", - "entity_id": sensor1.entity_id, - "type": "not_bat_low", - }, - "action": { - "service": "test.automation", - "data_template": { - "some": "not_bat_low {{ 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) == 1 - assert calls[0].data["some"] == "not_bat_low state - {} - on - off - None".format( - sensor1.entity_id - ) - - hass.states.async_set(sensor1.entity_id, STATE_ON) - await hass.async_block_till_done() - assert len(calls) == 2 - assert calls[1].data["some"] == "bat_low state - {} - off - on - None".format( - sensor1.entity_id - ) - - -async def test_if_state(hass, calls): - """Test for turn_on and turn_off conditions.""" - 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": "event", "event_type": "test_event1"}, - "condition": [ - { - "condition": "device", - "domain": DOMAIN, - "device_id": "", - "entity_id": sensor1.entity_id, - "type": "is_bat_low", - } - ], - "action": { - "service": "test.automation", - "data_template": { - "some": "is_on {{ trigger.%s }}" - % "}} - {{ trigger.".join(("platform", "event.event_type")) - }, - }, - }, - { - "trigger": {"platform": "event", "event_type": "test_event2"}, - "condition": [ - { - "condition": "device", - "domain": DOMAIN, - "device_id": "", - "entity_id": sensor1.entity_id, - "type": "is_not_bat_low", - } - ], - "action": { - "service": "test.automation", - "data_template": { - "some": "is_off {{ trigger.%s }}" - % "}} - {{ trigger.".join(("platform", "event.event_type")) - }, - }, - }, - ] - }, - ) - await hass.async_block_till_done() - assert hass.states.get(sensor1.entity_id).state == STATE_ON - assert len(calls) == 0 - - hass.bus.async_fire("test_event1") - hass.bus.async_fire("test_event2") - await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == "is_on event - test_event1" - - hass.states.async_set(sensor1.entity_id, STATE_OFF) - hass.bus.async_fire("test_event1") - hass.bus.async_fire("test_event2") - await hass.async_block_till_done() - assert len(calls) == 2 - assert calls[1].data["some"] == "is_off event - test_event2" diff --git a/tests/components/binary_sensor/test_device_condition.py b/tests/components/binary_sensor/test_device_condition.py new file mode 100644 index 00000000000..b5502d8fe3d --- /dev/null +++ b/tests/components/binary_sensor/test_device_condition.py @@ -0,0 +1,144 @@ +"""The test for binary_sensor device automation.""" +import pytest + +from homeassistant.components.binary_sensor import DOMAIN, DEVICE_CLASSES +from homeassistant.components.binary_sensor.device_condition import ENTITY_CONDITIONS +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 + +from tests.common import ( + MockConfigEntry, + async_mock_service, + mock_device_registry, + mock_registry, + async_get_device_automations, +) + + +@pytest.fixture +def device_reg(hass): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +@pytest.fixture +def entity_reg(hass): + """Return an empty, loaded, registry.""" + return mock_registry(hass) + + +@pytest.fixture +def calls(hass): + """Track calls to a mock serivce.""" + return async_mock_service(hass, "test", "automation") + + +async def test_get_conditions(hass, device_reg, entity_reg): + """Test we get the expected conditions from a binary_sensor.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + + 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")}, + ) + for device_class in DEVICE_CLASSES: + entity_reg.async_get_or_create( + DOMAIN, + "test", + platform.ENTITIES[device_class].unique_id, + device_id=device_entry.id, + ) + + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + expected_conditions = [ + { + "condition": "device", + "domain": DOMAIN, + "type": condition["type"], + "device_id": device_entry.id, + "entity_id": platform.ENTITIES[device_class].entity_id, + } + for device_class in DEVICE_CLASSES + for condition in ENTITY_CONDITIONS[device_class] + ] + conditions = await async_get_device_automations(hass, "condition", device_entry.id) + assert conditions == expected_conditions + + +async def test_if_state(hass, calls): + """Test for turn_on and turn_off conditions.""" + 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": "event", "event_type": "test_event1"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": sensor1.entity_id, + "type": "is_bat_low", + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "is_on {{ trigger.%s }}" + % "}} - {{ trigger.".join(("platform", "event.event_type")) + }, + }, + }, + { + "trigger": {"platform": "event", "event_type": "test_event2"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": sensor1.entity_id, + "type": "is_not_bat_low", + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "is_off {{ trigger.%s }}" + % "}} - {{ trigger.".join(("platform", "event.event_type")) + }, + }, + }, + ] + }, + ) + await hass.async_block_till_done() + assert hass.states.get(sensor1.entity_id).state == STATE_ON + assert len(calls) == 0 + + hass.bus.async_fire("test_event1") + hass.bus.async_fire("test_event2") + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["some"] == "is_on event - test_event1" + + hass.states.async_set(sensor1.entity_id, STATE_OFF) + hass.bus.async_fire("test_event1") + hass.bus.async_fire("test_event2") + await hass.async_block_till_done() + assert len(calls) == 2 + assert calls[1].data["some"] == "is_off event - test_event2" diff --git a/tests/components/binary_sensor/test_device_trigger.py b/tests/components/binary_sensor/test_device_trigger.py new file mode 100644 index 00000000000..5be354c78fc --- /dev/null +++ b/tests/components/binary_sensor/test_device_trigger.py @@ -0,0 +1,154 @@ +"""The test for binary_sensor device automation.""" +import pytest + +from homeassistant.components.binary_sensor import DOMAIN, DEVICE_CLASSES +from homeassistant.components.binary_sensor.device_trigger import ENTITY_TRIGGERS +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 + +from tests.common import ( + MockConfigEntry, + async_mock_service, + mock_device_registry, + mock_registry, + async_get_device_automations, +) + + +@pytest.fixture +def device_reg(hass): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +@pytest.fixture +def entity_reg(hass): + """Return an empty, loaded, registry.""" + return mock_registry(hass) + + +@pytest.fixture +def calls(hass): + """Track calls to a mock serivce.""" + return async_mock_service(hass, "test", "automation") + + +async def test_get_triggers(hass, device_reg, entity_reg): + """Test we get the expected triggers from a binary_sensor.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + + 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")}, + ) + for device_class in DEVICE_CLASSES: + entity_reg.async_get_or_create( + DOMAIN, + "test", + platform.ENTITIES[device_class].unique_id, + device_id=device_entry.id, + ) + + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + expected_triggers = [ + { + "platform": "device", + "domain": DOMAIN, + "type": trigger["type"], + "device_id": device_entry.id, + "entity_id": platform.ENTITIES[device_class].entity_id, + } + for device_class in DEVICE_CLASSES + for trigger in ENTITY_TRIGGERS[device_class] + ] + triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + assert triggers == expected_triggers + + +async def test_if_fires_on_state_change(hass, calls): + """Test for on and off triggers firing.""" + 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": "bat_low", + }, + "action": { + "service": "test.automation", + "data_template": { + "some": "bat_low {{ trigger.%s }}" + % "}} - {{ trigger.".join( + ( + "platform", + "entity_id", + "from_state.state", + "to_state.state", + "for", + ) + ) + }, + }, + }, + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": sensor1.entity_id, + "type": "not_bat_low", + }, + "action": { + "service": "test.automation", + "data_template": { + "some": "not_bat_low {{ 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) == 1 + assert calls[0].data["some"] == "not_bat_low device - {} - on - off - None".format( + sensor1.entity_id + ) + + hass.states.async_set(sensor1.entity_id, STATE_ON) + await hass.async_block_till_done() + assert len(calls) == 2 + assert calls[1].data["some"] == "bat_low device - {} - off - on - None".format( + sensor1.entity_id + ) diff --git a/tests/components/binary_sensor/test_binary_sensor.py b/tests/components/binary_sensor/test_init.py similarity index 100% rename from tests/components/binary_sensor/test_binary_sensor.py rename to tests/components/binary_sensor/test_init.py diff --git a/tests/components/deconz/test_device_automation.py b/tests/components/deconz/test_device_trigger.py similarity index 71% rename from tests/components/deconz/test_device_automation.py rename to tests/components/deconz/test_device_trigger.py index 0be566d4b52..6590028d766 100644 --- a/tests/components/deconz/test_device_automation.py +++ b/tests/components/deconz/test_device_trigger.py @@ -3,9 +3,9 @@ from asynctest import patch from homeassistant import config_entries from homeassistant.components import deconz -from homeassistant.components.device_automation import ( - _async_get_device_automations as async_get_device_automations, -) +from homeassistant.components.deconz import device_trigger + +from tests.common import async_get_device_automations BRIDGEID = "0123456789" @@ -49,16 +49,6 @@ DECONZ_SENSOR = { DECONZ_WEB_REQUEST = {"config": DECONZ_CONFIG, "sensors": DECONZ_SENSOR} -def _same_lists(a, b): - if len(a) != len(b): - return False - - for d in a: - if d not in b: - return False - return True - - async def setup_deconz(hass, options): """Create the deCONZ gateway.""" config_entry = config_entries.ConfigEntry( @@ -88,51 +78,51 @@ async def test_get_triggers(hass): """Test triggers work.""" gateway = await setup_deconz(hass, options={}) device_id = gateway.events[0].device_id - triggers = await async_get_device_automations(hass, "async_get_triggers", device_id) + triggers = await async_get_device_automations(hass, "trigger", device_id) expected_triggers = [ { "device_id": device_id, "domain": "deconz", "platform": "device", - "type": deconz.device_automation.CONF_SHORT_PRESS, - "subtype": deconz.device_automation.CONF_TURN_ON, + "type": device_trigger.CONF_SHORT_PRESS, + "subtype": device_trigger.CONF_TURN_ON, }, { "device_id": device_id, "domain": "deconz", "platform": "device", - "type": deconz.device_automation.CONF_LONG_PRESS, - "subtype": deconz.device_automation.CONF_TURN_ON, + "type": device_trigger.CONF_LONG_PRESS, + "subtype": device_trigger.CONF_TURN_ON, }, { "device_id": device_id, "domain": "deconz", "platform": "device", - "type": deconz.device_automation.CONF_LONG_RELEASE, - "subtype": deconz.device_automation.CONF_TURN_ON, + "type": device_trigger.CONF_LONG_RELEASE, + "subtype": device_trigger.CONF_TURN_ON, }, { "device_id": device_id, "domain": "deconz", "platform": "device", - "type": deconz.device_automation.CONF_SHORT_PRESS, - "subtype": deconz.device_automation.CONF_TURN_OFF, + "type": device_trigger.CONF_SHORT_PRESS, + "subtype": device_trigger.CONF_TURN_OFF, }, { "device_id": device_id, "domain": "deconz", "platform": "device", - "type": deconz.device_automation.CONF_LONG_PRESS, - "subtype": deconz.device_automation.CONF_TURN_OFF, + "type": device_trigger.CONF_LONG_PRESS, + "subtype": device_trigger.CONF_TURN_OFF, }, { "device_id": device_id, "domain": "deconz", "platform": "device", - "type": deconz.device_automation.CONF_LONG_RELEASE, - "subtype": deconz.device_automation.CONF_TURN_OFF, + "type": device_trigger.CONF_LONG_RELEASE, + "subtype": device_trigger.CONF_TURN_OFF, }, ] - assert _same_lists(triggers, expected_triggers) + assert triggers == expected_triggers diff --git a/tests/components/light/test_device_action.py b/tests/components/light/test_device_action.py new file mode 100644 index 00000000000..bb50778db52 --- /dev/null +++ b/tests/components/light/test_device_action.py @@ -0,0 +1,140 @@ +"""The test for light device automation.""" +import pytest + +from homeassistant.components.light import DOMAIN +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 + +from tests.common import ( + MockConfigEntry, + async_mock_service, + mock_device_registry, + mock_registry, + async_get_device_automations, +) + + +@pytest.fixture +def device_reg(hass): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +@pytest.fixture +def entity_reg(hass): + """Return an empty, loaded, registry.""" + return mock_registry(hass) + + +@pytest.fixture +def calls(hass): + """Track calls to a mock serivce.""" + return async_mock_service(hass, "test", "automation") + + +async def test_get_actions(hass, device_reg, entity_reg): + """Test we get the expected actions from a light.""" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) + expected_actions = [ + { + "domain": DOMAIN, + "type": "turn_off", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + { + "domain": DOMAIN, + "type": "turn_on", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + { + "domain": DOMAIN, + "type": "toggle", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + ] + actions = await async_get_device_automations(hass, "action", device_entry.id) + assert actions == expected_actions + + +async def test_action(hass, calls): + """Test for turn_on and turn_off actions.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + + platform.init() + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + ent1, ent2, ent3 = platform.ENTITIES + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": {"platform": "event", "event_type": "test_event1"}, + "action": { + "domain": DOMAIN, + "device_id": "", + "entity_id": ent1.entity_id, + "type": "turn_off", + }, + }, + { + "trigger": {"platform": "event", "event_type": "test_event2"}, + "action": { + "domain": DOMAIN, + "device_id": "", + "entity_id": ent1.entity_id, + "type": "turn_on", + }, + }, + { + "trigger": {"platform": "event", "event_type": "test_event3"}, + "action": { + "domain": DOMAIN, + "device_id": "", + "entity_id": ent1.entity_id, + "type": "toggle", + }, + }, + ] + }, + ) + await hass.async_block_till_done() + assert hass.states.get(ent1.entity_id).state == STATE_ON + assert len(calls) == 0 + + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert hass.states.get(ent1.entity_id).state == STATE_OFF + + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert hass.states.get(ent1.entity_id).state == STATE_OFF + + hass.bus.async_fire("test_event2") + await hass.async_block_till_done() + assert hass.states.get(ent1.entity_id).state == STATE_ON + + hass.bus.async_fire("test_event2") + await hass.async_block_till_done() + assert hass.states.get(ent1.entity_id).state == STATE_ON + + hass.bus.async_fire("test_event3") + await hass.async_block_till_done() + assert hass.states.get(ent1.entity_id).state == STATE_OFF + + hass.bus.async_fire("test_event3") + await hass.async_block_till_done() + assert hass.states.get(ent1.entity_id).state == STATE_ON diff --git a/tests/components/light/test_device_automation.py b/tests/components/light/test_device_automation.py deleted file mode 100644 index 27b8b860d72..00000000000 --- a/tests/components/light/test_device_automation.py +++ /dev/null @@ -1,373 +0,0 @@ -"""The test for light device automation.""" -import pytest - -from homeassistant.components.light import DOMAIN -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.components.device_automation import ( - _async_get_device_automations as async_get_device_automations, -) -from homeassistant.helpers import device_registry - -from tests.common import ( - MockConfigEntry, - async_mock_service, - mock_device_registry, - mock_registry, -) - - -@pytest.fixture -def device_reg(hass): - """Return an empty, loaded, registry.""" - return mock_device_registry(hass) - - -@pytest.fixture -def entity_reg(hass): - """Return an empty, loaded, registry.""" - return mock_registry(hass) - - -@pytest.fixture -def calls(hass): - """Track calls to a mock serivce.""" - return async_mock_service(hass, "test", "automation") - - -def _same_lists(a, b): - if len(a) != len(b): - return False - - for d in a: - if d not in b: - return False - return True - - -async def test_get_actions(hass, device_reg, entity_reg): - """Test we get the expected actions from a light.""" - config_entry = MockConfigEntry(domain="test", data={}) - config_entry.add_to_hass(hass) - device_entry = device_reg.async_get_or_create( - config_entry_id=config_entry.entry_id, - connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, - ) - entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) - expected_actions = [ - { - "domain": DOMAIN, - "type": "turn_off", - "device_id": device_entry.id, - "entity_id": f"{DOMAIN}.test_5678", - }, - { - "domain": DOMAIN, - "type": "turn_on", - "device_id": device_entry.id, - "entity_id": f"{DOMAIN}.test_5678", - }, - { - "domain": DOMAIN, - "type": "toggle", - "device_id": device_entry.id, - "entity_id": f"{DOMAIN}.test_5678", - }, - ] - actions = await async_get_device_automations( - hass, "async_get_actions", device_entry.id - ) - assert _same_lists(actions, expected_actions) - - -async def test_get_conditions(hass, device_reg, entity_reg): - """Test we get the expected conditions from a light.""" - config_entry = MockConfigEntry(domain="test", data={}) - config_entry.add_to_hass(hass) - device_entry = device_reg.async_get_or_create( - config_entry_id=config_entry.entry_id, - connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, - ) - entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) - expected_conditions = [ - { - "condition": "device", - "domain": DOMAIN, - "type": "is_off", - "device_id": device_entry.id, - "entity_id": f"{DOMAIN}.test_5678", - }, - { - "condition": "device", - "domain": DOMAIN, - "type": "is_on", - "device_id": device_entry.id, - "entity_id": f"{DOMAIN}.test_5678", - }, - ] - conditions = await async_get_device_automations( - hass, "async_get_conditions", device_entry.id - ) - assert _same_lists(conditions, expected_conditions) - - -async def test_get_triggers(hass, device_reg, entity_reg): - """Test we get the expected triggers from a light.""" - config_entry = MockConfigEntry(domain="test", data={}) - config_entry.add_to_hass(hass) - device_entry = device_reg.async_get_or_create( - config_entry_id=config_entry.entry_id, - connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, - ) - entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) - expected_triggers = [ - { - "platform": "device", - "domain": DOMAIN, - "type": "turned_off", - "device_id": device_entry.id, - "entity_id": f"{DOMAIN}.test_5678", - }, - { - "platform": "device", - "domain": DOMAIN, - "type": "turned_on", - "device_id": device_entry.id, - "entity_id": f"{DOMAIN}.test_5678", - }, - ] - triggers = await async_get_device_automations( - hass, "async_get_triggers", device_entry.id - ) - assert _same_lists(triggers, expected_triggers) - - -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}") - - 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_on", - }, - "action": { - "service": "test.automation", - "data_template": { - "some": "turn_on {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) - ) - }, - }, - }, - { - "trigger": { - "platform": "device", - "domain": DOMAIN, - "device_id": "", - "entity_id": ent1.entity_id, - "type": "turned_off", - }, - "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) == 1 - assert calls[0].data["some"] == "turn_off state - {} - on - off - None".format( - ent1.entity_id - ) - - hass.states.async_set(ent1.entity_id, STATE_ON) - await hass.async_block_till_done() - assert len(calls) == 2 - assert calls[1].data["some"] == "turn_on state - {} - off - on - None".format( - ent1.entity_id - ) - - -async def test_if_state(hass, calls): - """Test for turn_on and turn_off conditions.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - - platform.init() - assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) - - ent1, ent2, ent3 = platform.ENTITIES - - assert await async_setup_component( - hass, - automation.DOMAIN, - { - automation.DOMAIN: [ - { - "trigger": {"platform": "event", "event_type": "test_event1"}, - "condition": [ - { - "condition": "device", - "domain": DOMAIN, - "device_id": "", - "entity_id": ent1.entity_id, - "type": "is_on", - } - ], - "action": { - "service": "test.automation", - "data_template": { - "some": "is_on {{ trigger.%s }}" - % "}} - {{ trigger.".join(("platform", "event.event_type")) - }, - }, - }, - { - "trigger": {"platform": "event", "event_type": "test_event2"}, - "condition": [ - { - "condition": "device", - "domain": DOMAIN, - "device_id": "", - "entity_id": ent1.entity_id, - "type": "is_off", - } - ], - "action": { - "service": "test.automation", - "data_template": { - "some": "is_off {{ trigger.%s }}" - % "}} - {{ trigger.".join(("platform", "event.event_type")) - }, - }, - }, - ] - }, - ) - await hass.async_block_till_done() - assert hass.states.get(ent1.entity_id).state == STATE_ON - assert len(calls) == 0 - - hass.bus.async_fire("test_event1") - hass.bus.async_fire("test_event2") - await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == "is_on event - test_event1" - - hass.states.async_set(ent1.entity_id, STATE_OFF) - hass.bus.async_fire("test_event1") - hass.bus.async_fire("test_event2") - await hass.async_block_till_done() - assert len(calls) == 2 - assert calls[1].data["some"] == "is_off event - test_event2" - - -async def test_action(hass, calls): - """Test for turn_on and turn_off actions.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - - platform.init() - assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) - - ent1, ent2, ent3 = platform.ENTITIES - - assert await async_setup_component( - hass, - automation.DOMAIN, - { - automation.DOMAIN: [ - { - "trigger": {"platform": "event", "event_type": "test_event1"}, - "action": { - "domain": DOMAIN, - "device_id": "", - "entity_id": ent1.entity_id, - "type": "turn_off", - }, - }, - { - "trigger": {"platform": "event", "event_type": "test_event2"}, - "action": { - "domain": DOMAIN, - "device_id": "", - "entity_id": ent1.entity_id, - "type": "turn_on", - }, - }, - { - "trigger": {"platform": "event", "event_type": "test_event3"}, - "action": { - "domain": DOMAIN, - "device_id": "", - "entity_id": ent1.entity_id, - "type": "toggle", - }, - }, - ] - }, - ) - await hass.async_block_till_done() - assert hass.states.get(ent1.entity_id).state == STATE_ON - assert len(calls) == 0 - - hass.bus.async_fire("test_event1") - await hass.async_block_till_done() - assert hass.states.get(ent1.entity_id).state == STATE_OFF - - hass.bus.async_fire("test_event1") - await hass.async_block_till_done() - assert hass.states.get(ent1.entity_id).state == STATE_OFF - - hass.bus.async_fire("test_event2") - await hass.async_block_till_done() - assert hass.states.get(ent1.entity_id).state == STATE_ON - - hass.bus.async_fire("test_event2") - await hass.async_block_till_done() - assert hass.states.get(ent1.entity_id).state == STATE_ON - - hass.bus.async_fire("test_event3") - await hass.async_block_till_done() - assert hass.states.get(ent1.entity_id).state == STATE_OFF - - hass.bus.async_fire("test_event3") - await hass.async_block_till_done() - assert hass.states.get(ent1.entity_id).state == STATE_ON diff --git a/tests/components/light/test_device_condition.py b/tests/components/light/test_device_condition.py new file mode 100644 index 00000000000..8009fbd6337 --- /dev/null +++ b/tests/components/light/test_device_condition.py @@ -0,0 +1,136 @@ +"""The test for light device automation.""" +import pytest + +from homeassistant.components.light import DOMAIN +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 + +from tests.common import ( + MockConfigEntry, + async_mock_service, + mock_device_registry, + mock_registry, + async_get_device_automations, +) + + +@pytest.fixture +def device_reg(hass): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +@pytest.fixture +def entity_reg(hass): + """Return an empty, loaded, registry.""" + return mock_registry(hass) + + +@pytest.fixture +def calls(hass): + """Track calls to a mock serivce.""" + return async_mock_service(hass, "test", "automation") + + +async def test_get_conditions(hass, device_reg, entity_reg): + """Test we get the expected conditions from a light.""" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) + expected_conditions = [ + { + "condition": "device", + "domain": DOMAIN, + "type": "is_off", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + { + "condition": "device", + "domain": DOMAIN, + "type": "is_on", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + ] + conditions = await async_get_device_automations(hass, "condition", device_entry.id) + assert conditions == expected_conditions + + +async def test_if_state(hass, calls): + """Test for turn_on and turn_off conditions.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + + platform.init() + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + ent1, ent2, ent3 = platform.ENTITIES + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": {"platform": "event", "event_type": "test_event1"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": ent1.entity_id, + "type": "is_on", + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "is_on {{ trigger.%s }}" + % "}} - {{ trigger.".join(("platform", "event.event_type")) + }, + }, + }, + { + "trigger": {"platform": "event", "event_type": "test_event2"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": ent1.entity_id, + "type": "is_off", + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "is_off {{ trigger.%s }}" + % "}} - {{ trigger.".join(("platform", "event.event_type")) + }, + }, + }, + ] + }, + ) + await hass.async_block_till_done() + assert hass.states.get(ent1.entity_id).state == STATE_ON + assert len(calls) == 0 + + hass.bus.async_fire("test_event1") + hass.bus.async_fire("test_event2") + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["some"] == "is_on event - test_event1" + + hass.states.async_set(ent1.entity_id, STATE_OFF) + hass.bus.async_fire("test_event1") + hass.bus.async_fire("test_event2") + await hass.async_block_till_done() + assert len(calls) == 2 + assert calls[1].data["some"] == "is_off event - test_event2" diff --git a/tests/components/light/test_device_trigger.py b/tests/components/light/test_device_trigger.py new file mode 100644 index 00000000000..9b540c7aa15 --- /dev/null +++ b/tests/components/light/test_device_trigger.py @@ -0,0 +1,147 @@ +"""The test for light device automation.""" +import pytest + +from homeassistant.components.light import DOMAIN +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 + +from tests.common import ( + MockConfigEntry, + async_mock_service, + mock_device_registry, + mock_registry, + async_get_device_automations, +) + + +@pytest.fixture +def device_reg(hass): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +@pytest.fixture +def entity_reg(hass): + """Return an empty, loaded, registry.""" + return mock_registry(hass) + + +@pytest.fixture +def calls(hass): + """Track calls to a mock serivce.""" + return async_mock_service(hass, "test", "automation") + + +async def test_get_triggers(hass, device_reg, entity_reg): + """Test we get the expected triggers from a light.""" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) + expected_triggers = [ + { + "platform": "device", + "domain": DOMAIN, + "type": "turned_off", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + { + "platform": "device", + "domain": DOMAIN, + "type": "turned_on", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + ] + triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + assert triggers == expected_triggers + + +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}") + + 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_on", + }, + "action": { + "service": "test.automation", + "data_template": { + "some": "turn_on {{ trigger.%s }}" + % "}} - {{ trigger.".join( + ( + "platform", + "entity_id", + "from_state.state", + "to_state.state", + "for", + ) + ) + }, + }, + }, + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": ent1.entity_id, + "type": "turned_off", + }, + "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) == 1 + assert calls[0].data["some"] == "turn_off device - {} - on - off - None".format( + ent1.entity_id + ) + + hass.states.async_set(ent1.entity_id, STATE_ON) + await hass.async_block_till_done() + assert len(calls) == 2 + assert calls[1].data["some"] == "turn_on device - {} - off - on - None".format( + ent1.entity_id + ) diff --git a/tests/components/switch/test_device_action.py b/tests/components/switch/test_device_action.py new file mode 100644 index 00000000000..888e06e0214 --- /dev/null +++ b/tests/components/switch/test_device_action.py @@ -0,0 +1,142 @@ +"""The test for switch device automation.""" +import pytest + +from homeassistant.components.switch import DOMAIN +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.components.device_automation import ( + _async_get_device_automations as async_get_device_automations, +) +from homeassistant.helpers import device_registry + +from tests.common import ( + MockConfigEntry, + async_mock_service, + mock_device_registry, + mock_registry, +) + + +@pytest.fixture +def device_reg(hass): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +@pytest.fixture +def entity_reg(hass): + """Return an empty, loaded, registry.""" + return mock_registry(hass) + + +@pytest.fixture +def calls(hass): + """Track calls to a mock serivce.""" + return async_mock_service(hass, "test", "automation") + + +async def test_get_actions(hass, device_reg, entity_reg): + """Test we get the expected actions from a switch.""" + 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_actions = [ + { + "domain": DOMAIN, + "type": "turn_off", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + { + "domain": DOMAIN, + "type": "turn_on", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + { + "domain": DOMAIN, + "type": "toggle", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + ] + actions = await async_get_device_automations(hass, "action", device_entry.id) + assert actions == expected_actions + + +async def test_action(hass, calls): + """Test for turn_on and turn_off actions.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + + platform.init() + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + ent1, ent2, ent3 = platform.ENTITIES + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": {"platform": "event", "event_type": "test_event1"}, + "action": { + "domain": DOMAIN, + "device_id": "", + "entity_id": ent1.entity_id, + "type": "turn_off", + }, + }, + { + "trigger": {"platform": "event", "event_type": "test_event2"}, + "action": { + "domain": DOMAIN, + "device_id": "", + "entity_id": ent1.entity_id, + "type": "turn_on", + }, + }, + { + "trigger": {"platform": "event", "event_type": "test_event3"}, + "action": { + "domain": DOMAIN, + "device_id": "", + "entity_id": ent1.entity_id, + "type": "toggle", + }, + }, + ] + }, + ) + await hass.async_block_till_done() + assert hass.states.get(ent1.entity_id).state == STATE_ON + assert len(calls) == 0 + + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert hass.states.get(ent1.entity_id).state == STATE_OFF + + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert hass.states.get(ent1.entity_id).state == STATE_OFF + + hass.bus.async_fire("test_event2") + await hass.async_block_till_done() + assert hass.states.get(ent1.entity_id).state == STATE_ON + + hass.bus.async_fire("test_event2") + await hass.async_block_till_done() + assert hass.states.get(ent1.entity_id).state == STATE_ON + + hass.bus.async_fire("test_event3") + await hass.async_block_till_done() + assert hass.states.get(ent1.entity_id).state == STATE_OFF + + hass.bus.async_fire("test_event3") + await hass.async_block_till_done() + assert hass.states.get(ent1.entity_id).state == STATE_ON diff --git a/tests/components/switch/test_device_automation.py b/tests/components/switch/test_device_automation.py deleted file mode 100644 index 1ebe4785761..00000000000 --- a/tests/components/switch/test_device_automation.py +++ /dev/null @@ -1,373 +0,0 @@ -"""The test for switch device automation.""" -import pytest - -from homeassistant.components.switch import DOMAIN -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.components.device_automation import ( - _async_get_device_automations as async_get_device_automations, -) -from homeassistant.helpers import device_registry - -from tests.common import ( - MockConfigEntry, - async_mock_service, - mock_device_registry, - mock_registry, -) - - -@pytest.fixture -def device_reg(hass): - """Return an empty, loaded, registry.""" - return mock_device_registry(hass) - - -@pytest.fixture -def entity_reg(hass): - """Return an empty, loaded, registry.""" - return mock_registry(hass) - - -@pytest.fixture -def calls(hass): - """Track calls to a mock serivce.""" - return async_mock_service(hass, "test", "automation") - - -def _same_lists(a, b): - if len(a) != len(b): - return False - - for d in a: - if d not in b: - return False - return True - - -async def test_get_actions(hass, device_reg, entity_reg): - """Test we get the expected actions from a switch.""" - 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_actions = [ - { - "domain": DOMAIN, - "type": "turn_off", - "device_id": device_entry.id, - "entity_id": f"{DOMAIN}.test_5678", - }, - { - "domain": DOMAIN, - "type": "turn_on", - "device_id": device_entry.id, - "entity_id": f"{DOMAIN}.test_5678", - }, - { - "domain": DOMAIN, - "type": "toggle", - "device_id": device_entry.id, - "entity_id": f"{DOMAIN}.test_5678", - }, - ] - actions = await async_get_device_automations( - hass, "async_get_actions", device_entry.id - ) - assert _same_lists(actions, expected_actions) - - -async def test_get_conditions(hass, device_reg, entity_reg): - """Test we get the expected conditions from a switch.""" - 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_conditions = [ - { - "condition": "device", - "domain": DOMAIN, - "type": "is_off", - "device_id": device_entry.id, - "entity_id": f"{DOMAIN}.test_5678", - }, - { - "condition": "device", - "domain": DOMAIN, - "type": "is_on", - "device_id": device_entry.id, - "entity_id": f"{DOMAIN}.test_5678", - }, - ] - conditions = await async_get_device_automations( - hass, "async_get_conditions", device_entry.id - ) - assert _same_lists(conditions, expected_conditions) - - -async def test_get_triggers(hass, device_reg, entity_reg): - """Test we get the expected triggers from a switch.""" - 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_triggers = [ - { - "platform": "device", - "domain": DOMAIN, - "type": "turned_off", - "device_id": device_entry.id, - "entity_id": f"{DOMAIN}.test_5678", - }, - { - "platform": "device", - "domain": DOMAIN, - "type": "turned_on", - "device_id": device_entry.id, - "entity_id": f"{DOMAIN}.test_5678", - }, - ] - triggers = await async_get_device_automations( - hass, "async_get_triggers", device_entry.id - ) - assert _same_lists(triggers, expected_triggers) - - -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}") - - 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_on", - }, - "action": { - "service": "test.automation", - "data_template": { - "some": "turn_on {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) - ) - }, - }, - }, - { - "trigger": { - "platform": "device", - "domain": DOMAIN, - "device_id": "", - "entity_id": ent1.entity_id, - "type": "turned_off", - }, - "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) == 1 - assert calls[0].data["some"] == "turn_off state - {} - on - off - None".format( - ent1.entity_id - ) - - hass.states.async_set(ent1.entity_id, STATE_ON) - await hass.async_block_till_done() - assert len(calls) == 2 - assert calls[1].data["some"] == "turn_on state - {} - off - on - None".format( - ent1.entity_id - ) - - -async def test_if_state(hass, calls): - """Test for turn_on and turn_off conditions.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - - platform.init() - assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) - - ent1, ent2, ent3 = platform.ENTITIES - - assert await async_setup_component( - hass, - automation.DOMAIN, - { - automation.DOMAIN: [ - { - "trigger": {"platform": "event", "event_type": "test_event1"}, - "condition": [ - { - "condition": "device", - "domain": DOMAIN, - "device_id": "", - "entity_id": ent1.entity_id, - "type": "is_on", - } - ], - "action": { - "service": "test.automation", - "data_template": { - "some": "is_on {{ trigger.%s }}" - % "}} - {{ trigger.".join(("platform", "event.event_type")) - }, - }, - }, - { - "trigger": {"platform": "event", "event_type": "test_event2"}, - "condition": [ - { - "condition": "device", - "domain": DOMAIN, - "device_id": "", - "entity_id": ent1.entity_id, - "type": "is_off", - } - ], - "action": { - "service": "test.automation", - "data_template": { - "some": "is_off {{ trigger.%s }}" - % "}} - {{ trigger.".join(("platform", "event.event_type")) - }, - }, - }, - ] - }, - ) - await hass.async_block_till_done() - assert hass.states.get(ent1.entity_id).state == STATE_ON - assert len(calls) == 0 - - hass.bus.async_fire("test_event1") - hass.bus.async_fire("test_event2") - await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == "is_on event - test_event1" - - hass.states.async_set(ent1.entity_id, STATE_OFF) - hass.bus.async_fire("test_event1") - hass.bus.async_fire("test_event2") - await hass.async_block_till_done() - assert len(calls) == 2 - assert calls[1].data["some"] == "is_off event - test_event2" - - -async def test_action(hass, calls): - """Test for turn_on and turn_off actions.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - - platform.init() - assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) - - ent1, ent2, ent3 = platform.ENTITIES - - assert await async_setup_component( - hass, - automation.DOMAIN, - { - automation.DOMAIN: [ - { - "trigger": {"platform": "event", "event_type": "test_event1"}, - "action": { - "domain": DOMAIN, - "device_id": "", - "entity_id": ent1.entity_id, - "type": "turn_off", - }, - }, - { - "trigger": {"platform": "event", "event_type": "test_event2"}, - "action": { - "domain": DOMAIN, - "device_id": "", - "entity_id": ent1.entity_id, - "type": "turn_on", - }, - }, - { - "trigger": {"platform": "event", "event_type": "test_event3"}, - "action": { - "domain": DOMAIN, - "device_id": "", - "entity_id": ent1.entity_id, - "type": "toggle", - }, - }, - ] - }, - ) - await hass.async_block_till_done() - assert hass.states.get(ent1.entity_id).state == STATE_ON - assert len(calls) == 0 - - hass.bus.async_fire("test_event1") - await hass.async_block_till_done() - assert hass.states.get(ent1.entity_id).state == STATE_OFF - - hass.bus.async_fire("test_event1") - await hass.async_block_till_done() - assert hass.states.get(ent1.entity_id).state == STATE_OFF - - hass.bus.async_fire("test_event2") - await hass.async_block_till_done() - assert hass.states.get(ent1.entity_id).state == STATE_ON - - hass.bus.async_fire("test_event2") - await hass.async_block_till_done() - assert hass.states.get(ent1.entity_id).state == STATE_ON - - hass.bus.async_fire("test_event3") - await hass.async_block_till_done() - assert hass.states.get(ent1.entity_id).state == STATE_OFF - - hass.bus.async_fire("test_event3") - await hass.async_block_till_done() - assert hass.states.get(ent1.entity_id).state == STATE_ON diff --git a/tests/components/switch/test_device_condition.py b/tests/components/switch/test_device_condition.py new file mode 100644 index 00000000000..e2ce5a373d2 --- /dev/null +++ b/tests/components/switch/test_device_condition.py @@ -0,0 +1,138 @@ +"""The test for switch device automation.""" +import pytest + +from homeassistant.components.switch import DOMAIN +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.components.device_automation import ( + _async_get_device_automations as async_get_device_automations, +) +from homeassistant.helpers import device_registry + +from tests.common import ( + MockConfigEntry, + async_mock_service, + mock_device_registry, + mock_registry, +) + + +@pytest.fixture +def device_reg(hass): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +@pytest.fixture +def entity_reg(hass): + """Return an empty, loaded, registry.""" + return mock_registry(hass) + + +@pytest.fixture +def calls(hass): + """Track calls to a mock serivce.""" + return async_mock_service(hass, "test", "automation") + + +async def test_get_conditions(hass, device_reg, entity_reg): + """Test we get the expected conditions from a switch.""" + 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_conditions = [ + { + "condition": "device", + "domain": DOMAIN, + "type": "is_off", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + { + "condition": "device", + "domain": DOMAIN, + "type": "is_on", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + ] + conditions = await async_get_device_automations(hass, "condition", device_entry.id) + assert conditions == expected_conditions + + +async def test_if_state(hass, calls): + """Test for turn_on and turn_off conditions.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + + platform.init() + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + ent1, ent2, ent3 = platform.ENTITIES + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": {"platform": "event", "event_type": "test_event1"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": ent1.entity_id, + "type": "is_on", + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "is_on {{ trigger.%s }}" + % "}} - {{ trigger.".join(("platform", "event.event_type")) + }, + }, + }, + { + "trigger": {"platform": "event", "event_type": "test_event2"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": ent1.entity_id, + "type": "is_off", + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "is_off {{ trigger.%s }}" + % "}} - {{ trigger.".join(("platform", "event.event_type")) + }, + }, + }, + ] + }, + ) + await hass.async_block_till_done() + assert hass.states.get(ent1.entity_id).state == STATE_ON + assert len(calls) == 0 + + hass.bus.async_fire("test_event1") + hass.bus.async_fire("test_event2") + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["some"] == "is_on event - test_event1" + + hass.states.async_set(ent1.entity_id, STATE_OFF) + hass.bus.async_fire("test_event1") + hass.bus.async_fire("test_event2") + await hass.async_block_till_done() + assert len(calls) == 2 + assert calls[1].data["some"] == "is_off event - test_event2" diff --git a/tests/components/switch/test_device_trigger.py b/tests/components/switch/test_device_trigger.py new file mode 100644 index 00000000000..43af9fe3df3 --- /dev/null +++ b/tests/components/switch/test_device_trigger.py @@ -0,0 +1,147 @@ +"""The test for switch device automation.""" +import pytest + +from homeassistant.components.switch import DOMAIN +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 + +from tests.common import ( + MockConfigEntry, + async_mock_service, + mock_device_registry, + mock_registry, + async_get_device_automations, +) + + +@pytest.fixture +def device_reg(hass): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +@pytest.fixture +def entity_reg(hass): + """Return an empty, loaded, registry.""" + return mock_registry(hass) + + +@pytest.fixture +def calls(hass): + """Track calls to a mock serivce.""" + return async_mock_service(hass, "test", "automation") + + +async def test_get_triggers(hass, device_reg, entity_reg): + """Test we get the expected triggers from a switch.""" + 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_triggers = [ + { + "platform": "device", + "domain": DOMAIN, + "type": "turned_off", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + { + "platform": "device", + "domain": DOMAIN, + "type": "turned_on", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + ] + triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + assert triggers == expected_triggers + + +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}") + + 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_on", + }, + "action": { + "service": "test.automation", + "data_template": { + "some": "turn_on {{ trigger.%s }}" + % "}} - {{ trigger.".join( + ( + "platform", + "entity_id", + "from_state.state", + "to_state.state", + "for", + ) + ) + }, + }, + }, + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": ent1.entity_id, + "type": "turned_off", + }, + "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) == 1 + assert calls[0].data["some"] == "turn_off device - {} - on - off - None".format( + ent1.entity_id + ) + + hass.states.async_set(ent1.entity_id, STATE_ON) + await hass.async_block_till_done() + assert len(calls) == 2 + assert calls[1].data["some"] == "turn_on device - {} - off - on - None".format( + ent1.entity_id + ) diff --git a/tests/components/zha/test_device_automation.py b/tests/components/zha/test_device_automation.py index 9de04ae8e66..5a4b9d5616e 100644 --- a/tests/components/zha/test_device_automation.py +++ b/tests/components/zha/test_device_automation.py @@ -4,9 +4,6 @@ from unittest.mock import patch import pytest import homeassistant.components.automation as automation -from homeassistant.components.device_automation import ( - _async_get_device_automations as async_get_device_automations, -) from homeassistant.components.switch import DOMAIN from homeassistant.components.zha.core.const import CHANNEL_ON_OFF from homeassistant.helpers.device_registry import async_get_registry @@ -14,7 +11,7 @@ from homeassistant.setup import async_setup_component from .common import async_enable_traffic, async_init_zigpy_device -from tests.common import async_mock_service +from tests.common import async_mock_service, async_get_device_automations ON = 1 OFF = 0 @@ -73,9 +70,7 @@ async def test_triggers(hass, config_entry, zha_gateway): ha_device_registry = await async_get_registry(hass) reg_device = ha_device_registry.async_get_device({("zha", ieee_address)}, set()) - triggers = await async_get_device_automations( - hass, "async_get_triggers", reg_device.id - ) + triggers = await async_get_device_automations(hass, "trigger", reg_device.id) expected_triggers = [ { @@ -136,9 +131,7 @@ async def test_no_triggers(hass, config_entry, zha_gateway): ha_device_registry = await async_get_registry(hass) reg_device = ha_device_registry.async_get_device({("zha", ieee_address)}, set()) - triggers = await async_get_device_automations( - hass, "async_get_triggers", reg_device.id - ) + triggers = await async_get_device_automations(hass, "trigger", reg_device.id) assert triggers == []