Skip to content
6 changes: 3 additions & 3 deletions homeassistant/components/automation/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -386,7 +386,7 @@ async def _async_process_config(hass, config, component):
action = _async_get_action(hass, config_block.get(CONF_ACTION, {}), name)

if CONF_CONDITION in config_block:
cond_func = _async_process_if(hass, config, config_block)
cond_func = await _async_process_if(hass, config, config_block)

if cond_func is None:
continue
Expand Down Expand Up @@ -437,14 +437,14 @@ async def action(entity_id, variables, context):
return action


def _async_process_if(hass, config, p_config):
async def _async_process_if(hass, config, p_config):
"""Process if checks."""
if_configs = p_config.get(CONF_CONDITION)

checks = []
for if_config in if_configs:
try:
checks.append(condition.async_from_config(if_config, False))
checks.append(await condition.async_from_config(hass, if_config, False))
except HomeAssistantError as ex:
_LOGGER.warning("Invalid condition: %s", ex)
return None
Expand Down
71 changes: 55 additions & 16 deletions homeassistant/components/device_automation/__init__.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
"""Helpers for device automations."""
import asyncio
import logging
from typing import Callable, cast

import voluptuous as vol

from homeassistant.components import websocket_api
from homeassistant.core import split_entity_id
from homeassistant.const import CONF_DOMAIN
from homeassistant.core import split_entity_id, HomeAssistant
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_registry import async_entries_for_device
from homeassistant.helpers.typing import ConfigType
from homeassistant.loader import async_get_integration, IntegrationNotFound

DOMAIN = "device_automation"
Expand All @@ -16,14 +20,31 @@

async def async_setup(hass, config):
"""Set up device automation."""
hass.components.websocket_api.async_register_command(
websocket_device_automation_list_conditions
)
hass.components.websocket_api.async_register_command(
websocket_device_automation_list_triggers
)
return True


async def _async_get_device_automation_triggers(hass, domain, device_id):
"""List device triggers."""
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):
"""List device automations."""
integration = None
try:
integration = await async_get_integration(hass, domain)
Expand All @@ -37,19 +58,19 @@ async def _async_get_device_automation_triggers(hass, domain, device_id):
# The domain does not have device automations
return None

if hasattr(platform, "async_get_triggers"):
return await platform.async_get_triggers(hass, device_id)
if hasattr(platform, fname):
return await getattr(platform, fname)(hass, device_id)


async def async_get_device_automation_triggers(hass, device_id):
"""List device triggers."""
async def _async_get_device_automations(hass, fname, device_id):
"""List device automations."""
device_registry, entity_registry = await asyncio.gather(
hass.helpers.device_registry.async_get_registry(),
hass.helpers.entity_registry.async_get_registry(),
)

domains = set()
triggers = []
automations = []
device = device_registry.async_get(device_id)
for entry_id in device.config_entries:
config_entry = hass.config_entries.async_get_entry(entry_id)
Expand All @@ -59,17 +80,33 @@ async def async_get_device_automation_triggers(hass, device_id):
for entity in entities:
domains.add(split_entity_id(entity.entity_id)[0])

device_triggers = await asyncio.gather(
device_automations = await asyncio.gather(
*(
_async_get_device_automation_triggers(hass, domain, device_id)
_async_get_device_automations_from_domain(hass, domain, fname, device_id)
for domain in domains
)
)
for device_trigger in device_triggers:
if device_trigger is not None:
triggers.extend(device_trigger)
for device_automation in device_automations:
if device_automation is not None:
automations.extend(device_automation)

return automations

return triggers

@websocket_api.async_response
@websocket_api.websocket_command(
{
vol.Required("type"): "device_automation/condition/list",
vol.Required("device_id"): str,
}
)
async def websocket_device_automation_list_conditions(hass, connection, msg):
"""Handle request for device conditions."""
device_id = msg["device_id"]
conditions = await _async_get_device_automations(
hass, "async_get_conditions", device_id
)
connection.send_result(msg["id"], conditions)


@websocket_api.async_response
Expand All @@ -82,5 +119,7 @@ async def async_get_device_automation_triggers(hass, device_id):
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_automation_triggers(hass, device_id)
connection.send_result(msg["id"], {"triggers": triggers})
triggers = await _async_get_device_automations(
hass, "async_get_triggers", device_id
)
connection.send_result(msg["id"], triggers)
5 changes: 5 additions & 0 deletions homeassistant/components/device_automation/const.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"""Constants for device automations."""
CONF_IS_OFF = "is_off"
CONF_IS_ON = "is_on"
CONF_TURN_OFF = "turn_off"
CONF_TURN_ON = "turn_on"
89 changes: 74 additions & 15 deletions homeassistant/components/light/device_automation.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,47 +2,78 @@
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_TURN_OFF,
CONF_TURN_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,
)
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers import condition, config_validation as cv
from homeassistant.helpers.entity_registry import async_entries_for_device
from . import DOMAIN


