diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index c2698211241..c6d422f5c2b 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -1665,3 +1665,29 @@ class AlexaEqualizerController(AlexaCapability): configurations = {"modes": {"supported": supported_sound_modes}} return configurations + + +class AlexaTimeHoldController(AlexaCapability): + """Implements Alexa.TimeHoldController. + + https://developer.amazon.com/docs/device-apis/alexa-timeholdcontroller.html + """ + + supported_locales = {"en-US"} + + def __init__(self, entity, allow_remote_resume=False): + """Initialize the entity.""" + super().__init__(entity) + self._allow_remote_resume = allow_remote_resume + + def name(self): + """Return the Alexa API name of this interface.""" + return "Alexa.TimeHoldController" + + def configuration(self): + """Return configuration object. + + Set allowRemoteResume to True if Alexa can restart the operation on the device. + When false, Alexa does not send the Resume directive. + """ + return {"allowRemoteResume": self._allow_remote_resume} diff --git a/homeassistant/components/alexa/entities.py b/homeassistant/components/alexa/entities.py index b14bebb3302..6d1997589a4 100644 --- a/homeassistant/components/alexa/entities.py +++ b/homeassistant/components/alexa/entities.py @@ -19,6 +19,7 @@ from homeassistant.components import ( script, sensor, switch, + timer, ) from homeassistant.components.climate import const as climate from homeassistant.const import ( @@ -61,6 +62,7 @@ from .capabilities import ( AlexaStepSpeaker, AlexaTemperatureSensor, AlexaThermostatController, + AlexaTimeHoldController, AlexaToggleController, ) from .const import CONF_DESCRIPTION, CONF_DISPLAY_CATEGORIES @@ -708,3 +710,17 @@ class InputNumberCapabilities(AlexaEntity): ) yield AlexaEndpointHealth(self.hass, self.entity) yield Alexa(self.hass) + + +@ENTITY_ADAPTERS.register(timer.DOMAIN) +class TimerCapabilities(AlexaEntity): + """Class to represent Timer capabilities.""" + + def default_display_categories(self): + """Return the display categories for this entity.""" + return [DisplayCategory.OTHER] + + def interfaces(self): + """Yield the supported interfaces.""" + yield AlexaTimeHoldController(self.entity, allow_remote_resume=True) + yield Alexa(self.entity) diff --git a/homeassistant/components/alexa/handlers.py b/homeassistant/components/alexa/handlers.py index 74c1b24d42b..b920f11821a 100644 --- a/homeassistant/components/alexa/handlers.py +++ b/homeassistant/components/alexa/handlers.py @@ -10,6 +10,7 @@ from homeassistant.components import ( input_number, light, media_player, + timer, ) from homeassistant.components.climate import const as climate from homeassistant.const import ( @@ -1396,3 +1397,29 @@ async def async_api_bands_directive(hass, config, directive, context): # Currently bands directives are not supported. msg = "Entity does not support directive" raise AlexaInvalidDirectiveError(msg) + + +@HANDLERS.register(("Alexa.TimeHoldController", "Hold")) +async def async_api_hold(hass, config, directive, context): + """Process a TimeHoldController Hold request.""" + entity = directive.entity + data = {ATTR_ENTITY_ID: entity.entity_id} + + await hass.services.async_call( + entity.domain, timer.SERVICE_PAUSE, data, blocking=False, context=context + ) + + return directive.response() + + +@HANDLERS.register(("Alexa.TimeHoldController", "Resume")) +async def async_api_resume(hass, config, directive, context): + """Process a TimeHoldController Resume request.""" + entity = directive.entity + data = {ATTR_ENTITY_ID: entity.entity_id} + + await hass.services.async_call( + entity.domain, timer.SERVICE_START, data, blocking=False, context=context + ) + + return directive.response() diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 37301c3555e..23100bc2078 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -3037,3 +3037,44 @@ async def test_media_player_eq_bands_not_supported(hass): assert msg["header"]["name"] == "ErrorResponse" assert msg["header"]["namespace"] == "Alexa" assert msg["payload"]["type"] == "INVALID_DIRECTIVE" + + +async def test_timer_hold(hass): + """Test timer hold.""" + device = ( + "timer.laundry", + "active", + {"friendly_name": "Laundry", "duration": "00:01:00", "remaining": "00:50:00"}, + ) + appliance = await discovery_test(device, hass) + + assert appliance["endpointId"] == "timer#laundry" + assert appliance["displayCategories"][0] == "OTHER" + assert appliance["friendlyName"] == "Laundry" + + capabilities = assert_endpoint_capabilities( + appliance, "Alexa", "Alexa.TimeHoldController" + ) + + time_hold_capability = get_capability(capabilities, "Alexa.TimeHoldController") + assert time_hold_capability is not None + configuration = time_hold_capability["configuration"] + assert configuration["allowRemoteResume"] is True + + await assert_request_calls_service( + "Alexa.TimeHoldController", "Hold", "timer#laundry", "timer.pause", hass + ) + + +async def test_timer_resume(hass): + """Test timer resume.""" + device = ( + "timer.laundry", + "paused", + {"friendly_name": "Laundry", "duration": "00:01:00", "remaining": "00:50:00"}, + ) + await discovery_test(device, hass) + + await assert_request_calls_service( + "Alexa.TimeHoldController", "Resume", "timer#laundry", "timer.start", hass + )