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_CUSTOM_BYPASS,
|
||||||
STATE_ALARM_ARMED_HOME,
|
STATE_ALARM_ARMED_HOME,
|
||||||
STATE_ALARM_ARMED_NIGHT,
|
STATE_ALARM_ARMED_NIGHT,
|
||||||
|
STATE_CLOSED,
|
||||||
STATE_LOCKED,
|
STATE_LOCKED,
|
||||||
STATE_OFF,
|
STATE_OFF,
|
||||||
STATE_ON,
|
STATE_ON,
|
||||||
|
STATE_OPEN,
|
||||||
STATE_PAUSED,
|
STATE_PAUSED,
|
||||||
STATE_PLAYING,
|
STATE_PLAYING,
|
||||||
STATE_UNAVAILABLE,
|
STATE_UNAVAILABLE,
|
||||||
|
@ -888,6 +890,9 @@ class AlexaModeController(AlexaCapability):
|
||||||
if self.instance == f"{fan.DOMAIN}.{fan.ATTR_DIRECTION}":
|
if self.instance == f"{fan.DOMAIN}.{fan.ATTR_DIRECTION}":
|
||||||
return self.entity.attributes.get(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
|
return None
|
||||||
|
|
||||||
def configuration(self):
|
def configuration(self):
|
||||||
|
@ -903,6 +908,12 @@ class AlexaModeController(AlexaCapability):
|
||||||
{"type": Catalog.LABEL_ASSET, "value": Catalog.SETTING_DIRECTION}
|
{"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
|
return capability_resources
|
||||||
|
|
||||||
def mode_resources(self):
|
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
|
return mode_resources
|
||||||
|
|
||||||
def serialize_mode_resources(self):
|
def serialize_mode_resources(self):
|
||||||
|
|
|
@ -311,7 +311,10 @@ class CoverCapabilities(AlexaEntity):
|
||||||
|
|
||||||
def default_display_categories(self):
|
def default_display_categories(self):
|
||||||
"""Return the display categories for this entity."""
|
"""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):
|
def interfaces(self):
|
||||||
"""Yield the supported interfaces."""
|
"""Yield the supported interfaces."""
|
||||||
|
@ -319,6 +322,10 @@ class CoverCapabilities(AlexaEntity):
|
||||||
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||||
if supported & cover.SUPPORT_SET_POSITION:
|
if supported & cover.SUPPORT_SET_POSITION:
|
||||||
yield AlexaPercentageController(self.entity)
|
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)
|
yield AlexaEndpointHealth(self.hass, self.entity)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,6 @@ from homeassistant.const import (
|
||||||
ATTR_ENTITY_ID,
|
ATTR_ENTITY_ID,
|
||||||
ATTR_SUPPORTED_FEATURES,
|
ATTR_SUPPORTED_FEATURES,
|
||||||
ATTR_TEMPERATURE,
|
ATTR_TEMPERATURE,
|
||||||
STATE_ALARM_DISARMED,
|
|
||||||
SERVICE_ALARM_ARM_AWAY,
|
SERVICE_ALARM_ARM_AWAY,
|
||||||
SERVICE_ALARM_ARM_HOME,
|
SERVICE_ALARM_ARM_HOME,
|
||||||
SERVICE_ALARM_ARM_NIGHT,
|
SERVICE_ALARM_ARM_NIGHT,
|
||||||
|
@ -28,6 +27,9 @@ from homeassistant.const import (
|
||||||
SERVICE_VOLUME_MUTE,
|
SERVICE_VOLUME_MUTE,
|
||||||
SERVICE_VOLUME_SET,
|
SERVICE_VOLUME_SET,
|
||||||
SERVICE_VOLUME_UP,
|
SERVICE_VOLUME_UP,
|
||||||
|
STATE_ALARM_DISARMED,
|
||||||
|
STATE_CLOSED,
|
||||||
|
STATE_OPEN,
|
||||||
TEMP_CELSIUS,
|
TEMP_CELSIUS,
|
||||||
TEMP_FAHRENHEIT,
|
TEMP_FAHRENHEIT,
|
||||||
)
|
)
|
||||||
|
@ -956,23 +958,42 @@ async def async_api_set_mode(hass, config, directive, context):
|
||||||
domain = entity.domain
|
domain = entity.domain
|
||||||
service = None
|
service = None
|
||||||
data = {ATTR_ENTITY_ID: entity.entity_id}
|
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"
|
msg = "Entity does not support directive"
|
||||||
raise AlexaInvalidDirectiveError(msg)
|
raise AlexaInvalidDirectiveError(msg)
|
||||||
|
|
||||||
if instance == f"{fan.DOMAIN}.{fan.ATTR_DIRECTION}":
|
if instance == f"{fan.DOMAIN}.{fan.ATTR_DIRECTION}":
|
||||||
mode, direction = mode.split(".")
|
_, direction = capability_mode.split(".")
|
||||||
if direction in [fan.DIRECTION_REVERSE, fan.DIRECTION_FORWARD]:
|
if direction in (fan.DIRECTION_REVERSE, fan.DIRECTION_FORWARD):
|
||||||
service = fan.SERVICE_SET_DIRECTION
|
service = fan.SERVICE_SET_DIRECTION
|
||||||
data[fan.ATTR_DIRECTION] = 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(
|
await hass.services.async_call(
|
||||||
domain, service, data, blocking=False, context=context
|
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"))
|
@HANDLERS.register(("Alexa.ModeController", "AdjustMode"))
|
||||||
|
|
|
@ -565,7 +565,7 @@ async def test_direction_fan(hass):
|
||||||
},
|
},
|
||||||
} in supported_modes
|
} in supported_modes
|
||||||
|
|
||||||
call, _ = await assert_request_calls_service(
|
call, msg = await assert_request_calls_service(
|
||||||
"Alexa.ModeController",
|
"Alexa.ModeController",
|
||||||
"SetMode",
|
"SetMode",
|
||||||
"fan#test_4",
|
"fan#test_4",
|
||||||
|
@ -575,6 +575,25 @@ async def test_direction_fan(hass):
|
||||||
instance="fan.direction",
|
instance="fan.direction",
|
||||||
)
|
)
|
||||||
assert call.data["direction"] == "reverse"
|
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
|
# Test for AdjustMode instance=None Error coverage
|
||||||
with pytest.raises(AssertionError):
|
with pytest.raises(AssertionError):
|
||||||
|
@ -1190,11 +1209,12 @@ async def test_cover(hass):
|
||||||
appliance = await discovery_test(device, hass)
|
appliance = await discovery_test(device, hass)
|
||||||
|
|
||||||
assert appliance["endpointId"] == "cover#test"
|
assert appliance["endpointId"] == "cover#test"
|
||||||
assert appliance["displayCategories"][0] == "DOOR"
|
assert appliance["displayCategories"][0] == "OTHER"
|
||||||
assert appliance["friendlyName"] == "Test cover"
|
assert appliance["friendlyName"] == "Test cover"
|
||||||
|
|
||||||
assert_endpoint_capabilities(
|
assert_endpoint_capabilities(
|
||||||
appliance,
|
appliance,
|
||||||
|
"Alexa.ModeController",
|
||||||
"Alexa.PercentageController",
|
"Alexa.PercentageController",
|
||||||
"Alexa.PowerController",
|
"Alexa.PowerController",
|
||||||
"Alexa.EndpointHealth",
|
"Alexa.EndpointHealth",
|
||||||
|
@ -2076,3 +2096,98 @@ async def test_mode_unsupported_domain(hass):
|
||||||
assert msg["header"]["name"] == "ErrorResponse"
|
assert msg["header"]["name"] == "ErrorResponse"
|
||||||
assert msg["header"]["namespace"] == "Alexa"
|
assert msg["header"]["namespace"] == "Alexa"
|
||||||
assert msg["payload"]["type"] == "INVALID_DIRECTIVE"
|
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