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
parent
dc8c085872
commit
5ea5db32d2
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
@ -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"))
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue