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
parent
7f2921b0e6
commit
a2678b2aff
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
)
|
||||
|
|
Loading…
Reference in New Issue