Allow Alexa to stop a cover (#130846)

* Allow Alexa to stop a cover

* Fix tests

* Update tests/components/alexa/test_smart_home.py

Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>

---------

Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
This commit is contained in:
Jan Bouwhuis 2024-11-24 17:11:56 +01:00 committed by GitHub
parent 076a351ce4
commit d790a2d74c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 146 additions and 15 deletions

View File

@ -816,6 +816,12 @@ class AlexaPlaybackController(AlexaCapability):
""" """
supported_features = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) supported_features = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
operations: dict[
cover.CoverEntityFeature | media_player.MediaPlayerEntityFeature, str
]
if self.entity.domain == cover.DOMAIN:
operations = {cover.CoverEntityFeature.STOP: "Stop"}
else:
operations = { operations = {
media_player.MediaPlayerEntityFeature.NEXT_TRACK: "Next", media_player.MediaPlayerEntityFeature.NEXT_TRACK: "Next",
media_player.MediaPlayerEntityFeature.PAUSE: "Pause", media_player.MediaPlayerEntityFeature.PAUSE: "Pause",

View File

@ -559,6 +559,10 @@ class CoverCapabilities(AlexaEntity):
) )
if supported & cover.CoverEntityFeature.SET_TILT_POSITION: if supported & cover.CoverEntityFeature.SET_TILT_POSITION:
yield AlexaRangeController(self.entity, instance=f"{cover.DOMAIN}.tilt") yield AlexaRangeController(self.entity, instance=f"{cover.DOMAIN}.tilt")
if supported & (
cover.CoverEntityFeature.STOP | cover.CoverEntityFeature.STOP_TILT
):
yield AlexaPlaybackController(self.entity, instance=f"{cover.DOMAIN}.stop")
yield AlexaEndpointHealth(self.hass, self.entity) yield AlexaEndpointHealth(self.hass, self.entity)
yield Alexa(self.entity) yield Alexa(self.entity)

View File

