Add support for input_number entities in Alexa integration (#30139)

* Add support for input_number entities

* Update homeassistant/components/alexa/capabilities.py

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

* Removed get methods to directly access required attributes dicts.

Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
pull/30178/head
ochlocracy 2019-12-23 09:17:37 -05:00 committed by Paulus Schoutsen
parent 7f2921b0e6
commit a2678b2aff
5 changed files with 256 additions and 8 deletions

View File

@ -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):

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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",
)