Fix Alexa semantics for covers with tilt support. (#30911)
* Fix Alexa semantics for covers with tilt support. * Clarify wording. * Korrect grammar.pull/30924/head
parent
e33698b17d
commit
1d41cf96ca
|
@ -1091,6 +1091,15 @@ class AlexaSecurityPanelController(AlexaCapability):
|
|||
class AlexaModeController(AlexaCapability):
|
||||
"""Implements Alexa.ModeController.
|
||||
|
||||
The instance property must be unique across ModeController, RangeController, ToggleController within the same device.
|
||||
The instance property should be a concatenated string of device domain period and single word.
|
||||
e.g. fan.speed & fan.direction.
|
||||
|
||||
The instance property must not contain words from other instance property strings within the same device.
|
||||
e.g. Instance property cover.position & cover.tilt_position will cause the Alexa.Discovery directive to fail.
|
||||
|
||||
An instance property string value may be reused for different devices.
|
||||
|
||||
https://developer.amazon.com/docs/device-apis/alexa-modecontroller.html
|
||||
"""
|
||||
|
||||
|
@ -1201,20 +1210,18 @@ class AlexaModeController(AlexaCapability):
|
|||
|
||||
def semantics(self):
|
||||
"""Build and return semantics object."""
|
||||
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||
|
||||
# Cover Position
|
||||
if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
|
||||
lower_labels = [AlexaSemantics.ACTION_LOWER]
|
||||
raise_labels = [AlexaSemantics.ACTION_RAISE]
|
||||
self._semantics = AlexaSemantics()
|
||||
self._semantics.add_action_to_directive(
|
||||
[AlexaSemantics.ACTION_CLOSE, AlexaSemantics.ACTION_LOWER],
|
||||
"SetMode",
|
||||
{"mode": f"{cover.ATTR_POSITION}.{cover.STATE_CLOSED}"},
|
||||
)
|
||||
self._semantics.add_action_to_directive(
|
||||
[AlexaSemantics.ACTION_OPEN, AlexaSemantics.ACTION_RAISE],
|
||||
"SetMode",
|
||||
{"mode": f"{cover.ATTR_POSITION}.{cover.STATE_OPEN}"},
|
||||
)
|
||||
|
||||
# Add open/close semantics if tilt is not supported.
|
||||
if not supported & cover.SUPPORT_SET_TILT_POSITION:
|
||||
lower_labels.append(AlexaSemantics.ACTION_CLOSE)
|
||||
raise_labels.append(AlexaSemantics.ACTION_OPEN)
|
||||
self._semantics.add_states_to_value(
|
||||
[AlexaSemantics.STATES_CLOSED],
|
||||
f"{cover.ATTR_POSITION}.{cover.STATE_CLOSED}",
|
||||
|
@ -1223,6 +1230,18 @@ class AlexaModeController(AlexaCapability):
|
|||
[AlexaSemantics.STATES_OPEN],
|
||||
f"{cover.ATTR_POSITION}.{cover.STATE_OPEN}",
|
||||
)
|
||||
|
||||
self._semantics.add_action_to_directive(
|
||||
lower_labels,
|
||||
"SetMode",
|
||||
{"mode": f"{cover.ATTR_POSITION}.{cover.STATE_CLOSED}"},
|
||||
)
|
||||
self._semantics.add_action_to_directive(
|
||||
raise_labels,
|
||||
"SetMode",
|
||||
{"mode": f"{cover.ATTR_POSITION}.{cover.STATE_OPEN}"},
|
||||
)
|
||||
|
||||
return self._semantics.serialize_semantics()
|
||||
|
||||
return None
|
||||
|
@ -1231,6 +1250,15 @@ class AlexaModeController(AlexaCapability):
|
|||
class AlexaRangeController(AlexaCapability):
|
||||
"""Implements Alexa.RangeController.
|
||||
|
||||
The instance property must be unique across ModeController, RangeController, ToggleController within the same device.
|
||||
The instance property should be a concatenated string of device domain period and single word.
|
||||
e.g. fan.speed & fan.direction.
|
||||
|
||||
The instance property must not contain words from other instance property strings within the same device.
|
||||
e.g. Instance property cover.position & cover.tilt_position will cause the Alexa.Discovery directive to fail.
|
||||
|
||||
An instance property string value may be reused for different devices.
|
||||
|
||||
https://developer.amazon.com/docs/device-apis/alexa-rangecontroller.html
|
||||
"""
|
||||
|
||||
|
@ -1290,8 +1318,8 @@ class AlexaRangeController(AlexaCapability):
|
|||
if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
|
||||
return self.entity.attributes.get(cover.ATTR_CURRENT_POSITION)
|
||||
|
||||
# Cover Tilt Position
|
||||
if self.instance == f"{cover.DOMAIN}.{cover.ATTR_TILT_POSITION}":
|
||||
# Cover Tilt
|
||||
if self.instance == f"{cover.DOMAIN}.tilt":
|
||||
return self.entity.attributes.get(cover.ATTR_CURRENT_TILT_POSITION)
|
||||
|
||||
# Input Number Value
|
||||
|
@ -1350,10 +1378,10 @@ class AlexaRangeController(AlexaCapability):
|
|||
)
|
||||
return self._resource.serialize_capability_resources()
|
||||
|
||||
# Cover Tilt Position Resources
|
||||
if self.instance == f"{cover.DOMAIN}.{cover.ATTR_TILT_POSITION}":
|
||||
# Cover Tilt Resources
|
||||
if self.instance == f"{cover.DOMAIN}.tilt":
|
||||
self._resource = AlexaPresetResource(
|
||||
["Tilt Position", AlexaGlobalCatalog.SETTING_OPENING],
|
||||
["Tilt", "Angle", AlexaGlobalCatalog.SETTING_DIRECTION],
|
||||
min_value=0,
|
||||
max_value=100,
|
||||
precision=1,
|
||||
|
@ -1407,24 +1435,35 @@ class AlexaRangeController(AlexaCapability):
|
|||
|
||||
def semantics(self):
|
||||
"""Build and return semantics object."""
|
||||
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||
|
||||
# Cover Position
|
||||
if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
|
||||
lower_labels = [AlexaSemantics.ACTION_LOWER]
|
||||
raise_labels = [AlexaSemantics.ACTION_RAISE]
|
||||
self._semantics = AlexaSemantics()
|
||||
self._semantics.add_action_to_directive(
|
||||
[AlexaSemantics.ACTION_LOWER], "SetRangeValue", {"rangeValue": 0}
|
||||
|
||||
# Add open/close semantics if tilt is not supported.
|
||||
if not supported & cover.SUPPORT_SET_TILT_POSITION:
|
||||
lower_labels.append(AlexaSemantics.ACTION_CLOSE)
|
||||
raise_labels.append(AlexaSemantics.ACTION_OPEN)
|
||||
self._semantics.add_states_to_value(
|
||||
[AlexaSemantics.STATES_CLOSED], value=0
|
||||
)
|
||||
self._semantics.add_action_to_directive(
|
||||
[AlexaSemantics.ACTION_RAISE], "SetRangeValue", {"rangeValue": 100}
|
||||
)
|
||||
self._semantics.add_states_to_value([AlexaSemantics.STATES_CLOSED], value=0)
|
||||
self._semantics.add_states_to_range(
|
||||
[AlexaSemantics.STATES_OPEN], min_value=1, max_value=100
|
||||
)
|
||||
|
||||
self._semantics.add_action_to_directive(
|
||||
lower_labels, "SetRangeValue", {"rangeValue": 0}
|
||||
)
|
||||
self._semantics.add_action_to_directive(
|
||||
raise_labels, "SetRangeValue", {"rangeValue": 100}
|
||||
)
|
||||
return self._semantics.serialize_semantics()
|
||||
|
||||
# Cover Tilt Position
|
||||
if self.instance == f"{cover.DOMAIN}.{cover.ATTR_TILT_POSITION}":
|
||||
# Cover Tilt
|
||||
if self.instance == f"{cover.DOMAIN}.tilt":
|
||||
self._semantics = AlexaSemantics()
|
||||
self._semantics.add_action_to_directive(
|
||||
[AlexaSemantics.ACTION_CLOSE], "SetRangeValue", {"rangeValue": 0}
|
||||
|
@ -1444,6 +1483,15 @@ class AlexaRangeController(AlexaCapability):
|
|||
class AlexaToggleController(AlexaCapability):
|
||||
"""Implements Alexa.ToggleController.
|
||||
|
||||
The instance property must be unique across ModeController, RangeController, ToggleController within the same device.
|
||||
The instance property should be a concatenated string of device domain period and single word.
|
||||
e.g. fan.speed & fan.direction.
|
||||
|
||||
The instance property must not contain words from other instance property strings within the same device.
|
||||
e.g. Instance property cover.position & cover.tilt_position will cause the Alexa.Discovery directive to fail.
|
||||
|
||||
An instance property string value may be reused for different devices.
|
||||
|
||||
https://developer.amazon.com/docs/device-apis/alexa-togglecontroller.html
|
||||
"""
|
||||
|
||||
|
|
|
@ -410,9 +410,7 @@ class CoverCapabilities(AlexaEntity):
|
|||
self.entity, instance=f"{cover.DOMAIN}.{cover.ATTR_POSITION}"
|
||||
)
|
||||
if supported & cover.SUPPORT_SET_TILT_POSITION:
|
||||
yield AlexaRangeController(
|
||||
self.entity, instance=f"{cover.DOMAIN}.{cover.ATTR_TILT_POSITION}"
|
||||
)
|
||||
yield AlexaRangeController(self.entity, instance=f"{cover.DOMAIN}.tilt")
|
||||
yield AlexaEndpointHealth(self.hass, self.entity)
|
||||
yield Alexa(self.hass)
|
||||
|
||||
|
|
|
@ -1120,8 +1120,8 @@ async def async_api_set_range(hass, config, directive, context):
|
|||
service = cover.SERVICE_SET_COVER_POSITION
|
||||
data[cover.ATTR_POSITION] = range_value
|
||||
|
||||
# Cover Tilt Position
|
||||
elif instance == f"{cover.DOMAIN}.{cover.ATTR_TILT_POSITION}":
|
||||
# Cover Tilt
|
||||
elif instance == f"{cover.DOMAIN}.tilt":
|
||||
range_value = int(range_value)
|
||||
if range_value == 0:
|
||||
service = cover.SERVICE_CLOSE_COVER_TILT
|
||||
|
@ -1215,8 +1215,8 @@ async def async_api_adjust_range(hass, config, directive, context):
|
|||
100, max(0, range_delta + current)
|
||||
)
|
||||
|
||||
# Cover Tilt Position
|
||||
elif instance == f"{cover.DOMAIN}.{cover.ATTR_TILT_POSITION}":
|
||||
# Cover Tilt
|
||||
elif instance == f"{cover.DOMAIN}.tilt":
|
||||
range_delta = int(range_delta)
|
||||
service = SERVICE_SET_COVER_TILT_POSITION
|
||||
current = entity.attributes.get(cover.ATTR_TILT_POSITION)
|
||||
|
|
|
@ -190,7 +190,12 @@ class AlexaGlobalCatalog:
|
|||
|
||||
|
||||
class AlexaCapabilityResource:
|
||||
"""Base class for Alexa capabilityResources, ModeResources, and presetResources objects.
|
||||
"""Base class for Alexa capabilityResources, modeResources, and presetResources objects.
|
||||
|
||||
Resources objects labels must be unique across all modeResources and presetResources within the same device.
|
||||
To provide support for all supported locales, include one label from the AlexaGlobalCatalog in the labels array.
|
||||
You cannot use any words from the following list as friendly names:
|
||||
https://developer.amazon.com/docs/alexa/device-apis/resources-and-assets.html#names-you-cannot-use
|
||||
|
||||
https://developer.amazon.com/docs/device-apis/resources-and-assets.html#capability-resources
|
||||
"""
|
||||
|
@ -312,6 +317,14 @@ class AlexaSemantics:
|
|||
|
||||
Semantics is supported for the following interfaces only: ModeController, RangeController, and ToggleController.
|
||||
|
||||
Semantics stateMappings are only supported for one interface of the same type on the same device. If a device has
|
||||
multiple RangeControllers only one interface may use stateMappings otherwise discovery will fail.
|
||||
|
||||
You can support semantics actionMappings on different controllers for the same device, however each controller must
|
||||
support different phrases. For example, you can support "raise" on a RangeController, and "open" on a ModeController,
|
||||
but you can't support "open" on both RangeController and ModeController. Semantics stateMappings are only supported
|
||||
for one interface on the same device.
|
||||
|
||||
https://developer.amazon.com/docs/device-apis/alexa-discovery.html#semantics-object
|
||||
"""
|
||||
|
||||
|
|
|
@ -133,7 +133,7 @@ def get_capability(capabilities, capability_name, instance=None):
|
|||
for capability in capabilities:
|
||||
if instance and capability["instance"] == instance:
|
||||
return capability
|
||||
if capability["interface"] == capability_name:
|
||||
if not instance and capability["interface"] == capability_name:
|
||||
return capability
|
||||
|
||||
return None
|
||||
|
@ -1484,6 +1484,36 @@ async def test_cover_position_range(hass):
|
|||
assert supported_range["maximumValue"] == 100
|
||||
assert supported_range["precision"] == 1
|
||||
|
||||
# Assert for Position Semantics
|
||||
position_semantics = range_capability["semantics"]
|
||||
assert position_semantics is not None
|
||||
|
||||
position_action_mappings = position_semantics["actionMappings"]
|
||||
assert position_action_mappings is not None
|
||||
assert {
|
||||
"@type": "ActionsToDirective",
|
||||
"actions": ["Alexa.Actions.Lower", "Alexa.Actions.Close"],
|
||||
"directive": {"name": "SetRangeValue", "payload": {"rangeValue": 0}},
|
||||
} in position_action_mappings
|
||||
assert {
|
||||
"@type": "ActionsToDirective",
|
||||
"actions": ["Alexa.Actions.Raise", "Alexa.Actions.Open"],
|
||||
"directive": {"name": "SetRangeValue", "payload": {"rangeValue": 100}},
|
||||
} in position_action_mappings
|
||||
|
||||
position_state_mappings = position_semantics["stateMappings"]
|
||||
assert position_state_mappings is not None
|
||||
assert {
|
||||
"@type": "StatesToValue",
|
||||
"states": ["Alexa.States.Closed"],
|
||||
"value": 0,
|
||||
} in position_state_mappings
|
||||
assert {
|
||||
"@type": "StatesToRange",
|
||||
"states": ["Alexa.States.Open"],
|
||||
"range": {"minimumValue": 1, "maximumValue": 100},
|
||||
} in position_state_mappings
|
||||
|
||||
call, _ = await assert_request_calls_service(
|
||||
"Alexa.RangeController",
|
||||
"SetRangeValue",
|
||||
|
@ -2511,16 +2541,37 @@ async def test_cover_position_mode(hass):
|
|||
},
|
||||
} in supported_modes
|
||||
|
||||
semantics = mode_capability["semantics"]
|
||||
assert semantics is not None
|
||||
# Assert for Position Semantics
|
||||
position_semantics = mode_capability["semantics"]
|
||||
assert position_semantics is not None
|
||||
|
||||
action_mappings = semantics["actionMappings"]
|
||||
assert action_mappings is not None
|
||||
position_action_mappings = position_semantics["actionMappings"]
|
||||
assert position_action_mappings is not None
|
||||
assert {
|
||||
"@type": "ActionsToDirective",
|
||||
"actions": ["Alexa.Actions.Lower", "Alexa.Actions.Close"],
|
||||
"directive": {"name": "SetMode", "payload": {"mode": "position.closed"}},
|
||||
} in position_action_mappings
|
||||
assert {
|
||||
"@type": "ActionsToDirective",
|
||||
"actions": ["Alexa.Actions.Raise", "Alexa.Actions.Open"],
|
||||
"directive": {"name": "SetMode", "payload": {"mode": "position.open"}},
|
||||
} in position_action_mappings
|
||||
|
||||
state_mappings = semantics["stateMappings"]
|
||||
assert state_mappings is not None
|
||||
position_state_mappings = position_semantics["stateMappings"]
|
||||
assert position_state_mappings is not None
|
||||
assert {
|
||||
"@type": "StatesToValue",
|
||||
"states": ["Alexa.States.Closed"],
|
||||
"value": "position.closed",
|
||||
} in position_state_mappings
|
||||
assert {
|
||||
"@type": "StatesToValue",
|
||||
"states": ["Alexa.States.Open"],
|
||||
"value": "position.open",
|
||||
} in position_state_mappings
|
||||
|
||||
call, msg = await assert_request_calls_service(
|
||||
_, msg = await assert_request_calls_service(
|
||||
"Alexa.ModeController",
|
||||
"SetMode",
|
||||
"cover#test_mode",
|
||||
|
@ -2534,7 +2585,7 @@ async def test_cover_position_mode(hass):
|
|||
assert properties["namespace"] == "Alexa.ModeController"
|
||||
assert properties["value"] == "position.closed"
|
||||
|
||||
call, msg = await assert_request_calls_service(
|
||||
_, msg = await assert_request_calls_service(
|
||||
"Alexa.ModeController",
|
||||
"SetMode",
|
||||
"cover#test_mode",
|
||||
|
@ -2548,7 +2599,7 @@ async def test_cover_position_mode(hass):
|
|||
assert properties["namespace"] == "Alexa.ModeController"
|
||||
assert properties["value"] == "position.open"
|
||||
|
||||
call, msg = await assert_request_calls_service(
|
||||
_, msg = await assert_request_calls_service(
|
||||
"Alexa.ModeController",
|
||||
"SetMode",
|
||||
"cover#test_mode",
|
||||
|
@ -2668,7 +2719,7 @@ async def test_cover_tilt_position_range(hass):
|
|||
|
||||
range_capability = get_capability(capabilities, "Alexa.RangeController")
|
||||
assert range_capability is not None
|
||||
assert range_capability["instance"] == "cover.tilt_position"
|
||||
assert range_capability["instance"] == "cover.tilt"
|
||||
|
||||
semantics = range_capability["semantics"]
|
||||
assert semantics is not None
|
||||
|
@ -2686,7 +2737,7 @@ async def test_cover_tilt_position_range(hass):
|
|||
"cover.set_cover_tilt_position",
|
||||
hass,
|
||||
payload={"rangeValue": "50"},
|
||||
instance="cover.tilt_position",
|
||||
instance="cover.tilt",
|
||||
)
|
||||
assert call.data["position"] == 50
|
||||
|
||||
|
@ -2697,7 +2748,7 @@ async def test_cover_tilt_position_range(hass):
|
|||
"cover.close_cover_tilt",
|
||||
hass,
|
||||
payload={"rangeValue": "0"},
|
||||
instance="cover.tilt_position",
|
||||
instance="cover.tilt",
|
||||
)
|
||||
properties = msg["context"]["properties"][0]
|
||||
assert properties["name"] == "rangeValue"
|
||||
|
@ -2711,7 +2762,7 @@ async def test_cover_tilt_position_range(hass):
|
|||
"cover.open_cover_tilt",
|
||||
hass,
|
||||
payload={"rangeValue": "100"},
|
||||
instance="cover.tilt_position",
|
||||
instance="cover.tilt",
|
||||
)
|
||||
properties = msg["context"]["properties"][0]
|
||||
assert properties["name"] == "rangeValue"
|
||||
|
@ -2727,12 +2778,12 @@ async def test_cover_tilt_position_range(hass):
|
|||
False,
|
||||
"cover.set_cover_tilt_position",
|
||||
"tilt_position",
|
||||
instance="cover.tilt_position",
|
||||
instance="cover.tilt",
|
||||
)
|
||||
|
||||
|
||||
async def test_cover_semantics(hass):
|
||||
"""Test cover discovery and semantics."""
|
||||
async def test_cover_semantics_position_and_tilt(hass):
|
||||
"""Test cover discovery and semantics with position and tilt support."""
|
||||
device = (
|
||||
"cover.test_semantics",
|
||||
"open",
|
||||
|
@ -2754,50 +2805,57 @@ async def test_cover_semantics(hass):
|
|||
appliance, "Alexa.RangeController", "Alexa.EndpointHealth", "Alexa"
|
||||
)
|
||||
|
||||
for range_instance in ("cover.position", "cover.tilt_position"):
|
||||
range_capability = get_capability(
|
||||
capabilities, "Alexa.RangeController", range_instance
|
||||
# Assert for Position Semantics
|
||||
position_capability = get_capability(
|
||||
capabilities, "Alexa.RangeController", "cover.position"
|
||||
)
|
||||
semantics = range_capability["semantics"]
|
||||
assert semantics is not None
|
||||
position_semantics = position_capability["semantics"]
|
||||
assert position_semantics is not None
|
||||
|
||||
action_mappings = semantics["actionMappings"]
|
||||
assert action_mappings is not None
|
||||
if range_instance == "cover.position":
|
||||
position_action_mappings = position_semantics["actionMappings"]
|
||||
assert position_action_mappings is not None
|
||||
assert {
|
||||
"@type": "ActionsToDirective",
|
||||
"actions": ["Alexa.Actions.Lower"],
|
||||
"directive": {"name": "SetRangeValue", "payload": {"rangeValue": 0}},
|
||||
} in action_mappings
|
||||
} in position_action_mappings
|
||||
assert {
|
||||
"@type": "ActionsToDirective",
|
||||
"actions": ["Alexa.Actions.Raise"],
|
||||
"directive": {"name": "SetRangeValue", "payload": {"rangeValue": 100}},
|
||||
} in action_mappings
|
||||
elif range_instance == "cover.position":
|
||||
} in position_action_mappings
|
||||
|
||||
# Assert for Tilt Semantics
|
||||
tilt_capability = get_capability(
|
||||
capabilities, "Alexa.RangeController", "cover.tilt"
|
||||
)
|
||||
tilt_semantics = tilt_capability["semantics"]
|
||||
assert tilt_semantics is not None
|
||||
tilt_action_mappings = tilt_semantics["actionMappings"]
|
||||
assert tilt_action_mappings is not None
|
||||
assert {
|
||||
"@type": "ActionsToDirective",
|
||||
"actions": ["Alexa.Actions.Close"],
|
||||
"directive": {"name": "SetRangeValue", "payload": {"rangeValue": 0}},
|
||||
} in action_mappings
|
||||
} in tilt_action_mappings
|
||||
assert {
|
||||
"@type": "ActionsToDirective",
|
||||
"actions": ["Alexa.Actions.Open"],
|
||||
"directive": {"name": "SetRangeValue", "payload": {"rangeValue": 100}},
|
||||
} in action_mappings
|
||||
} in tilt_action_mappings
|
||||
|
||||
state_mappings = semantics["stateMappings"]
|
||||
assert state_mappings is not None
|
||||
tilt_state_mappings = tilt_semantics["stateMappings"]
|
||||
assert tilt_state_mappings is not None
|
||||
assert {
|
||||
"@type": "StatesToValue",
|
||||
"states": ["Alexa.States.Closed"],
|
||||
"value": 0,
|
||||
} in state_mappings
|
||||
} in tilt_state_mappings
|
||||
assert {
|
||||
"@type": "StatesToRange",
|
||||
"states": ["Alexa.States.Open"],
|
||||
"range": {"minimumValue": 1, "maximumValue": 100},
|
||||
} in state_mappings
|
||||
} in tilt_state_mappings
|
||||
|
||||
|
||||
async def test_input_number(hass):
|
||||
|
|
Loading…
Reference in New Issue