diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index 938101a7500..513a0c157d3 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -1,7 +1,7 @@ """Alexa capabilities.""" import logging -from homeassistant.components import cover, fan, image_processing, light +from homeassistant.components import cover, fan, image_processing, input_number, light from homeassistant.components.alarm_control_panel import ATTR_CODE_FORMAT, FORMAT_NUMBER import homeassistant.components.climate.const as climate import homeassistant.components.media_player.const as media_player @@ -1054,6 +1054,10 @@ class AlexaRangeController(AlexaCapability): if self.instance == f"{cover.DOMAIN}.{cover.ATTR_TILT_POSITION}": return self.entity.attributes.get(cover.ATTR_CURRENT_TILT_POSITION) + # Input Number Value + if self.instance == f"{input_number.DOMAIN}.{input_number.ATTR_VALUE}": + return float(self.entity.state) + return None def configuration(self): @@ -1110,6 +1114,28 @@ class AlexaRangeController(AlexaCapability): ) return self._resource.serialize_capability_resources() + # Input Number Value + if self.instance == f"{input_number.DOMAIN}.{input_number.ATTR_VALUE}": + min_value = float(self.entity.attributes[input_number.ATTR_MIN]) + max_value = float(self.entity.attributes[input_number.ATTR_MAX]) + precision = float(self.entity.attributes.get(input_number.ATTR_STEP, 1)) + unit = self.entity.attributes.get(input_number.ATTR_UNIT_OF_MEASUREMENT) + + self._resource = AlexaPresetResource( + ["Value"], + min_value=min_value, + max_value=max_value, + precision=precision, + unit=unit, + ) + self._resource.add_preset( + value=min_value, labels=[AlexaGlobalCatalog.VALUE_MINIMUM] + ) + self._resource.add_preset( + value=max_value, labels=[AlexaGlobalCatalog.VALUE_MAXIMUM] + ) + return self._resource.serialize_capability_resources() + return None def semantics(self): diff --git a/homeassistant/components/alexa/entities.py b/homeassistant/components/alexa/entities.py index 89ca646890b..ab3dc75bd2c 100644 --- a/homeassistant/components/alexa/entities.py +++ b/homeassistant/components/alexa/entities.py @@ -11,6 +11,7 @@ from homeassistant.components import ( group, image_processing, input_boolean, + input_number, light, lock, media_player, @@ -674,3 +675,21 @@ class ImageProcessingCapabilities(AlexaEntity): yield AlexaEventDetectionSensor(self.hass, self.entity) yield AlexaEndpointHealth(self.hass, self.entity) yield Alexa(self.hass) + + +@ENTITY_ADAPTERS.register(input_number.DOMAIN) +class InputNumberCapabilities(AlexaEntity): + """Class to represent input_number capabilities.""" + + def default_display_categories(self): + """Return the display categories for this entity.""" + return [DisplayCategory.OTHER] + + def interfaces(self): + """Yield the supported interfaces.""" + + yield AlexaRangeController( + self.entity, instance=f"{input_number.DOMAIN}.{input_number.ATTR_VALUE}" + ) + yield AlexaEndpointHealth(self.hass, self.entity) + yield Alexa(self.hass) diff --git a/homeassistant/components/alexa/handlers.py b/homeassistant/components/alexa/handlers.py index b5603af7402..133919be84d 100644 --- a/homeassistant/components/alexa/handlers.py +++ b/homeassistant/components/alexa/handlers.py @@ -3,7 +3,14 @@ import logging import math from homeassistant import core as ha -from homeassistant.components import cover, fan, group, light, media_player +from homeassistant.components import ( + cover, + fan, + group, + input_number, + light, + media_player, +) from homeassistant.components.climate import const as climate from homeassistant.const import ( ATTR_ENTITY_ID, @@ -1080,12 +1087,12 @@ async def async_api_set_range(hass, config, directive, context): domain = entity.domain service = None data = {ATTR_ENTITY_ID: entity.entity_id} - range_value = int(directive.payload["rangeValue"]) + range_value = directive.payload["rangeValue"] # Fan Speed if instance == f"{fan.DOMAIN}.{fan.ATTR_SPEED}": service = fan.SERVICE_SET_SPEED - speed = SPEED_FAN_MAP.get(range_value, None) + speed = SPEED_FAN_MAP.get(int(range_value)) if not speed: msg = "Entity does not support value" @@ -1098,6 +1105,7 @@ async def async_api_set_range(hass, config, directive, context): # Cover Position elif instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}": + range_value = int(range_value) if range_value == 0: service = cover.SERVICE_CLOSE_COVER elif range_value == 100: @@ -1108,6 +1116,7 @@ async def async_api_set_range(hass, config, directive, context): # Cover Tilt Position elif instance == f"{cover.DOMAIN}.{cover.ATTR_TILT_POSITION}": + range_value = int(range_value) if range_value == 0: service = cover.SERVICE_CLOSE_COVER_TILT elif range_value == 100: @@ -1116,6 +1125,14 @@ async def async_api_set_range(hass, config, directive, context): service = cover.SERVICE_SET_COVER_TILT_POSITION data[cover.ATTR_POSITION] = range_value + # Input Number Value + elif instance == f"{input_number.DOMAIN}.{input_number.ATTR_VALUE}": + range_value = float(range_value) + service = input_number.SERVICE_SET_VALUE + min_value = float(entity.attributes[input_number.ATTR_MIN]) + max_value = float(entity.attributes[input_number.ATTR_MAX]) + data[input_number.ATTR_VALUE] = min(max_value, max(min_value, range_value)) + else: msg = "Entity does not support directive" raise AlexaInvalidDirectiveError(msg) @@ -1145,11 +1162,12 @@ async def async_api_adjust_range(hass, config, directive, context): domain = entity.domain service = None data = {ATTR_ENTITY_ID: entity.entity_id} - range_delta = int(directive.payload["rangeValueDelta"]) + range_delta = directive.payload["rangeValueDelta"] response_value = 0 # Fan Speed if instance == f"{fan.DOMAIN}.{fan.ATTR_SPEED}": + range_delta = int(range_delta) service = fan.SERVICE_SET_SPEED current_range = RANGE_FAN_MAP.get(entity.attributes.get(fan.ATTR_SPEED), 0) speed = SPEED_FAN_MAP.get( @@ -1163,6 +1181,7 @@ async def async_api_adjust_range(hass, config, directive, context): # Cover Position elif instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}": + range_delta = int(range_delta) service = SERVICE_SET_COVER_POSITION current = entity.attributes.get(cover.ATTR_POSITION) data[cover.ATTR_POSITION] = response_value = min( @@ -1171,12 +1190,24 @@ async def async_api_adjust_range(hass, config, directive, context): # Cover Tilt Position elif instance == f"{cover.DOMAIN}.{cover.ATTR_TILT_POSITION}": + range_delta = int(range_delta) service = SERVICE_SET_COVER_TILT_POSITION current = entity.attributes.get(cover.ATTR_TILT_POSITION) data[cover.ATTR_TILT_POSITION] = response_value = min( 100, max(0, range_delta + current) ) + # Input Number Value + elif instance == f"{input_number.DOMAIN}.{input_number.ATTR_VALUE}": + range_delta = float(range_delta) + service = input_number.SERVICE_SET_VALUE + min_value = float(entity.attributes[input_number.ATTR_MIN]) + max_value = float(entity.attributes[input_number.ATTR_MAX]) + current = float(entity.state) + data[input_number.ATTR_VALUE] = response_value = min( + max_value, max(min_value, range_delta + current) + ) + else: msg = "Entity does not support directive" raise AlexaInvalidDirectiveError(msg) diff --git a/homeassistant/components/alexa/resources.py b/homeassistant/components/alexa/resources.py index 061005252dc..09927321c36 100644 --- a/homeassistant/components/alexa/resources.py +++ b/homeassistant/components/alexa/resources.py @@ -266,9 +266,9 @@ class AlexaPresetResource(AlexaCapabilityResource): """Initialize an Alexa presetResource.""" super().__init__(labels) self._presets = [] - self._minimum_value = int(min_value) - self._maximum_value = int(max_value) - self._precision = int(precision) + self._minimum_value = min_value + self._maximum_value = max_value + self._precision = precision self._unit_of_measure = None if unit in AlexaGlobalCatalog.__dict__.values(): self._unit_of_measure = unit diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 468652bf6d2..3d7c2b118e7 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -2740,3 +2740,175 @@ async def test_cover_semantics(hass): "states": ["Alexa.States.Open"], "range": {"minimumValue": 1, "maximumValue": 100}, } in state_mappings + + +async def test_input_number(hass): + """Test input_number discovery.""" + device = ( + "input_number.test_slider", + 30, + { + "initial": 30, + "min": -20, + "max": 35, + "step": 1, + "mode": "slider", + "friendly_name": "Test Slider", + }, + ) + appliance = await discovery_test(device, hass) + + assert appliance["endpointId"] == "input_number#test_slider" + assert appliance["displayCategories"][0] == "OTHER" + assert appliance["friendlyName"] == "Test Slider" + + capabilities = assert_endpoint_capabilities( + appliance, "Alexa.RangeController", "Alexa.EndpointHealth", "Alexa" + ) + + range_capability = get_capability( + capabilities, "Alexa.RangeController", "input_number.value" + ) + + capability_resources = range_capability["capabilityResources"] + assert capability_resources is not None + assert { + "@type": "text", + "value": {"text": "Value", "locale": "en-US"}, + } in capability_resources["friendlyNames"] + + configuration = range_capability["configuration"] + assert configuration is not None + + supported_range = configuration["supportedRange"] + assert supported_range["minimumValue"] == -20 + assert supported_range["maximumValue"] == 35 + assert supported_range["precision"] == 1 + + presets = configuration["presets"] + assert { + "rangeValue": 35, + "presetResources": { + "friendlyNames": [ + {"@type": "asset", "value": {"assetId": "Alexa.Value.Maximum"}} + ] + }, + } in presets + + assert { + "rangeValue": -20, + "presetResources": { + "friendlyNames": [ + {"@type": "asset", "value": {"assetId": "Alexa.Value.Minimum"}} + ] + }, + } in presets + + call, _ = await assert_request_calls_service( + "Alexa.RangeController", + "SetRangeValue", + "input_number#test_slider", + "input_number.set_value", + hass, + payload={"rangeValue": "10"}, + instance="input_number.value", + ) + assert call.data["value"] == 10 + + await assert_range_changes( + hass, + [(25, "-5"), (35, "5"), (-20, "-100"), (35, "100")], + "Alexa.RangeController", + "AdjustRangeValue", + "input_number#test_slider", + False, + "input_number.set_value", + "value", + instance="input_number.value", + ) + + +async def test_input_number_float(hass): + """Test input_number discovery.""" + device = ( + "input_number.test_slider_float", + 0.5, + { + "initial": 0.5, + "min": 0, + "max": 1, + "step": 0.01, + "mode": "slider", + "friendly_name": "Test Slider Float", + }, + ) + appliance = await discovery_test(device, hass) + + assert appliance["endpointId"] == "input_number#test_slider_float" + assert appliance["displayCategories"][0] == "OTHER" + assert appliance["friendlyName"] == "Test Slider Float" + + capabilities = assert_endpoint_capabilities( + appliance, "Alexa.RangeController", "Alexa.EndpointHealth", "Alexa" + ) + + range_capability = get_capability( + capabilities, "Alexa.RangeController", "input_number.value" + ) + + capability_resources = range_capability["capabilityResources"] + assert capability_resources is not None + assert { + "@type": "text", + "value": {"text": "Value", "locale": "en-US"}, + } in capability_resources["friendlyNames"] + + configuration = range_capability["configuration"] + assert configuration is not None + + supported_range = configuration["supportedRange"] + assert supported_range["minimumValue"] == 0 + assert supported_range["maximumValue"] == 1 + assert supported_range["precision"] == 0.01 + + presets = configuration["presets"] + assert { + "rangeValue": 1, + "presetResources": { + "friendlyNames": [ + {"@type": "asset", "value": {"assetId": "Alexa.Value.Maximum"}} + ] + }, + } in presets + + assert { + "rangeValue": 0, + "presetResources": { + "friendlyNames": [ + {"@type": "asset", "value": {"assetId": "Alexa.Value.Minimum"}} + ] + }, + } in presets + + call, _ = await assert_request_calls_service( + "Alexa.RangeController", + "SetRangeValue", + "input_number#test_slider_float", + "input_number.set_value", + hass, + payload={"rangeValue": "0.333"}, + instance="input_number.value", + ) + assert call.data["value"] == 0.333 + + await assert_range_changes( + hass, + [(0.4, "-0.1"), (0.6, "0.1"), (0, "-100"), (1, "100"), (0.51, "0.01")], + "Alexa.RangeController", + "AdjustRangeValue", + "input_number#test_slider_float", + False, + "input_number.set_value", + "value", + instance="input_number.value", + )