mirror of
https://github.com/home-assistant/core.git
synced 2025-07-10 06:47:09 +00:00
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>
This commit is contained in:
parent
7f2921b0e6
commit
a2678b2aff
@ -1,7 +1,7 @@
|
|||||||
"""Alexa capabilities."""
|
"""Alexa capabilities."""
|
||||||
import logging
|
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
|
from homeassistant.components.alarm_control_panel import ATTR_CODE_FORMAT, FORMAT_NUMBER
|
||||||
import homeassistant.components.climate.const as climate
|
import homeassistant.components.climate.const as climate
|
||||||
import homeassistant.components.media_player.const as media_player
|
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}":
|
if self.instance == f"{cover.DOMAIN}.{cover.ATTR_TILT_POSITION}":
|
||||||
return self.entity.attributes.get(cover.ATTR_CURRENT_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
|
return None
|
||||||
|
|
||||||
def configuration(self):
|
def configuration(self):
|
||||||
@ -1110,6 +1114,28 @@ class AlexaRangeController(AlexaCapability):
|
|||||||
)
|
)
|
||||||
return self._resource.serialize_capability_resources()
|
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
|
return None
|
||||||
|
|
||||||
def semantics(self):
|
def semantics(self):
|
||||||
|
@ -11,6 +11,7 @@ from homeassistant.components import (
|
|||||||
group,
|
group,
|
||||||
image_processing,
|
image_processing,
|
||||||
input_boolean,
|
input_boolean,
|
||||||
|
input_number,
|
||||||
light,
|
light,
|
||||||
lock,
|
lock,
|
||||||
media_player,
|
media_player,
|
||||||
@ -674,3 +675,21 @@ class ImageProcessingCapabilities(AlexaEntity):
|
|||||||
yield AlexaEventDetectionSensor(self.hass, self.entity)
|
yield AlexaEventDetectionSensor(self.hass, self.entity)
|
||||||
yield AlexaEndpointHealth(self.hass, self.entity)
|
yield AlexaEndpointHealth(self.hass, self.entity)
|
||||||
yield Alexa(self.hass)
|
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
|
import math
|
||||||
|
|
||||||
from homeassistant import core as ha
|
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.components.climate import const as climate
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_ENTITY_ID,
|
ATTR_ENTITY_ID,
|
||||||
@ -1080,12 +1087,12 @@ async def async_api_set_range(hass, config, directive, context):
|
|||||||
domain = entity.domain
|
domain = entity.domain
|
||||||
service = None
|
service = None
|
||||||
data = {ATTR_ENTITY_ID: entity.entity_id}
|
data = {ATTR_ENTITY_ID: entity.entity_id}
|
||||||
range_value = int(directive.payload["rangeValue"])
|
range_value = directive.payload["rangeValue"]
|
||||||
|
|
||||||
# Fan Speed
|
# Fan Speed
|
||||||
if instance == f"{fan.DOMAIN}.{fan.ATTR_SPEED}":
|
if instance == f"{fan.DOMAIN}.{fan.ATTR_SPEED}":
|
||||||
service = fan.SERVICE_SET_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:
|
if not speed:
|
||||||
msg = "Entity does not support value"
|
msg = "Entity does not support value"
|
||||||
@ -1098,6 +1105,7 @@ async def async_api_set_range(hass, config, directive, context):
|
|||||||
|
|
||||||
# Cover Position
|
# Cover Position
|
||||||
elif instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
|
elif instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
|
||||||
|
range_value = int(range_value)
|
||||||
if range_value == 0:
|
if range_value == 0:
|
||||||
service = cover.SERVICE_CLOSE_COVER
|
service = cover.SERVICE_CLOSE_COVER
|
||||||
elif range_value == 100:
|
elif range_value == 100:
|
||||||
@ -1108,6 +1116,7 @@ async def async_api_set_range(hass, config, directive, context):
|
|||||||
|
|
||||||
# Cover Tilt Position
|
# Cover Tilt Position
|
||||||
elif instance == f"{cover.DOMAIN}.{cover.ATTR_TILT_POSITION}":
|
elif instance == f"{cover.DOMAIN}.{cover.ATTR_TILT_POSITION}":
|
||||||
|
range_value = int(range_value)
|
||||||
if range_value == 0:
|
if range_value == 0:
|
||||||
service = cover.SERVICE_CLOSE_COVER_TILT
|
service = cover.SERVICE_CLOSE_COVER_TILT
|
||||||
elif range_value == 100:
|
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
|
service = cover.SERVICE_SET_COVER_TILT_POSITION
|
||||||
data[cover.ATTR_POSITION] = range_value
|
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:
|
else:
|
||||||
msg = "Entity does not support directive"
|
msg = "Entity does not support directive"
|
||||||
raise AlexaInvalidDirectiveError(msg)
|
raise AlexaInvalidDirectiveError(msg)
|
||||||
@ -1145,11 +1162,12 @@ async def async_api_adjust_range(hass, config, directive, context):
|
|||||||
domain = entity.domain
|
domain = entity.domain
|
||||||
service = None
|
service = None
|
||||||
data = {ATTR_ENTITY_ID: entity.entity_id}
|
data = {ATTR_ENTITY_ID: entity.entity_id}
|
||||||
range_delta = int(directive.payload["rangeValueDelta"])
|
range_delta = directive.payload["rangeValueDelta"]
|
||||||
response_value = 0
|
response_value = 0
|
||||||
|
|
||||||
# Fan Speed
|
# Fan Speed
|
||||||
if instance == f"{fan.DOMAIN}.{fan.ATTR_SPEED}":
|
if instance == f"{fan.DOMAIN}.{fan.ATTR_SPEED}":
|
||||||
|
range_delta = int(range_delta)
|
||||||
service = fan.SERVICE_SET_SPEED
|
service = fan.SERVICE_SET_SPEED
|
||||||
current_range = RANGE_FAN_MAP.get(entity.attributes.get(fan.ATTR_SPEED), 0)
|
current_range = RANGE_FAN_MAP.get(entity.attributes.get(fan.ATTR_SPEED), 0)
|
||||||
speed = SPEED_FAN_MAP.get(
|
speed = SPEED_FAN_MAP.get(
|
||||||
@ -1163,6 +1181,7 @@ async def async_api_adjust_range(hass, config, directive, context):
|
|||||||
|
|
||||||
# Cover Position
|
# Cover Position
|
||||||
elif instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
|
elif instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
|
||||||
|
range_delta = int(range_delta)
|
||||||
service = SERVICE_SET_COVER_POSITION
|
service = SERVICE_SET_COVER_POSITION
|
||||||
current = entity.attributes.get(cover.ATTR_POSITION)
|
current = entity.attributes.get(cover.ATTR_POSITION)
|
||||||
data[cover.ATTR_POSITION] = response_value = min(
|
data[cover.ATTR_POSITION] = response_value = min(
|
||||||
@ -1171,12 +1190,24 @@ async def async_api_adjust_range(hass, config, directive, context):
|
|||||||
|
|
||||||
# Cover Tilt Position
|
# Cover Tilt Position
|
||||||
elif instance == f"{cover.DOMAIN}.{cover.ATTR_TILT_POSITION}":
|
elif instance == f"{cover.DOMAIN}.{cover.ATTR_TILT_POSITION}":
|
||||||
|
range_delta = int(range_delta)
|
||||||
service = SERVICE_SET_COVER_TILT_POSITION
|
service = SERVICE_SET_COVER_TILT_POSITION
|
||||||
current = entity.attributes.get(cover.ATTR_TILT_POSITION)
|
current = entity.attributes.get(cover.ATTR_TILT_POSITION)
|
||||||
data[cover.ATTR_TILT_POSITION] = response_value = min(
|
data[cover.ATTR_TILT_POSITION] = response_value = min(
|
||||||
100, max(0, range_delta + current)
|
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:
|
else:
|
||||||
msg = "Entity does not support directive"
|
msg = "Entity does not support directive"
|
||||||
raise AlexaInvalidDirectiveError(msg)
|
raise AlexaInvalidDirectiveError(msg)
|
||||||
|
@ -266,9 +266,9 @@ class AlexaPresetResource(AlexaCapabilityResource):
|
|||||||
"""Initialize an Alexa presetResource."""
|
"""Initialize an Alexa presetResource."""
|
||||||
super().__init__(labels)
|
super().__init__(labels)
|
||||||
self._presets = []
|
self._presets = []
|
||||||
self._minimum_value = int(min_value)
|
self._minimum_value = min_value
|
||||||
self._maximum_value = int(max_value)
|
self._maximum_value = max_value
|
||||||
self._precision = int(precision)
|
self._precision = precision
|
||||||
self._unit_of_measure = None
|
self._unit_of_measure = None
|
||||||
if unit in AlexaGlobalCatalog.__dict__.values():
|
if unit in AlexaGlobalCatalog.__dict__.values():
|
||||||
self._unit_of_measure = unit
|
self._unit_of_measure = unit
|
||||||
|
@ -2740,3 +2740,175 @@ async def test_cover_semantics(hass):
|
|||||||
"states": ["Alexa.States.Open"],
|
"states": ["Alexa.States.Open"],
|
||||||
"range": {"minimumValue": 1, "maximumValue": 100},
|
"range": {"minimumValue": 1, "maximumValue": 100},
|
||||||
} in state_mappings
|
} 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…
x
Reference in New Issue
Block a user