@ -2,6 +2,7 @@
from __future__ import annotations from __future__ import annotations
import asyncio
from collections.abc import Callable, Coroutine from collections.abc import Callable, Coroutine
import logging import logging
import math import math
@ -764,6 +765,22 @@ async def async_api_stop(
entity = directive.entity entity = directive.entity
data: dict[str, Any] = {ATTR_ENTITY_ID: entity.entity_id} data: dict[str, Any] = {ATTR_ENTITY_ID: entity.entity_id}
if entity.domain == cover.DOMAIN:
supported: int = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
feature_services: dict[int, str] = {
cover.CoverEntityFeature.STOP.value: cover.SERVICE_STOP_COVER,
cover.CoverEntityFeature.STOP_TILT.value: cover.SERVICE_STOP_COVER_TILT,
}
await asyncio.gather(
*(
hass.services.async_call(
entity.domain, service, data, blocking=False, context=context
)
for feature, service in feature_services.items()
if feature & supported
)
)
else:
await hass.services.async_call( await hass.services.async_call(
entity.domain, SERVICE_MEDIA_STOP, data, blocking=False, context=context entity.domain, SERVICE_MEDIA_STOP, data, blocking=False, context=context
) )

View File

@ -4546,6 +4546,7 @@ async def test_presence_sensor(hass: HomeAssistant) -> None:
"tilt_position_attr_in_service_call", "tilt_position_attr_in_service_call",
"supported_features", "supported_features",
"service_call", "service_call",
"stop_feature_enabled",
), ),
[ [
( (
@ -4556,6 +4557,7 @@ async def test_presence_sensor(hass: HomeAssistant) -> None:
| CoverEntityFeature.CLOSE_TILT | CoverEntityFeature.CLOSE_TILT
| CoverEntityFeature.STOP_TILT, | CoverEntityFeature.STOP_TILT,
"cover.set_cover_tilt_position", "cover.set_cover_tilt_position",
True,
), ),
( (
0, 0,
@ -4565,6 +4567,7 @@ async def test_presence_sensor(hass: HomeAssistant) -> None:
| CoverEntityFeature.CLOSE_TILT | CoverEntityFeature.CLOSE_TILT
| CoverEntityFeature.STOP_TILT, | CoverEntityFeature.STOP_TILT,
"cover.close_cover_tilt", "cover.close_cover_tilt",
True,
), ),
( (
99, 99,
@ -4574,6 +4577,7 @@ async def test_presence_sensor(hass: HomeAssistant) -> None:
| CoverEntityFeature.CLOSE_TILT | CoverEntityFeature.CLOSE_TILT
| CoverEntityFeature.STOP_TILT, | CoverEntityFeature.STOP_TILT,
"cover.set_cover_tilt_position", "cover.set_cover_tilt_position",
True,
), ),
( (
100, 100,
@ -4583,36 +4587,42 @@ async def test_presence_sensor(hass: HomeAssistant) -> None:
| CoverEntityFeature.CLOSE_TILT | CoverEntityFeature.CLOSE_TILT
| CoverEntityFeature.STOP_TILT, | CoverEntityFeature.STOP_TILT,
"cover.open_cover_tilt", "cover.open_cover_tilt",
True,
), ),
( (
0, 0,
0, 0,
CoverEntityFeature.SET_TILT_POSITION, CoverEntityFeature.SET_TILT_POSITION,
"cover.set_cover_tilt_position", "cover.set_cover_tilt_position",
False,
), ),
( (
60, 60,
60, 60,
CoverEntityFeature.SET_TILT_POSITION, CoverEntityFeature.SET_TILT_POSITION,
"cover.set_cover_tilt_position", "cover.set_cover_tilt_position",
False,
), ),
( (
100, 100,
100, 100,
CoverEntityFeature.SET_TILT_POSITION, CoverEntityFeature.SET_TILT_POSITION,
"cover.set_cover_tilt_position", "cover.set_cover_tilt_position",
False,
), ),
( (
0, 0,
0, 0,
CoverEntityFeature.SET_TILT_POSITION | CoverEntityFeature.OPEN_TILT, CoverEntityFeature.SET_TILT_POSITION | CoverEntityFeature.OPEN_TILT,
"cover.set_cover_tilt_position", "cover.set_cover_tilt_position",
False,
), ),
( (
100, 100,
100, 100,
CoverEntityFeature.SET_TILT_POSITION | CoverEntityFeature.CLOSE_TILT, CoverEntityFeature.SET_TILT_POSITION | CoverEntityFeature.CLOSE_TILT,
"cover.set_cover_tilt_position", "cover.set_cover_tilt_position",
False,
), ),
], ],
ids=[ ids=[
@ -4633,6 +4643,7 @@ async def test_cover_tilt_position(
tilt_position_attr_in_service_call: int | None, tilt_position_attr_in_service_call: int | None,
supported_features: CoverEntityFeature, supported_features: CoverEntityFeature,
service_call: str, service_call: str,
stop_feature_enabled: bool,
) -> None: ) -> None:
"""Test cover discovery and tilt position using rangeController.""" """Test cover discovery and tilt position using rangeController."""
device = ( device = (
@ -4651,12 +4662,24 @@ async def test_cover_tilt_position(
assert appliance["displayCategories"][0] == "INTERIOR_BLIND" assert appliance["displayCategories"][0] == "INTERIOR_BLIND"
assert appliance["friendlyName"] == "Test cover tilt range" assert appliance["friendlyName"] == "Test cover tilt range"
capabilities = assert_endpoint_capabilities( expected_interfaces: dict[bool, list[str]] = {
appliance, False: [
"Alexa.PowerController", "Alexa.PowerController",
"Alexa.RangeController", "Alexa.RangeController",
"Alexa.EndpointHealth", "Alexa.EndpointHealth",
"Alexa", "Alexa",
],
True: [
"Alexa.PowerController",
"Alexa.RangeController",
"Alexa.PlaybackController",
"Alexa.EndpointHealth",
"Alexa",
],
}
capabilities = assert_endpoint_capabilities(
appliance, *expected_interfaces[stop_feature_enabled]
) )
range_capability = get_capability(capabilities, "Alexa.RangeController") range_capability = get_capability(capabilities, "Alexa.RangeController")
@ -4713,6 +4736,7 @@ async def test_cover_tilt_position_range(hass: HomeAssistant) -> None:
appliance, appliance,
"Alexa.PowerController", "Alexa.PowerController",
"Alexa.RangeController", "Alexa.RangeController",
"Alexa.PlaybackController",
"Alexa.EndpointHealth", "Alexa.EndpointHealth",
"Alexa", "Alexa",
) )
@ -4767,6 +4791,66 @@ async def test_cover_tilt_position_range(hass: HomeAssistant) -> None:
) )
@pytest.mark.parametrize(
("supported_stop_features", "cover_stop_calls", "cover_stop_tilt_calls"),
[
(CoverEntityFeature(0), 0, 0),
(CoverEntityFeature.STOP, 1, 0),
(CoverEntityFeature.STOP_TILT, 0, 1),
(CoverEntityFeature.STOP | CoverEntityFeature.STOP_TILT, 1, 1),
],
ids=["no_stop", "stop_cover", "stop_cover_tilt", "stop_cover_and_stop_cover_tilt"],
)
async def test_cover_stop(
hass: HomeAssistant,
supported_stop_features: CoverEntityFeature,
cover_stop_calls: int,
cover_stop_tilt_calls: int,
) -> None:
"""Test cover and cover tilt can be stopped."""
base_features = (
CoverEntityFeature.OPEN
| CoverEntityFeature.CLOSE
| CoverEntityFeature.OPEN_TILT
| CoverEntityFeature.CLOSE_TILT
| CoverEntityFeature.SET_POSITION
| CoverEntityFeature.SET_TILT_POSITION
)
device = (
"cover.test_semantics",
"open",
{
"friendly_name": "Test cover semantics",
"device_class": "blind",
"supported_features": int(base_features | supported_stop_features),
"current_position": 30,
"tilt_position": 30,
},
)
appliance = await discovery_test(device, hass)
assert appliance["endpointId"] == "cover#test_semantics"
assert appliance["displayCategories"][0] == "INTERIOR_BLIND"
assert appliance["friendlyName"] == "Test cover semantics"
calls_stop = async_mock_service(hass, "cover", "stop_cover")
calls_stop_tilt = async_mock_service(hass, "cover", "stop_cover_tilt")
context = Context()
request = get_new_request(
"Alexa.PlaybackController", "Stop", "cover#test_semantics"
)
await smart_home.async_handle_message(
hass, get_default_config(hass), request, context
)
await hass.async_block_till_done()
assert len(calls_stop) == cover_stop_calls
assert len(calls_stop_tilt) == cover_stop_tilt_calls
async def test_cover_semantics_position_and_tilt(hass: HomeAssistant) -> None: async def test_cover_semantics_position_and_tilt(hass: HomeAssistant) -> None:
"""Test cover discovery and semantics with position and tilt support.""" """Test cover discovery and semantics with position and tilt support."""
device = ( device = (
@ -4790,10 +4874,30 @@ async def test_cover_semantics_position_and_tilt(hass: HomeAssistant) -> None:
appliance, appliance,
"Alexa.PowerController", "Alexa.PowerController",
"Alexa.RangeController", "Alexa.RangeController",
"Alexa.PlaybackController",
"Alexa.EndpointHealth", "Alexa.EndpointHealth",
"Alexa", "Alexa",
) )
playback_controller_capability = get_capability(
capabilities, "Alexa.PlaybackController"
)
assert playback_controller_capability is not None
assert playback_controller_capability["supportedOperations"] == ["Stop"]
# Assert both the cover and tilt stop calls are invoked
stop_cover_tilt_calls = async_mock_service(hass, "cover", "stop_cover_tilt")
await assert_request_calls_service(
"Alexa.PlaybackController",
"Stop",
"cover#test_semantics",
"cover.stop_cover",
hass,
)
assert len(stop_cover_tilt_calls) == 1
call = stop_cover_tilt_calls[0]
assert call.data == {"entity_id": "cover.test_semantics"}
# Assert for Position Semantics # Assert for Position Semantics
position_capability = get_capability( position_capability = get_capability(
capabilities, "Alexa.RangeController", "cover.position" capabilities, "Alexa.RangeController", "cover.position"