Add device actions to cover (#28064)

* Add device actions to cover

* Remove toggle action, improve tests

* lint

* lint

* Simplify actions

* Update homeassistant/components/cover/device_action.py

Co-Authored-By: Paulus Schoutsen <paulus@home-assistant.io>

* Improve tests

Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
This commit is contained in:
Erik Montnemery 2020-02-28 06:05:43 +01:00 committed by GitHub
parent 223c01d842
commit 3b17e570df
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 686 additions and 2 deletions

View File

@ -0,0 +1,176 @@
"""Provides device automations for Cover."""
from typing import List, Optional
import voluptuous as vol
from homeassistant.const import (
ATTR_ENTITY_ID,
ATTR_SUPPORTED_FEATURES,
CONF_DEVICE_ID,
CONF_DOMAIN,
CONF_ENTITY_ID,
CONF_TYPE,
SERVICE_CLOSE_COVER,
SERVICE_CLOSE_COVER_TILT,
SERVICE_OPEN_COVER,
SERVICE_OPEN_COVER_TILT,
SERVICE_SET_COVER_POSITION,
SERVICE_SET_COVER_TILT_POSITION,
)
from homeassistant.core import Context, HomeAssistant
from homeassistant.helpers import entity_registry
import homeassistant.helpers.config_validation as cv
from . import (
ATTR_POSITION,
ATTR_TILT_POSITION,
DOMAIN,
SUPPORT_CLOSE,
SUPPORT_CLOSE_TILT,
SUPPORT_OPEN,
SUPPORT_OPEN_TILT,
SUPPORT_SET_POSITION,
SUPPORT_SET_TILT_POSITION,
)
CMD_ACTION_TYPES = {"open", "close", "open_tilt", "close_tilt"}
POSITION_ACTION_TYPES = {"set_position", "set_tilt_position"}
CMD_ACTION_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend(
{
vol.Required(CONF_TYPE): vol.In(CMD_ACTION_TYPES),
vol.Required(CONF_ENTITY_ID): cv.entity_domain(DOMAIN),
}
)
POSITION_ACTION_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend(
{
vol.Required(CONF_TYPE): vol.In(POSITION_ACTION_TYPES),
vol.Required(CONF_ENTITY_ID): cv.entity_domain(DOMAIN),
vol.Required("position"): vol.All(vol.Coerce(int), vol.Range(min=0, max=100)),
}
)
ACTION_SCHEMA = vol.Any(CMD_ACTION_SCHEMA, POSITION_ACTION_SCHEMA)
async def async_get_actions(hass: HomeAssistant, device_id: str) -> List[dict]:
"""List device actions for Cover devices."""
registry = await entity_registry.async_get_registry(hass)
actions = []
# Get all the integrations entities for this device
for entry in entity_registry.async_entries_for_device(registry, device_id):
if entry.domain != DOMAIN:
continue
state = hass.states.get(entry.entity_id)
if not state or ATTR_SUPPORTED_FEATURES not in state.attributes:
continue
supported_features = state.attributes[ATTR_SUPPORTED_FEATURES]
# Add actions for each entity that belongs to this integration
if supported_features & SUPPORT_SET_POSITION:
actions.append(
{
CONF_DEVICE_ID: device_id,
CONF_DOMAIN: DOMAIN,
CONF_ENTITY_ID: entry.entity_id,
CONF_TYPE: "set_position",
}
)
else:
if supported_features & SUPPORT_OPEN:
actions.append(
{
CONF_DEVICE_ID: device_id,
CONF_DOMAIN: DOMAIN,
CONF_ENTITY_ID: entry.entity_id,
CONF_TYPE: "open",
}
)
if supported_features & SUPPORT_CLOSE:
actions.append(
{
CONF_DEVICE_ID: device_id,
CONF_DOMAIN: DOMAIN,
CONF_ENTITY_ID: entry.entity_id,
CONF_TYPE: "close",
}
)
if supported_features & SUPPORT_SET_TILT_POSITION:
actions.append(
{
CONF_DEVICE_ID: device_id,
CONF_DOMAIN: DOMAIN,
CONF_ENTITY_ID: entry.entity_id,
CONF_TYPE: "set_tilt_position",
}
)
else:
if supported_features & SUPPORT_OPEN_TILT:
actions.append(
{
CONF_DEVICE_ID: device_id,
CONF_DOMAIN: DOMAIN,
CONF_ENTITY_ID: entry.entity_id,
CONF_TYPE: "open_tilt",
}
)
if supported_features & SUPPORT_CLOSE_TILT:
actions.append(
{
CONF_DEVICE_ID: device_id,
CONF_DOMAIN: DOMAIN,
CONF_ENTITY_ID: entry.entity_id,
CONF_TYPE: "close_tilt",
}
)
return actions
async def async_get_action_capabilities(hass: HomeAssistant, config: dict) -> dict:
"""List action capabilities."""
if config[CONF_TYPE] not in POSITION_ACTION_TYPES:
return {}
return {
"extra_fields": vol.Schema(
{
vol.Optional("position", default=0): vol.All(
vol.Coerce(int), vol.Range(min=0, max=100)
)
}
)
}
async def async_call_action_from_config(
hass: HomeAssistant, config: dict, variables: dict, context: Optional[Context]
) -> None:
"""Execute a device action."""
config = ACTION_SCHEMA(config)
service_data = {ATTR_ENTITY_ID: config[CONF_ENTITY_ID]}
if config[CONF_TYPE] == "open":
service = SERVICE_OPEN_COVER
elif config[CONF_TYPE] == "close":
service = SERVICE_CLOSE_COVER
elif config[CONF_TYPE] == "open_tilt":
service = SERVICE_OPEN_COVER_TILT
elif config[CONF_TYPE] == "close_tilt":
service = SERVICE_CLOSE_COVER_TILT
elif config[CONF_TYPE] == "set_position":
service = SERVICE_SET_COVER_POSITION
service_data[ATTR_POSITION] = config["position"]
elif config[CONF_TYPE] == "set_tilt_position":
service = SERVICE_SET_COVER_TILT_POSITION
service_data[ATTR_TILT_POSITION] = config["position"]
await hass.services.async_call(
DOMAIN, service, service_data, blocking=True, context=context
)

View File

@ -1,5 +1,13 @@
{ {
"device_automation": { "device_automation": {
"action_type": {
"open": "Open {entity_name}",
"close": "Close {entity_name}",
"open_tilt": "Open {entity_name} tilt",
"close_tilt": "Close {entity_name} tilt",
"set_position": "Set {entity_name} position",
"set_tilt_position": "Set {entity_name} tilt position"
},
"condition_type": { "condition_type": {
"is_open": "{entity_name} is open", "is_open": "{entity_name} is open",
"is_closed": "{entity_name} is closed", "is_closed": "{entity_name} is closed",

View File

@ -0,0 +1,454 @@
"""The tests for Cover device actions."""
import pytest
import homeassistant.components.automation as automation
from homeassistant.components.cover import DOMAIN
from homeassistant.const import CONF_PLATFORM
from homeassistant.helpers import device_registry
from homeassistant.setup import async_setup_component
from tests.common import (
MockConfigEntry,
assert_lists_same,
async_get_device_automation_capabilities,
async_get_device_automations,
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)
async def test_get_actions(hass, device_reg, entity_reg):
"""Test we get the expected actions from a cover."""
platform = getattr(hass.components, f"test.{DOMAIN}")
platform.init()
ent = platform.ENTITIES[0]
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", ent.unique_id, device_id=device_entry.id
)
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
expected_actions = [
{
"domain": DOMAIN,
"type": "open",
"device_id": device_entry.id,
"entity_id": ent.entity_id,
},
{
"domain": DOMAIN,
"type": "close",
"device_id": device_entry.id,
"entity_id": ent.entity_id,
},
]
actions = await async_get_device_automations(hass, "action", device_entry.id)
assert_lists_same(actions, expected_actions)
async def test_get_actions_tilt(hass, device_reg, entity_reg):
"""Test we get the expected actions from a cover."""
platform = getattr(hass.components, f"test.{DOMAIN}")
platform.init()
ent = platform.ENTITIES[3]
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", ent.unique_id, device_id=device_entry.id
)
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
expected_actions = [
{
"domain": DOMAIN,
"type": "open",
"device_id": device_entry.id,
"entity_id": ent.entity_id,
},
{
"domain": DOMAIN,
"type": "close",
"device_id": device_entry.id,
"entity_id": ent.entity_id,
},
{
"domain": DOMAIN,
"type": "open_tilt",
"device_id": device_entry.id,
"entity_id": ent.entity_id,
},
{
"domain": DOMAIN,
"type": "close_tilt",
"device_id": device_entry.id,
"entity_id": ent.entity_id,
},
]
actions = await async_get_device_automations(hass, "action", device_entry.id)
assert_lists_same(actions, expected_actions)
async def test_get_actions_set_pos(hass, device_reg, entity_reg):
"""Test we get the expected actions from a cover."""
platform = getattr(hass.components, f"test.{DOMAIN}")
platform.init()
ent = platform.ENTITIES[1]
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", ent.unique_id, device_id=device_entry.id
)
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
expected_actions = [
{
"domain": DOMAIN,
"type": "set_position",
"device_id": device_entry.id,
"entity_id": ent.entity_id,
},
]
actions = await async_get_device_automations(hass, "action", device_entry.id)
assert_lists_same(actions, expected_actions)
async def test_get_actions_set_tilt_pos(hass, device_reg, entity_reg):
"""Test we get the expected actions from a cover."""
platform = getattr(hass.components, f"test.{DOMAIN}")
platform.init()
ent = platform.ENTITIES[2]
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", ent.unique_id, device_id=device_entry.id
)
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
expected_actions = [
{
"domain": DOMAIN,
"type": "open",
"device_id": device_entry.id,
"entity_id": ent.entity_id,
},
{
"domain": DOMAIN,
"type": "close",
"device_id": device_entry.id,
"entity_id": ent.entity_id,
},
{
"domain": DOMAIN,
"type": "set_tilt_position",
"device_id": device_entry.id,
"entity_id": ent.entity_id,
},
]
actions = await async_get_device_automations(hass, "action", device_entry.id)
assert_lists_same(actions, expected_actions)
async def test_get_action_capabilities(hass, device_reg, entity_reg):
"""Test we get the expected capabilities from a cover action."""
platform = getattr(hass.components, f"test.{DOMAIN}")
platform.init()
ent = platform.ENTITIES[0]
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", ent.unique_id, device_id=device_entry.id
)
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
actions = await async_get_device_automations(hass, "action", device_entry.id)
assert len(actions) == 2 # open, close
for action in actions:
capabilities = await async_get_device_automation_capabilities(
hass, "action", action
)
assert capabilities == {"extra_fields": []}
async def test_get_action_capabilities_set_pos(hass, device_reg, entity_reg):
"""Test we get the expected capabilities from a cover action."""
platform = getattr(hass.components, f"test.{DOMAIN}")
platform.init()
ent = platform.ENTITIES[1]
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", ent.unique_id, device_id=device_entry.id
)
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
expected_capabilities = {
"extra_fields": [
{
"name": "position",
"optional": True,
"type": "integer",
"default": 0,
"valueMax": 100,
"valueMin": 0,
}
]
}
actions = await async_get_device_automations(hass, "action", device_entry.id)
assert len(actions) == 1 # set_position
for action in actions:
capabilities = await async_get_device_automation_capabilities(
hass, "action", action
)
if action["type"] == "set_position":
assert capabilities == expected_capabilities
else:
assert capabilities == {"extra_fields": []}
async def test_get_action_capabilities_set_tilt_pos(hass, device_reg, entity_reg):
"""Test we get the expected capabilities from a cover action."""
platform = getattr(hass.components, f"test.{DOMAIN}")
platform.init()
ent = platform.ENTITIES[2]
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", ent.unique_id, device_id=device_entry.id
)
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
expected_capabilities = {
"extra_fields": [
{
"name": "position",
"optional": True,
"type": "integer",
"default": 0,
"valueMax": 100,
"valueMin": 0,
}
]
}
actions = await async_get_device_automations(hass, "action", device_entry.id)
assert len(actions) == 3 # open, close, set_tilt_position
for action in actions:
capabilities = await async_get_device_automation_capabilities(
hass, "action", action
)
if action["type"] == "set_tilt_position":
assert capabilities == expected_capabilities
else:
assert capabilities == {"extra_fields": []}
async def test_action(hass):
"""Test for cover actions."""
platform = getattr(hass.components, f"test.{DOMAIN}")
platform.init()
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
assert await async_setup_component(
hass,
automation.DOMAIN,
{
automation.DOMAIN: [
{
"trigger": {"platform": "event", "event_type": "test_event_open"},
"action": {
"domain": DOMAIN,
"device_id": "abcdefgh",
"entity_id": "cover.entity",
"type": "open",
},
},
{
"trigger": {"platform": "event", "event_type": "test_event_close"},
"action": {
"domain": DOMAIN,
"device_id": "abcdefgh",
"entity_id": "cover.entity",
"type": "close",
},
},
]
},
)
open_calls = async_mock_service(hass, "cover", "open_cover")
close_calls = async_mock_service(hass, "cover", "close_cover")
hass.bus.async_fire("test_event_open")
await hass.async_block_till_done()
assert len(open_calls) == 1
assert len(close_calls) == 0
hass.bus.async_fire("test_event_close")
await hass.async_block_till_done()
assert len(open_calls) == 1
assert len(close_calls) == 1
hass.bus.async_fire("test_event_stop")
await hass.async_block_till_done()
assert len(open_calls) == 1
assert len(close_calls) == 1
async def test_action_tilt(hass):
"""Test for cover tilt actions."""
platform = getattr(hass.components, f"test.{DOMAIN}")
platform.init()
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
assert await async_setup_component(
hass,
automation.DOMAIN,
{
automation.DOMAIN: [
{
"trigger": {"platform": "event", "event_type": "test_event_open"},
"action": {
"domain": DOMAIN,
"device_id": "abcdefgh",
"entity_id": "cover.entity",
"type": "open_tilt",
},
},
{
"trigger": {"platform": "event", "event_type": "test_event_close"},
"action": {
"domain": DOMAIN,
"device_id": "abcdefgh",
"entity_id": "cover.entity",
"type": "close_tilt",
},
},
]
},
)
open_calls = async_mock_service(hass, "cover", "open_cover_tilt")
close_calls = async_mock_service(hass, "cover", "close_cover_tilt")
hass.bus.async_fire("test_event_open")
await hass.async_block_till_done()
assert len(open_calls) == 1
assert len(close_calls) == 0
hass.bus.async_fire("test_event_close")
await hass.async_block_till_done()
assert len(open_calls) == 1
assert len(close_calls) == 1
hass.bus.async_fire("test_event_stop")
await hass.async_block_till_done()
assert len(open_calls) == 1
assert len(close_calls) == 1
async def test_action_set_position(hass):
"""Test for cover set position actions."""
platform = getattr(hass.components, f"test.{DOMAIN}")
platform.init()
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
assert await async_setup_component(
hass,
automation.DOMAIN,
{
automation.DOMAIN: [
{
"trigger": {
"platform": "event",
"event_type": "test_event_set_pos",
},
"action": {
"domain": DOMAIN,
"device_id": "abcdefgh",
"entity_id": "cover.entity",
"type": "set_position",
"position": 25,
},
},
{
"trigger": {
"platform": "event",
"event_type": "test_event_set_tilt_pos",
},
"action": {
"domain": DOMAIN,
"device_id": "abcdefgh",
"entity_id": "cover.entity",
"type": "set_tilt_position",
"position": 75,
},
},
]
},
)
cover_pos_calls = async_mock_service(hass, "cover", "set_cover_position")
tilt_pos_calls = async_mock_service(hass, "cover", "set_cover_tilt_position")
hass.bus.async_fire("test_event_set_pos")
await hass.async_block_till_done()
assert len(cover_pos_calls) == 1
assert cover_pos_calls[0].data["position"] == 25
assert len(tilt_pos_calls) == 0
hass.bus.async_fire("test_event_set_tilt_pos")
await hass.async_block_till_done()
assert len(cover_pos_calls) == 1
assert len(tilt_pos_calls) == 1
assert tilt_pos_calls[0].data["tilt_position"] == 75

