Add device automation action (#26455)

* Add support for device actions, with light as example.

* Add translation; return list
This commit is contained in:
Erik Montnemery 2019-09-06 01:26:22 +02:00 committed by Paulus Schoutsen
parent 23fdc04554
commit b1c2a5fa08
8 changed files with 274 additions and 2 deletions

View File

@ -20,6 +20,9 @@ _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_actions
)
hass.components.websocket_api.async_register_command(
websocket_device_automation_list_conditions
)
@ -93,6 +96,20 @@ async def _async_get_device_automations(hass, fname, device_id):
return automations
@websocket_api.async_response
@websocket_api.websocket_command(
{
vol.Required("type"): "device_automation/action/list",
vol.Required("device_id"): str,
}
)
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)
connection.send_result(msg["id"], actions)
@websocket_api.async_response
@websocket_api.websocket_command(
{

View File

@ -1,5 +1,6 @@
"""Constants for device automations."""
CONF_IS_OFF = "is_off"
CONF_IS_ON = "is_on"
CONF_TOGGLE = "toggle"
CONF_TURN_OFF = "turn_off"
CONF_TURN_ON = "turn_on"

View File

@ -5,25 +5,48 @@ import homeassistant.components.automation.state as state
from homeassistant.components.device_automation.const import (
CONF_IS_OFF,
CONF_IS_ON,
CONF_TOGGLE,
CONF_TURN_OFF,
CONF_TURN_ON,
)
from homeassistant.core import split_entity_id
from homeassistant.const import (
CONF_CONDITION,
CONF_DEVICE,
CONF_DEVICE_ID,
CONF_DOMAIN,
CONF_ENTITY_ID,
CONF_PLATFORM,
CONF_TYPE,
)
from homeassistant.helpers import condition, config_validation as cv
from homeassistant.helpers import condition, config_validation as cv, service
from homeassistant.helpers.entity_registry import async_entries_for_device
from . import DOMAIN
# mypy: allow-untyped-defs, no-check-untyped-defs
ENTITY_ACTIONS = [
{
# Turn light off
CONF_DEVICE: None,
CONF_DOMAIN: DOMAIN,
CONF_TYPE: CONF_TURN_OFF,
},
{
# Turn light on
CONF_DEVICE: None,
CONF_DOMAIN: DOMAIN,
CONF_TYPE: CONF_TURN_ON,
},
{
# Toggle light
CONF_DEVICE: None,
CONF_DOMAIN: DOMAIN,
CONF_TYPE: CONF_TOGGLE,
},
]
ENTITY_CONDITIONS = [
{
# True when light is turned off
@ -54,6 +77,18 @@ ENTITY_TRIGGERS = [
},
]
ACTION_SCHEMA = vol.All(
vol.Schema(
{
vol.Required(CONF_DEVICE): None,
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_TOGGLE, CONF_TURN_OFF, CONF_TURN_ON]),
}
)
)
CONDITION_SCHEMA = vol.All(
vol.Schema(
{
@ -83,6 +118,31 @@ def _is_domain(entity, domain):
return split_entity_id(entity.entity_id)[0] == domain
async def async_action_from_config(hass, config, variables, context):
"""Change state based on configuration."""
config = ACTION_SCHEMA(config)
action_type = config[CONF_TYPE]
if action_type == CONF_TURN_ON:
action = "turn_on"
elif action_type == CONF_TURN_OFF:
action = "turn_off"
else:
action = "toggle"
service_action = {
service.CONF_SERVICE: "light.{}".format(action),
CONF_ENTITY_ID: config[CONF_ENTITY_ID],
}
await service.async_call_from_config(
hass,
service_action,
blocking=True,
variables=variables,
# validate_config=False,
context=context,
)
def async_condition_from_config(config, config_validation):
"""Evaluate state based on configuration."""
config = CONDITION_SCHEMA(config)
@ -140,6 +200,11 @@ async def _async_get_automations(hass, device_id, automation_templates):
return automations
async def async_get_actions(hass, device_id):
"""List device actions."""
return await _async_get_automations(hass, device_id, ENTITY_ACTIONS)
async def async_get_conditions(hass, device_id):
"""List device conditions."""
return await _async_get_automations(hass, device_id, ENTITY_CONDITIONS)

View File

@ -1,5 +1,10 @@
{
"device_automation": {
"action_type": {
"toggle": "Toggle {name}",
"turn_on": "Turn on {name}",
"turn_off": "Turn off {name}"
},
"condition_type": {
"is_on": "{name} is on",
"is_off": "{name} is off"

View File

@ -24,6 +24,7 @@ from homeassistant.const import (
CONF_ALIAS,
CONF_BELOW,
CONF_CONDITION,
CONF_DEVICE,
CONF_DOMAIN,
CONF_ENTITY_ID,
CONF_ENTITY_NAMESPACE,
@ -861,6 +862,11 @@ _SCRIPT_WAIT_TEMPLATE_SCHEMA = vol.Schema(
}
)
DEVICE_ACTION_SCHEMA = vol.Schema(
{vol.Required(CONF_DEVICE): None, vol.Required(CONF_DOMAIN): str},
extra=vol.ALLOW_EXTRA,
)
SCRIPT_SCHEMA = vol.All(
ensure_list,
[
@ -870,6 +876,7 @@ SCRIPT_SCHEMA = vol.All(
_SCRIPT_WAIT_TEMPLATE_SCHEMA,
EVENT_SCHEMA,
CONDITION_SCHEMA,
DEVICE_ACTION_SCHEMA,
)
],
)

View File

@ -9,7 +9,7 @@ from typing import Optional, Sequence, Callable, Dict, List, Set, Tuple
import voluptuous as vol
from homeassistant.core import HomeAssistant, Context, callback, CALLBACK_TYPE
from homeassistant.const import CONF_CONDITION, CONF_TIMEOUT
from homeassistant.const import CONF_CONDITION, CONF_DEVICE, CONF_DOMAIN, CONF_TIMEOUT
from homeassistant import exceptions
from homeassistant.helpers import (
service,
@ -22,6 +22,7 @@ from homeassistant.helpers.event import (
async_track_template,
)
from homeassistant.helpers.typing import ConfigType
from homeassistant.loader import async_get_integration
import homeassistant.util.dt as date_util
from homeassistant.util.async_ import run_coroutine_threadsafe, run_callback_threadsafe
@ -48,6 +49,7 @@ ACTION_WAIT_TEMPLATE = "wait_template"
ACTION_CHECK_CONDITION = "condition"
ACTION_FIRE_EVENT = "event"
ACTION_CALL_SERVICE = "call_service"
ACTION_DEVICE_AUTOMATION = "device"
def _determine_action(action):
@ -64,6 +66,9 @@ def _determine_action(action):
if CONF_EVENT in action:
return ACTION_FIRE_EVENT
if CONF_DEVICE in action:
return ACTION_DEVICE_AUTOMATION
return ACTION_CALL_SERVICE
@ -117,6 +122,7 @@ class Script:
ACTION_CHECK_CONDITION: self._async_check_condition,
ACTION_FIRE_EVENT: self._async_fire_event,
ACTION_CALL_SERVICE: self._async_call_service,
ACTION_DEVICE_AUTOMATION: self._async_device_automation,
}
@property
@ -318,6 +324,17 @@ class Script:
context=context,
)
async def _async_device_automation(self, action, variables, context):
"""Perform the device automation specified in the action.
This method is a coroutine.
"""
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")
await platform.async_action_from_config(self.hass, action, variables, context)
async def _async_fire_event(self, action, variables, context):
"""Fire an event."""
self.last_action = action.get(CONF_ALIAS, action[CONF_EVENT])

View File

@ -31,6 +31,53 @@ def _same_lists(a, b):
return True
async def test_websocket_get_actions(hass, hass_ws_client, device_reg, entity_reg):
"""Test we get the expected conditions 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_actions = [
{
"device": None,
"domain": "light",
"type": "turn_off",
"device_id": device_entry.id,
"entity_id": "light.test_5678",
},
{
"device": None,
"domain": "light",
"type": "turn_on",
"device_id": device_entry.id,
"entity_id": "light.test_5678",
},
{
"device": None,
"domain": "light",
"type": "toggle",
"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/action/list", "device_id": device_entry.id}
)
msg = await client.receive_json()
assert msg["id"] == 1
assert msg["type"] == TYPE_RESULT
assert msg["success"]
actions = msg["result"]
assert _same_lists(actions, expected_actions)
async def test_websocket_get_conditions(hass, hass_ws_client, device_reg, entity_reg):
"""Test we get the expected conditions from a light through websocket."""
await async_setup_component(hass, "device_automation", {})

View File

@ -46,6 +46,44 @@ def _same_lists(a, b):
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("light", "test", "5678", device_id=device_entry.id)
expected_actions = [
{
"device": None,
"domain": "light",
"type": "turn_off",
"device_id": device_entry.id,
"entity_id": "light.test_5678",
},
{
"device": None,
"domain": "light",
"type": "turn_on",
"device_id": device_entry.id,
"entity_id": "light.test_5678",
},
{
"device": None,
"domain": "light",
"type": "toggle",
"device_id": device_entry.id,
"entity_id": "light.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={})
@ -263,3 +301,78 @@ async def test_if_state(hass, calls):
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, "test.light")
platform.init()
assert await async_setup_component(
hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}}
)
dev1, dev2, dev3 = platform.DEVICES
assert await async_setup_component(
hass,
automation.DOMAIN,
{
automation.DOMAIN: [
{
"trigger": {"platform": "event", "event_type": "test_event1"},
"action": {
"device": None,
"domain": "light",
"entity_id": dev1.entity_id,
"type": "turn_off",
},
},
{
"trigger": {"platform": "event", "event_type": "test_event2"},
"action": {
"device": None,
"domain": "light",
"entity_id": dev1.entity_id,
"type": "turn_on",
},
},
{
"trigger": {"platform": "event", "event_type": "test_event3"},
"action": {
"device": None,
"domain": "light",
"entity_id": dev1.entity_id,
"type": "toggle",
},
},
]
},
)
await hass.async_block_till_done()
assert hass.states.get(dev1.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(dev1.entity_id).state == STATE_OFF
hass.bus.async_fire("test_event1")
await hass.async_block_till_done()
assert hass.states.get(dev1.entity_id).state == STATE_OFF
hass.bus.async_fire("test_event2")
await hass.async_block_till_done()
assert hass.states.get(dev1.entity_id).state == STATE_ON
hass.bus.async_fire("test_event2")
await hass.async_block_till_done()
assert hass.states.get(dev1.entity_id).state == STATE_ON
hass.bus.async_fire("test_event3")
await hass.async_block_till_done()
assert hass.states.get(dev1.entity_id).state == STATE_OFF
hass.bus.async_fire("test_event3")
await hass.async_block_till_done()
assert hass.states.get(dev1.entity_id).state == STATE_ON