Add support for vacuums to Alexa. (#30764)

pull/30797/head
ochlocracy 2020-01-15 12:15:31 -05:00 committed by Paulus Schoutsen
parent 1e82813c3b
commit bb42ff93f4
4 changed files with 345 additions and 3 deletions

View File

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

View File

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

View File

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

View File

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