Add Alexa.ModeController to cover entities, adds open/close utterances! (#28309)

* Added Alexa.ModeController to cover entities.

* Added synonyms for directives.

* Updated tests for additional synonyms for directives.

* Added Alexa.ModeController to cover entities.

* Sacrifice unused variable in split() to please the Pylint gods.

* Removed duplicate instance check.

* Corrected variable name, clarified definition and consistency.

* Changed list to tuple.
pull/28253/head
ochlocracy 2019-11-25 18:07:33 -05:00 committed by Paulus Schoutsen
parent dc8c085872
commit 5ea5db32d2
4 changed files with 189 additions and 9 deletions

View File

@ -9,9 +9,11 @@ from homeassistant.const import (
STATE_ALARM_ARMED_CUSTOM_BYPASS,
STATE_ALARM_ARMED_HOME,
STATE_ALARM_ARMED_NIGHT,
STATE_CLOSED,
STATE_LOCKED,
STATE_OFF,
STATE_ON,
STATE_OPEN,
STATE_PAUSED,
STATE_PLAYING,
STATE_UNAVAILABLE,
@ -888,6 +890,9 @@ class AlexaModeController(AlexaCapability):
if self.instance == f"{fan.DOMAIN}.{fan.ATTR_DIRECTION}":
return self.entity.attributes.get(fan.ATTR_DIRECTION)
if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
return self.entity.attributes.get(cover.ATTR_POSITION)
return None
def configuration(self):
@ -903,6 +908,12 @@ class AlexaModeController(AlexaCapability):
{"type": Catalog.LABEL_ASSET, "value": Catalog.SETTING_DIRECTION}
]
if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
capability_resources = [
{"type": Catalog.LABEL_ASSET, "value": Catalog.SETTING_MODE},
{"type": Catalog.LABEL_ASSET, "value": Catalog.SETTING_PRESET},
]
return capability_resources
def mode_resources(self):
@ -927,6 +938,32 @@ class AlexaModeController(AlexaCapability):
],
}
if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
mode_resources = {
"ordered": False,
"resources": [
{
"value": f"{cover.ATTR_POSITION}.{STATE_OPEN}",
"friendly_names": [
{"type": Catalog.LABEL_TEXT, "value": "open"},
{"type": Catalog.LABEL_TEXT, "value": "opened"},
{"type": Catalog.LABEL_TEXT, "value": "raise"},
{"type": Catalog.LABEL_TEXT, "value": "raised"},
],
},
{
"value": f"{cover.ATTR_POSITION}.{STATE_CLOSED}",
"friendly_names": [
{"type": Catalog.LABEL_TEXT, "value": "close"},
{"type": Catalog.LABEL_TEXT, "value": "closed"},
{"type": Catalog.LABEL_TEXT, "value": "shut"},
{"type": Catalog.LABEL_TEXT, "value": "lower"},
{"type": Catalog.LABEL_TEXT, "value": "lowered"},
],
},
],
}
return mode_resources
def serialize_mode_resources(self):

View File

@ -311,7 +311,10 @@ class CoverCapabilities(AlexaEntity):
def default_display_categories(self):
"""Return the display categories for this entity."""
return [DisplayCategory.DOOR]
device_class = self.entity.attributes.get(ATTR_DEVICE_CLASS)
if device_class in (cover.DEVICE_CLASS_GARAGE, cover.DEVICE_CLASS_DOOR):
return [DisplayCategory.DOOR]
return [DisplayCategory.OTHER]
def interfaces(self):
"""Yield the supported interfaces."""
@ -319,6 +322,10 @@ class CoverCapabilities(AlexaEntity):
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
if supported & cover.SUPPORT_SET_POSITION:
yield AlexaPercentageController(self.entity)
if supported & (cover.SUPPORT_CLOSE | cover.SUPPORT_OPEN):
yield AlexaModeController(
self.entity, instance=f"{cover.DOMAIN}.{cover.ATTR_POSITION}"
)
yield AlexaEndpointHealth(self.hass, self.entity)

View File

