Add support for vacuums to Alexa. (#30764)
parent
1e82813c3b
commit
bb42ff93f4
|
@ -1,7 +1,14 @@
|
|||
"""Alexa capabilities."""
|
||||
import logging
|
||||
|
||||
from homeassistant.components import cover, fan, image_processing, input_number, light
|
||||
from homeassistant.components import (
|
||||
cover,
|
||||
fan,
|
||||
image_processing,
|
||||
input_number,
|
||||
light,
|
||||
vacuum,
|
||||
)
|
||||
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
|
||||
|
@ -1291,6 +1298,15 @@ class AlexaRangeController(AlexaCapability):
|
|||
if self.instance == f"{input_number.DOMAIN}.{input_number.ATTR_VALUE}":
|
||||
return float(self.entity.state)
|
||||
|
||||
# Vacuum Fan Speed
|
||||
if self.instance == f"{vacuum.DOMAIN}.{vacuum.ATTR_FAN_SPEED}":
|
||||
speed_list = self.entity.attributes[vacuum.ATTR_FAN_SPEED_LIST]
|
||||
speed = self.entity.attributes[vacuum.ATTR_FAN_SPEED]
|
||||
speed_index = next(
|
||||
(i for i, v in enumerate(speed_list) if v == speed), None
|
||||
)
|
||||
return speed_index
|
||||
|
||||
return None
|
||||
|
||||
def configuration(self):
|
||||
|
@ -1367,6 +1383,26 @@ class AlexaRangeController(AlexaCapability):
|
|||
)
|
||||
return self._resource.serialize_capability_resources()
|
||||
|
||||
# Vacuum Fan Speed Resources
|
||||
if self.instance == f"{vacuum.DOMAIN}.{vacuum.ATTR_FAN_SPEED}":
|
||||
speed_list = self.entity.attributes[vacuum.ATTR_FAN_SPEED_LIST]
|
||||
max_value = len(speed_list) - 1
|
||||
self._resource = AlexaPresetResource(
|
||||
labels=[AlexaGlobalCatalog.SETTING_FAN_SPEED],
|
||||
min_value=0,
|
||||
max_value=max_value,
|
||||
precision=1,
|
||||
)
|
||||
for index, speed in enumerate(speed_list):
|
||||
labels = [speed.replace("_", " ")]
|
||||
if index == 1:
|
||||
labels.append(AlexaGlobalCatalog.VALUE_MINIMUM)
|
||||
if index == max_value:
|
||||
labels.append(AlexaGlobalCatalog.VALUE_MAXIMUM)
|
||||
self._resource.add_preset(value=index, labels=labels)
|
||||
|
||||
return self._resource.serialize_capability_resources()
|
||||
|
||||
return None
|
||||
|
||||
def semantics(self):
|
||||
|
|
|
@ -20,6 +20,7 @@ from homeassistant.components import (
|
|||
sensor,
|
||||
switch,
|
||||
timer,
|
||||
vacuum,
|
||||
)
|
||||
from homeassistant.components.climate import const as climate
|
||||
from homeassistant.const import (
|
||||
|
@ -724,3 +725,34 @@ class TimerCapabilities(AlexaEntity):
|
|||
"""Yield the supported interfaces."""
|
||||
yield AlexaTimeHoldController(self.entity, allow_remote_resume=True)
|
||||
yield Alexa(self.entity)
|
||||
|
||||
|
||||
@ENTITY_ADAPTERS.register(vacuum.DOMAIN)
|
||||
class VacuumCapabilities(AlexaEntity):
|
||||
"""Class to represent vacuum capabilities."""
|
||||
|
||||
def default_display_categories(self):
|
||||
"""Return the display categories for this entity."""
|
||||
return [DisplayCategory.OTHER]
|
||||
|
||||
def interfaces(self):
|
||||
"""Yield the supported interfaces."""
|
||||
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||
if (supported & vacuum.SUPPORT_TURN_ON) and (
|
||||
supported & vacuum.SUPPORT_TURN_OFF
|
||||
):
|
||||
yield AlexaPowerController(self.entity)
|
||||
|
||||
if supported & vacuum.SUPPORT_FAN_SPEED:
|
||||
yield AlexaRangeController(
|
||||
self.entity, instance=f"{vacuum.DOMAIN}.{vacuum.ATTR_FAN_SPEED}"
|
||||
)
|
||||
|
||||
if supported & vacuum.SUPPORT_PAUSE:
|
||||
support_resume = bool(supported & vacuum.SUPPORT_START)
|
||||
yield AlexaTimeHoldController(
|
||||
self.entity, allow_remote_resume=support_resume
|
||||
)
|
||||
|
||||
yield AlexaEndpointHealth(self.hass, self.entity)
|
||||
yield Alexa(self.hass)
|
||||
|
|
|
@ -11,6 +11,7 @@ from homeassistant.components import (
|
|||
light,
|
||||
media_player,
|
||||
timer,
|
||||
vacuum,
|
||||
)
|
||||
from homeassistant.components.climate import const as climate
|
||||
from homeassistant.const import (
|
||||
|
@ -1138,6 +1139,20 @@ async def async_api_set_range(hass, config, directive, context):
|
|||
max_value = float(entity.attributes[input_number.ATTR_MAX])
|
||||
data[input_number.ATTR_VALUE] = min(max_value, max(min_value, range_value))
|
||||
|
||||
# Vacuum Fan Speed
|
||||
elif instance == f"{vacuum.DOMAIN}.{vacuum.ATTR_FAN_SPEED}":
|
||||
service = vacuum.SERVICE_SET_FAN_SPEED
|
||||
speed_list = entity.attributes[vacuum.ATTR_FAN_SPEED_LIST]
|
||||
speed = next(
|
||||
(v for i, v in enumerate(speed_list) if i == int(range_value)), None
|
||||
)
|
||||
|
||||
if not speed:
|
||||
msg = "Entity does not support value"
|
||||
raise AlexaInvalidValueError(msg)
|
||||
|
||||
data[vacuum.ATTR_FAN_SPEED] = speed
|
||||
|
||||
else:
|
||||
msg = "Entity does not support directive"
|
||||
raise AlexaInvalidDirectiveError(msg)
|
||||
|
@ -1220,6 +1235,24 @@ async def async_api_adjust_range(hass, config, directive, context):
|
|||
max_value, max(min_value, range_delta + current)
|
||||
)
|
||||
|
||||
# Vacuum Fan Speed
|
||||
elif instance == f"{vacuum.DOMAIN}.{vacuum.ATTR_FAN_SPEED}":
|
||||
range_delta = int(range_delta)
|
||||
service = vacuum.SERVICE_SET_FAN_SPEED
|
||||
speed_list = entity.attributes[vacuum.ATTR_FAN_SPEED_LIST]
|
||||
current_speed = entity.attributes[vacuum.ATTR_FAN_SPEED]
|
||||
current_speed_index = next(
|
||||
(i for i, v in enumerate(speed_list) if v == current_speed), 0
|
||||
)
|
||||
new_speed_index = min(
|
||||
len(speed_list) - 1, max(0, current_speed_index + range_delta)
|
||||
)
|
||||
speed = next(
|
||||
(v for i, v in enumerate(speed_list) if i == new_speed_index), None
|
||||
)
|
||||
|
||||
data[vacuum.ATTR_FAN_SPEED] = response_value = speed
|
||||
|
||||
else:
|
||||
msg = "Entity does not support directive"
|
||||
raise AlexaInvalidDirectiveError(msg)
|
||||
|
@ -1412,8 +1445,18 @@ async def async_api_hold(hass, config, directive, context):
|
|||
entity = directive.entity
|
||||
data = {ATTR_ENTITY_ID: entity.entity_id}
|
||||
|
||||
if entity.domain == timer.DOMAIN:
|
||||
service = timer.SERVICE_PAUSE
|
||||
|
||||
elif entity.domain == vacuum.DOMAIN:
|
||||
service = vacuum.SERVICE_START_PAUSE
|
||||
|
||||
else:
|
||||
msg = "Entity does not support directive"
|
||||
raise AlexaInvalidDirectiveError(msg)
|
||||
|
||||
await hass.services.async_call(
|
||||
entity.domain, timer.SERVICE_PAUSE, data, blocking=False, context=context
|
||||
entity.domain, service, data, blocking=False, context=context
|
||||
)
|
||||
|
||||
return directive.response()
|
||||
|
@ -1425,8 +1468,18 @@ async def async_api_resume(hass, config, directive, context):
|
|||
entity = directive.entity
|
||||
data = {ATTR_ENTITY_ID: entity.entity_id}
|
||||
|
||||
if entity.domain == timer.DOMAIN:
|
||||
service = timer.SERVICE_START
|
||||
|
||||
elif entity.domain == vacuum.DOMAIN:
|
||||
service = vacuum.SERVICE_START_PAUSE
|
||||
|
||||
else:
|
||||
msg = "Entity does not support directive"
|
||||
raise AlexaInvalidDirectiveError(msg)
|
||||
|
||||
await hass.services.async_call(
|
||||
entity.domain, timer.SERVICE_START, data, blocking=False, context=context
|
||||
entity.domain, service, data, blocking=False, context=context
|
||||
)
|
||||
|
||||
return directive.response()
|
||||
|
|
|
@ -17,6 +17,7 @@ from homeassistant.components.media_player.const import (
|
|||
SUPPORT_VOLUME_MUTE,
|
||||
SUPPORT_VOLUME_SET,
|
||||
)
|
||||
import homeassistant.components.vacuum as vacuum
|
||||
from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT
|
||||
from homeassistant.core import Context, callback
|
||||
from homeassistant.helpers import entityfilter
|
||||
|
@ -3134,3 +3135,223 @@ async def test_timer_resume(hass):
|
|||
await assert_request_calls_service(
|
||||
"Alexa.TimeHoldController", "Resume", "timer#laundry", "timer.start", hass
|
||||
)
|
||||
|
||||
|
||||
async def test_vacuum_discovery(hass):
|
||||
"""Test vacuum discovery."""
|
||||
device = (
|
||||
"vacuum.test_1",
|
||||
"docked",
|
||||
{
|
||||
"friendly_name": "Test vacuum 1",
|
||||
"supported_features": vacuum.SUPPORT_TURN_ON
|
||||
| vacuum.SUPPORT_TURN_OFF
|
||||
| vacuum.SUPPORT_START
|
||||
| vacuum.SUPPORT_STOP
|
||||
| vacuum.SUPPORT_PAUSE,
|
||||
},
|
||||
)
|
||||
appliance = await discovery_test(device, hass)
|
||||
|
||||
assert appliance["endpointId"] == "vacuum#test_1"
|
||||
assert appliance["displayCategories"][0] == "OTHER"
|
||||
assert appliance["friendlyName"] == "Test vacuum 1"
|
||||
|
||||
assert_endpoint_capabilities(
|
||||
appliance,
|
||||
"Alexa.PowerController",
|
||||
"Alexa.TimeHoldController",
|
||||
"Alexa.EndpointHealth",
|
||||
"Alexa",
|
||||
)
|
||||
|
||||
|
||||
async def test_vacuum_fan_speed(hass):
|
||||
"""Test vacuum fan speed with rangeController."""
|
||||
device = (
|
||||
"vacuum.test_2",
|
||||
"cleaning",
|
||||
{
|
||||
"friendly_name": "Test vacuum 2",
|
||||
"supported_features": vacuum.SUPPORT_TURN_ON
|
||||
| vacuum.SUPPORT_TURN_OFF
|
||||
| vacuum.SUPPORT_START
|
||||
| vacuum.SUPPORT_STOP
|
||||
| vacuum.SUPPORT_PAUSE
|
||||
| vacuum.SUPPORT_FAN_SPEED,
|
||||
"fan_speed_list": ["off", "low", "medium", "high", "turbo", "super_sucker"],
|
||||
"fan_speed": "medium",
|
||||
},
|
||||
)
|
||||
appliance = await discovery_test(device, hass)
|
||||
|
||||
assert appliance["endpointId"] == "vacuum#test_2"
|
||||
assert appliance["displayCategories"][0] == "OTHER"
|
||||
assert appliance["friendlyName"] == "Test vacuum 2"
|
||||
|
||||
capabilities = assert_endpoint_capabilities(
|
||||
appliance,
|
||||
"Alexa.PowerController",
|
||||
"Alexa.RangeController",
|
||||
"Alexa.TimeHoldController",
|
||||
"Alexa.EndpointHealth",
|
||||
"Alexa",
|
||||
)
|
||||
|
||||
range_capability = get_capability(capabilities, "Alexa.RangeController")
|
||||
assert range_capability is not None
|
||||
assert range_capability["instance"] == "vacuum.fan_speed"
|
||||
|
||||
capability_resources = range_capability["capabilityResources"]
|
||||
assert capability_resources is not None
|
||||
assert {
|
||||
"@type": "asset",
|
||||
"value": {"assetId": "Alexa.Setting.FanSpeed"},
|
||||
} 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"] == 5
|
||||
assert supported_range["precision"] == 1
|
||||
|
||||
presets = configuration["presets"]
|
||||
assert {
|
||||
"rangeValue": 0,
|
||||
"presetResources": {
|
||||
"friendlyNames": [
|
||||
{"@type": "text", "value": {"text": "off", "locale": "en-US"}}
|
||||
]
|
||||
},
|
||||
} in presets
|
||||
|
||||
assert {
|
||||
"rangeValue": 1,
|
||||
"presetResources": {
|
||||
"friendlyNames": [
|
||||
{"@type": "text", "value": {"text": "low", "locale": "en-US"}},
|
||||
{"@type": "asset", "value": {"assetId": "Alexa.Value.Minimum"}},
|
||||
]
|
||||
},
|
||||
} in presets
|
||||
|
||||
assert {
|
||||
"rangeValue": 2,
|
||||
"presetResources": {
|
||||
"friendlyNames": [
|
||||
{"@type": "text", "value": {"text": "medium", "locale": "en-US"}}
|
||||
]
|
||||
},
|
||||
} in presets
|
||||
|
||||
assert {
|
||||
"rangeValue": 5,
|
||||
"presetResources": {
|
||||
"friendlyNames": [
|
||||
{"@type": "text", "value": {"text": "super sucker", "locale": "en-US"}},
|
||||
{"@type": "asset", "value": {"assetId": "Alexa.Value.Maximum"}},
|
||||
]
|
||||
},
|
||||
} in presets
|
||||
|
||||
call, _ = await assert_request_calls_service(
|
||||
"Alexa.RangeController",
|
||||
"SetRangeValue",
|
||||
"vacuum#test_2",
|
||||
"vacuum.set_fan_speed",
|
||||
hass,
|
||||
payload={"rangeValue": "1"},
|
||||
instance="vacuum.fan_speed",
|
||||
)
|
||||
assert call.data["fan_speed"] == "low"
|
||||
|
||||
call, _ = await assert_request_calls_service(
|
||||
"Alexa.RangeController",
|
||||
"SetRangeValue",
|
||||
"vacuum#test_2",
|
||||
"vacuum.set_fan_speed",
|
||||
hass,
|
||||
payload={"rangeValue": "5"},
|
||||
instance="vacuum.fan_speed",
|
||||
)
|
||||
assert call.data["fan_speed"] == "super_sucker"
|
||||
|
||||
await assert_range_changes(
|
||||
hass,
|
||||
[("low", "-1"), ("high", "1"), ("medium", "0"), ("super_sucker", "99")],
|
||||
"Alexa.RangeController",
|
||||
"AdjustRangeValue",
|
||||
"vacuum#test_2",
|
||||
False,
|
||||
"vacuum.set_fan_speed",
|
||||
"fan_speed",
|
||||
instance="vacuum.fan_speed",
|
||||
)
|
||||
|
||||
|
||||
async def test_vacuum_pause(hass):
|
||||
"""Test vacuum pause with TimeHoldController."""
|
||||
device = (
|
||||
"vacuum.test_3",
|
||||
"cleaning",
|
||||
{
|
||||
"friendly_name": "Test vacuum 3",
|
||||
"supported_features": vacuum.SUPPORT_TURN_ON
|
||||
| vacuum.SUPPORT_TURN_OFF
|
||||
| vacuum.SUPPORT_START
|
||||
| vacuum.SUPPORT_STOP
|
||||
| vacuum.SUPPORT_PAUSE
|
||||
| vacuum.SUPPORT_FAN_SPEED,
|
||||
"fan_speed_list": ["off", "low", "medium", "high", "turbo", "super_sucker"],
|
||||
"fan_speed": "medium",
|
||||
},
|
||||
)
|
||||
appliance = await discovery_test(device, hass)
|
||||
|
||||
capabilities = assert_endpoint_capabilities(
|
||||
appliance,
|
||||
"Alexa.PowerController",
|
||||
"Alexa.RangeController",
|
||||
"Alexa.TimeHoldController",
|
||||
"Alexa.EndpointHealth",
|
||||
"Alexa",
|
||||
)
|
||||
|
||||
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", "vacuum#test_3", "vacuum.start_pause", hass
|
||||
)
|
||||
|
||||
|
||||
async def test_vacuum_resume(hass):
|
||||
"""Test vacuum resume with TimeHoldController."""
|
||||
device = (
|
||||
"vacuum.test_4",
|
||||
"docked",
|
||||
{
|
||||
"friendly_name": "Test vacuum 4",
|
||||
"supported_features": vacuum.SUPPORT_TURN_ON
|
||||
| vacuum.SUPPORT_TURN_OFF
|
||||
| vacuum.SUPPORT_START
|
||||
| vacuum.SUPPORT_STOP
|
||||
| vacuum.SUPPORT_PAUSE
|
||||
| vacuum.SUPPORT_FAN_SPEED,
|
||||
"fan_speed_list": ["off", "low", "medium", "high", "turbo", "super_sucker"],
|
||||
"fan_speed": "medium",
|
||||
},
|
||||
)
|
||||
await discovery_test(device, hass)
|
||||
|
||||
await assert_request_calls_service(
|
||||
"Alexa.TimeHoldController",
|
||||
"Resume",
|
||||
"vacuum#test_4",
|
||||
"vacuum.start_pause",
|
||||
hass,
|
||||
)
|
||||
|
|
Loading…
Reference in New Issue