mirror of
https://github.com/home-assistant/core.git
synced 2025-07-13 00:07:10 +00:00
Add support for vacuums to Alexa. (#30764)
This commit is contained in:
parent
1e82813c3b
commit
bb42ff93f4
@ -1,7 +1,14 @@
|
|||||||
"""Alexa capabilities."""
|
"""Alexa capabilities."""
|
||||||
import logging
|
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
|
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
|
||||||
@ -1291,6 +1298,15 @@ class AlexaRangeController(AlexaCapability):
|
|||||||
if self.instance == f"{input_number.DOMAIN}.{input_number.ATTR_VALUE}":
|
if self.instance == f"{input_number.DOMAIN}.{input_number.ATTR_VALUE}":
|
||||||
return float(self.entity.state)
|
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
|
return None
|
||||||
|
|
||||||
def configuration(self):
|
def configuration(self):
|
||||||
@ -1367,6 +1383,26 @@ class AlexaRangeController(AlexaCapability):
|
|||||||
)
|
)
|
||||||
return self._resource.serialize_capability_resources()
|
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
|
return None
|
||||||
|
|
||||||
def semantics(self):
|
def semantics(self):
|
||||||
|
@ -20,6 +20,7 @@ from homeassistant.components import (
|
|||||||
sensor,
|
sensor,
|
||||||
switch,
|
switch,
|
||||||
timer,
|
timer,
|
||||||
|
vacuum,
|
||||||
)
|
)
|
||||||
from homeassistant.components.climate import const as climate
|
from homeassistant.components.climate import const as climate
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
@ -724,3 +725,34 @@ class TimerCapabilities(AlexaEntity):
|
|||||||
"""Yield the supported interfaces."""
|
"""Yield the supported interfaces."""
|
||||||
yield AlexaTimeHoldController(self.entity, allow_remote_resume=True)
|
yield AlexaTimeHoldController(self.entity, allow_remote_resume=True)
|
||||||
yield Alexa(self.entity)
|
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,
|
light,
|
||||||
media_player,
|
media_player,
|
||||||
timer,
|
timer,
|
||||||
|
vacuum,
|
||||||
)
|
)
|
||||||
from homeassistant.components.climate import const as climate
|
from homeassistant.components.climate import const as climate
|
||||||
from homeassistant.const import (
|
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])
|
max_value = float(entity.attributes[input_number.ATTR_MAX])
|
||||||
data[input_number.ATTR_VALUE] = min(max_value, max(min_value, range_value))
|
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:
|
else:
|
||||||
msg = "Entity does not support directive"
|
msg = "Entity does not support directive"
|
||||||
raise AlexaInvalidDirectiveError(msg)
|
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)
|
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:
|
else:
|
||||||
msg = "Entity does not support directive"
|
msg = "Entity does not support directive"
|
||||||
raise AlexaInvalidDirectiveError(msg)
|
raise AlexaInvalidDirectiveError(msg)
|
||||||
@ -1412,8 +1445,18 @@ async def async_api_hold(hass, config, directive, context):
|
|||||||
entity = directive.entity
|
entity = directive.entity
|
||||||
data = {ATTR_ENTITY_ID: entity.entity_id}
|
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(
|
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()
|
return directive.response()
|
||||||
@ -1425,8 +1468,18 @@ async def async_api_resume(hass, config, directive, context):
|
|||||||
entity = directive.entity
|
entity = directive.entity
|
||||||
data = {ATTR_ENTITY_ID: entity.entity_id}
|
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(
|
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()
|
return directive.response()
|
||||||
|
@ -17,6 +17,7 @@ from homeassistant.components.media_player.const import (
|
|||||||
SUPPORT_VOLUME_MUTE,
|
SUPPORT_VOLUME_MUTE,
|
||||||
SUPPORT_VOLUME_SET,
|
SUPPORT_VOLUME_SET,
|
||||||
)
|
)
|
||||||
|
import homeassistant.components.vacuum as vacuum
|
||||||
from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT
|
from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT
|
||||||
from homeassistant.core import Context, callback
|
from homeassistant.core import Context, callback
|
||||||
from homeassistant.helpers import entityfilter
|
from homeassistant.helpers import entityfilter
|
||||||
@ -3134,3 +3135,223 @@ async def test_timer_resume(hass):
|
|||||||
await assert_request_calls_service(
|
await assert_request_calls_service(
|
||||||
"Alexa.TimeHoldController", "Resume", "timer#laundry", "timer.start", hass
|
"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…
x
Reference in New Issue
Block a user