# mypy: allow-untyped-defs, no-check-untyped-defs

CONF_TURN_OFF = "turn_off"
CONF_TURN_ON = "turn_on"
ENTITY_CONDITIONS = [
{
# True when light is turned off
CONF_CONDITION: "device",
CONF_DOMAIN: DOMAIN,
CONF_TYPE: CONF_IS_OFF,
},
{
# True when light is turned on
CONF_CONDITION: "device",
CONF_DOMAIN: DOMAIN,
CONF_TYPE: CONF_IS_ON,
},
]

ENTITY_TRIGGERS = [
{
# Trigger when light is turned on
# Trigger when light is turned off
CONF_PLATFORM: "device",
CONF_DOMAIN: DOMAIN,
CONF_TYPE: CONF_TURN_OFF,
},
{
# Trigger when light is turned off
# Trigger when light is turned on
CONF_PLATFORM: "device",
CONF_DOMAIN: DOMAIN,
CONF_TYPE: CONF_TURN_ON,
},
]

CONDITION_SCHEMA = vol.All(
vol.Schema(
{
vol.Required(CONF_CONDITION): "device",
vol.Optional(CONF_DEVICE_ID): str,
vol.Required(CONF_DOMAIN): DOMAIN,
vol.Required(CONF_ENTITY_ID): cv.entity_id,
vol.Required(CONF_TYPE): vol.In([CONF_IS_OFF, CONF_IS_ON]),
}
)
)

TRIGGER_SCHEMA = vol.All(
vol.Schema(
{
vol.Required(CONF_PLATFORM): "device",
vol.Optional(CONF_DEVICE_ID): str,
vol.Required(CONF_DOMAIN): DOMAIN,
vol.Required(CONF_ENTITY_ID): cv.entity_id,
vol.Required(CONF_TYPE): str,
vol.Required(CONF_TYPE): vol.In([CONF_TURN_OFF, CONF_TURN_ON]),
}
)
)
Expand All @@ -52,9 +83,27 @@ def _is_domain(entity, domain):
return split_entity_id(entity.entity_id)[0] == domain


def async_condition_from_config(config, config_validation):
"""Evaluate state based on configuration."""
config = CONDITION_SCHEMA(config)
condition_type = config[CONF_TYPE]
if condition_type == CONF_IS_ON:
stat = "on"
else:
stat = "off"
state_config = {
condition.CONF_CONDITION: "state",
condition.CONF_ENTITY_ID: config[CONF_ENTITY_ID],
condition.CONF_STATE: stat,
}

return condition.state_from_config(state_config, config_validation)


async def async_attach_trigger(hass, config, action, automation_info):
"""Listen for state changes based on configuration."""
trigger_type = config.get(CONF_TYPE)
config = TRIGGER_SCHEMA(config)
trigger_type = config[CONF_TYPE]
if trigger_type == CONF_TURN_ON:
from_state = "off"
to_state = "on"
Expand All @@ -75,17 +124,27 @@ async def async_trigger(hass, config, action, automation_info):
return await async_attach_trigger(hass, config, action, automation_info)


async def async_get_triggers(hass, device_id):
"""List device triggers."""
triggers = []
async def _async_get_automations(hass, device_id, automation_templates):
"""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 trigger in ENTITY_TRIGGERS:
trigger = dict(trigger)
trigger.update(device_id=device_id, entity_id=entity.entity_id)
triggers.append(trigger)
for automation in automation_templates:
automation = dict(automation)
automation.update(device_id=device_id, entity_id=entity.entity_id)
automations.append(automation)

return automations


return triggers
async def async_get_conditions(hass, device_id):
"""List device conditions."""
return await _async_get_automations(hass, device_id, ENTITY_CONDITIONS)


async def async_get_triggers(hass, device_id):
"""List device triggers."""
return await _async_get_automations(hass, device_id, ENTITY_TRIGGERS)
4 changes: 4 additions & 0 deletions homeassistant/components/light/strings.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
{
"device_automation": {
"condition_type": {
"is_on": "{name} is on",
"is_off": "{name} is off"
},
"trigger_type": {
"turn_on": "{name} turned on",
"turn_off": "{name} turned off"
Expand Down
Loading