Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add websock command to query device for triggers #24044

Merged
merged 11 commits into from
Jun 10, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ homeassistant/components/daikin/* @fredrike @rofrantz
homeassistant/components/darksky/* @fabaff
homeassistant/components/deconz/* @kane610
homeassistant/components/demo/* @home-assistant/core
homeassistant/components/device_automation/* @home-assistant/core
homeassistant/components/digital_ocean/* @fabaff
homeassistant/components/discogs/* @thibmaek
homeassistant/components/doorbird/* @oblogic7
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/automation/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@


def _platform_validator(config):
"""Validate it is a valid platform."""
"""Validate it is a valid platform."""
try:
platform = importlib.import_module('.{}'.format(config[CONF_PLATFORM]),
__name__)
Expand Down
18 changes: 18 additions & 0 deletions homeassistant/components/automation/device.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
"""Offer device oriented automation."""
import voluptuous as vol

from homeassistant.const import CONF_DOMAIN, CONF_PLATFORM
from homeassistant.loader import async_get_integration


TRIGGER_SCHEMA = vol.Schema({
vol.Required(CONF_PLATFORM): 'device',
vol.Required(CONF_DOMAIN): str,
}, extra=vol.ALLOW_EXTRA)


async def async_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)
80 changes: 80 additions & 0 deletions homeassistant/components/device_automation/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
"""Helpers for device automations."""
import asyncio
import logging

import voluptuous as vol

from homeassistant.components import websocket_api
from homeassistant.core import split_entity_id
from homeassistant.helpers.entity_registry import async_entries_for_device
from homeassistant.loader import async_get_integration, IntegrationNotFound

DOMAIN = 'device_automation'

_LOGGER = logging.getLogger(__name__)


async def async_setup(hass, config):
"""Set up device automation."""
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."""
integration = None
try:
integration = await async_get_integration(hass, domain)
except IntegrationNotFound:
_LOGGER.warning('Integration %s not found', domain)
return None

try:
platform = integration.get_platform('device_automation')
except ImportError:
# The domain does not have device automations
return None

if hasattr(platform, 'async_get_triggers'):
return await platform.async_get_triggers(hass, device_id)


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

domains = set()
triggers = []
device = device_registry.async_get(device_id)
for entry_id in device.config_entries:
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])

device_triggers = await asyncio.gather(*[
_async_get_device_automation_triggers(hass, domain, device_id)
for domain in domains
])
for device_trigger in device_triggers:
if device_trigger is not None:
triggers.extend(device_trigger)

return triggers


@websocket_api.async_response
@websocket_api.websocket_command({
vol.Required('type'): 'device_automation/list_triggers',
vol.Required('device_id'): str,
})
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})
12 changes: 12 additions & 0 deletions homeassistant/components/device_automation/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"domain": "device_automation",
"name": "Device automation",
"documentation": "https://www.home-assistant.io/components/device_automation",
"requirements": [],
"dependencies": [
"webhook"
],
"codeowners": [
"@home-assistant/core"
]
}
80 changes: 80 additions & 0 deletions homeassistant/components/light/device_automation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
"""Provides device automations for lights."""
import voluptuous as vol

import homeassistant.components.automation.state as state
from homeassistant.core import split_entity_id
from homeassistant.const import (
CONF_DEVICE_ID, CONF_DOMAIN, CONF_ENTITY_ID, CONF_PLATFORM, CONF_TYPE)
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_registry import async_entries_for_device
from . import DOMAIN

CONF_TURN_OFF = 'turn_off'
CONF_TURN_ON = 'turn_on'

ENTITY_TRIGGERS = [
{
# Trigger when light is turned on
CONF_PLATFORM: 'device',
CONF_DOMAIN: DOMAIN,
CONF_TYPE: CONF_TURN_OFF,
},
{
# Trigger when light is turned off
CONF_PLATFORM: 'device',
CONF_DOMAIN: DOMAIN,
CONF_TYPE: CONF_TURN_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,
}))


def _is_domain(entity, domain):
return split_entity_id(entity.entity_id)[0] == domain


async def async_attach_trigger(hass, config, action, automation_info):
"""Listen for state changes based on configuration."""
trigger_type = config.get(CONF_TYPE)
if trigger_type == CONF_TURN_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
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Include a FROM or else a color update will trigger it too.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed.


return await state.async_trigger(hass, state_config, action,
automation_info)


async def async_trigger(hass, config, action, automation_info):
"""Temporary so existing automation framework can be used for testing."""
return await async_attach_trigger(hass, config, action, automation_info)


async def async_get_triggers(hass, device_id):
"""List device triggers."""
triggers = []
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)

return triggers
1 change: 1 addition & 0 deletions homeassistant/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
CONF_DELAY_TIME = 'delay_time'
CONF_DEVICE = 'device'
CONF_DEVICE_CLASS = 'device_class'
CONF_DEVICE_ID = 'device_id'
CONF_DEVICES = 'devices'
CONF_DISARM_AFTER_TRIGGER = 'disarm_after_trigger'
CONF_DISCOVERY = 'discovery'
Expand Down
67 changes: 67 additions & 0 deletions tests/components/device_automation/test_init.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
"""The test for light device automation."""
import pytest

from homeassistant.setup import async_setup_component
from homeassistant.components.websocket_api.const import TYPE_RESULT
from homeassistant.helpers import device_registry


from tests.common import (
MockConfigEntry, 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)


def _same_triggers(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_websocket_get_triggers(
hass, hass_ws_client, device_reg, entity_reg):
"""Test we get the expected triggers from a light through websocket."""
await async_setup_component(hass, 'device_automation', {})
config_entry = MockConfigEntry(domain='test', data={})
config_entry.add_to_hass(hass)
device_entry = device_reg.async_get_or_create(
config_entry_id=config_entry.entry_id,
connections={
(device_registry.CONNECTION_NETWORK_MAC, '12:34:56:AB:CD:EF')
})
entity_reg.async_get_or_create(
'light', 'test', '5678', device_id=device_entry.id)
expected_triggers = [
{'platform': 'device', 'domain': 'light', 'type': 'turn_off',
'device_id': device_entry.id, 'entity_id': 'light.test_5678'},
{'platform': 'device', 'domain': 'light', 'type': 'turn_on',
'device_id': device_entry.id, 'entity_id': 'light.test_5678'},
]

client = await hass_ws_client(hass)
await client.send_json({
'id': 1,
'type': 'device_automation/list_triggers',
'device_id': device_entry.id
})
msg = await client.receive_json()

assert msg['id'] == 1
assert msg['type'] == TYPE_RESULT
assert msg['success']
triggers = msg['result']['triggers']
assert _same_triggers(triggers, expected_triggers)
Loading