mirror of
https://github.com/home-assistant/core.git
synced 2025-04-24 01:08:12 +00:00
Add device automation action (#26455)
* Add support for device actions, with light as example. * Add translation; return list
This commit is contained in:
parent
23fdc04554
commit
b1c2a5fa08
@ -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(
|
||||
{
|
||||
|
@ -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"
|
||||
|
@ -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)
|
||||
|
@ -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"
|
||||
|
@ -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,
|
||||
)
|
||||
],
|
||||
)
|
||||
|
@ -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])
|
||||
|
@ -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", {})
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user