@ -9,7 +9,6 @@ from homeassistant.const import (
ATTR_ENTITY_ID,
ATTR_SUPPORTED_FEATURES,
ATTR_TEMPERATURE,
STATE_ALARM_DISARMED,
SERVICE_ALARM_ARM_AWAY,
SERVICE_ALARM_ARM_HOME,
SERVICE_ALARM_ARM_NIGHT,
@ -28,6 +27,9 @@ from homeassistant.const import (
SERVICE_VOLUME_MUTE,
SERVICE_VOLUME_SET,
SERVICE_VOLUME_UP,
STATE_ALARM_DISARMED,
STATE_CLOSED,
STATE_OPEN,
TEMP_CELSIUS,
TEMP_FAHRENHEIT,
)
@ -956,23 +958,42 @@ async def async_api_set_mode(hass, config, directive, context):
domain = entity.domain
service = None
data = {ATTR_ENTITY_ID: entity.entity_id}
mode = directive.payload["mode"]
capability_mode = directive.payload["mode"]
if domain != fan.DOMAIN:
if domain not in (fan.DOMAIN, cover.DOMAIN):
msg = "Entity does not support directive"
raise AlexaInvalidDirectiveError(msg)
if instance == f"{fan.DOMAIN}.{fan.ATTR_DIRECTION}":
mode, direction = mode.split(".")
if direction in [fan.DIRECTION_REVERSE, fan.DIRECTION_FORWARD]:
_, direction = capability_mode.split(".")
if direction in (fan.DIRECTION_REVERSE, fan.DIRECTION_FORWARD):
service = fan.SERVICE_SET_DIRECTION
data[fan.ATTR_DIRECTION] = direction
if instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
_, position = capability_mode.split(".")
if position == STATE_CLOSED:
service = cover.SERVICE_CLOSE_COVER
if position == STATE_OPEN:
service = cover.SERVICE_OPEN_COVER
await hass.services.async_call(
domain, service, data, blocking=False, context=context
)
return directive.response()
response = directive.response()
response.add_context_property(
{
"namespace": "Alexa.ModeController",
"instance": instance,
"name": "mode",
"value": capability_mode,
}
)
return response
@HANDLERS.register(("Alexa.ModeController", "AdjustMode"))

View File

@ -565,7 +565,7 @@ async def test_direction_fan(hass):
},
} in supported_modes
call, _ = await assert_request_calls_service(
call, msg = await assert_request_calls_service(
"Alexa.ModeController",
"SetMode",
"fan#test_4",
@ -575,6 +575,25 @@ async def test_direction_fan(hass):
instance="fan.direction",
)
assert call.data["direction"] == "reverse"
properties = msg["context"]["properties"][0]
assert properties["name"] == "mode"
assert properties["namespace"] == "Alexa.ModeController"
assert properties["value"] == "direction.reverse"
call, msg = await assert_request_calls_service(
"Alexa.ModeController",
"SetMode",
"fan#test_4",
"fan.set_direction",
hass,
payload={"mode": "direction.forward"},
instance="fan.direction",
)
assert call.data["direction"] == "forward"
properties = msg["context"]["properties"][0]
assert properties["name"] == "mode"
assert properties["namespace"] == "Alexa.ModeController"
assert properties["value"] == "direction.forward"
# Test for AdjustMode instance=None Error coverage
with pytest.raises(AssertionError):
@ -1190,11 +1209,12 @@ async def test_cover(hass):
appliance = await discovery_test(device, hass)
assert appliance["endpointId"] == "cover#test"
assert appliance["displayCategories"][0] == "DOOR"
assert appliance["displayCategories"][0] == "OTHER"
assert appliance["friendlyName"] == "Test cover"
assert_endpoint_capabilities(
appliance,
"Alexa.ModeController",
"Alexa.PercentageController",
"Alexa.PowerController",
"Alexa.EndpointHealth",
@ -2076,3 +2096,98 @@ async def test_mode_unsupported_domain(hass):
assert msg["header"]["name"] == "ErrorResponse"
assert msg["header"]["namespace"] == "Alexa"
assert msg["payload"]["type"] == "INVALID_DIRECTIVE"
async def test_cover_position(hass):
"""Test cover position mode discovery."""
device = (
"cover.test",
"off",
{"friendly_name": "Test cover", "supported_features": 255, "position": 30},
)
appliance = await discovery_test(device, hass)
assert appliance["endpointId"] == "cover#test"
assert appliance["displayCategories"][0] == "OTHER"
assert appliance["friendlyName"] == "Test cover"
capabilities = assert_endpoint_capabilities(
appliance,
"Alexa.ModeController",
"Alexa.PercentageController",
"Alexa.PowerController",
"Alexa.EndpointHealth",
)
mode_capability = get_capability(capabilities, "Alexa.ModeController")
assert mode_capability is not None
assert mode_capability["instance"] == "cover.position"
properties = mode_capability["properties"]
assert properties["nonControllable"] is False
assert {"name": "mode"} in properties["supported"]
capability_resources = mode_capability["capabilityResources"]
assert capability_resources is not None
assert {
"@type": "asset",
"value": {"assetId": "Alexa.Setting.Mode"},
} in capability_resources["friendlyNames"]
configuration = mode_capability["configuration"]
assert configuration is not None
assert configuration["ordered"] is False
supported_modes = configuration["supportedModes"]
assert supported_modes is not None
assert {
"value": "position.open",
"modeResources": {
"friendlyNames": [
{"@type": "text", "value": {"text": "open", "locale": "en-US"}},
{"@type": "text", "value": {"text": "opened", "locale": "en-US"}},
{"@type": "text", "value": {"text": "raise", "locale": "en-US"}},
{"@type": "text", "value": {"text": "raised", "locale": "en-US"}},
]
},
} in supported_modes
assert {
"value": "position.closed",
"modeResources": {
"friendlyNames": [
{"@type": "text", "value": {"text": "close", "locale": "en-US"}},
{"@type": "text", "value": {"text": "closed", "locale": "en-US"}},
{"@type": "text", "value": {"text": "shut", "locale": "en-US"}},
{"@type": "text", "value": {"text": "lower", "locale": "en-US"}},
{"@type": "text", "value": {"text": "lowered", "locale": "en-US"}},
]
},
} in supported_modes
call, msg = await assert_request_calls_service(
"Alexa.ModeController",
"SetMode",
"cover#test",
"cover.close_cover",
hass,
payload={"mode": "position.closed"},
instance="cover.position",
)
properties = msg["context"]["properties"][0]
assert properties["name"] == "mode"
assert properties["namespace"] == "Alexa.ModeController"
assert properties["value"] == "position.closed"
call, msg = await assert_request_calls_service(
"Alexa.ModeController",
"SetMode",
"cover#test",
"cover.open_cover",
hass,
payload={"mode": "position.open"},
instance="cover.position",
)
properties = msg["context"]["properties"][0]
assert properties["name"] == "mode"
assert properties["namespace"] == "Alexa.ModeController"
assert properties["value"] == "position.open"