View File

@ -3,7 +3,17 @@ Provide a mock cover platform.
Call init before using it in your tests to ensure clean test data. Call init before using it in your tests to ensure clean test data.
""" """
from homeassistant.components.cover import CoverDevice from homeassistant.components.cover import (
SUPPORT_CLOSE,
SUPPORT_CLOSE_TILT,
SUPPORT_OPEN,
SUPPORT_OPEN_TILT,
SUPPORT_SET_POSITION,
SUPPORT_SET_TILT_POSITION,
SUPPORT_STOP,
SUPPORT_STOP_TILT,
CoverDevice,
)
from tests.common import MockEntity from tests.common import MockEntity
@ -18,18 +28,31 @@ def init(empty=False):
[] []
if empty if empty
else [ else [
MockCover(name=f"Simple cover", is_on=True, unique_id=f"unique_cover"), MockCover(
name=f"Simple cover",
is_on=True,
unique_id=f"unique_cover",
supports_tilt=False,
),
MockCover( MockCover(
name=f"Set position cover", name=f"Set position cover",
is_on=True, is_on=True,
unique_id=f"unique_set_pos_cover", unique_id=f"unique_set_pos_cover",
current_cover_position=50, current_cover_position=50,
supports_tilt=False,
), ),
MockCover( MockCover(
name=f"Set tilt position cover", name=f"Set tilt position cover",
is_on=True, is_on=True,
unique_id=f"unique_set_pos_tilt_cover", unique_id=f"unique_set_pos_tilt_cover",
current_cover_tilt_position=50, current_cover_tilt_position=50,
supports_tilt=True,
),
MockCover(
name=f"Tilt cover",
is_on=True,
unique_id=f"unique_tilt_cover",
supports_tilt=True,
), ),
] ]
) )
@ -59,3 +82,26 @@ class MockCover(MockEntity, CoverDevice):
def current_cover_tilt_position(self): def current_cover_tilt_position(self):
"""Return current position of cover tilt.""" """Return current position of cover tilt."""
return self._handle("current_cover_tilt_position") return self._handle("current_cover_tilt_position")
@property
def supported_features(self):
"""Flag supported features."""
supported_features = SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_STOP
if self._handle("supports_tilt"):
supported_features |= (
SUPPORT_OPEN_TILT | SUPPORT_CLOSE_TILT | SUPPORT_STOP_TILT
)
if self.current_cover_position is not None:
supported_features |= SUPPORT_SET_POSITION
if self.current_cover_tilt_position is not None:
supported_features |= (
SUPPORT_OPEN_TILT
| SUPPORT_CLOSE_TILT
| SUPPORT_STOP_TILT
| SUPPORT_SET_TILT_POSITION
)
return supported_features