Add valve support to Amazon Alexa (#106053)

Add valve platform to Amazon Alexa
This commit is contained in:
Jan Bouwhuis 2023-12-22 12:08:06 +01:00 committed by GitHub
parent b4f8fe8d4d
commit f536bc1d0c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 876 additions and 10 deletions

View file

@ -9,8 +9,14 @@ import homeassistant.components.camera as camera
from homeassistant.components.cover import CoverDeviceClass, CoverEntityFeature
from homeassistant.components.media_player import MediaPlayerEntityFeature
from homeassistant.components.vacuum import VacuumEntityFeature
from homeassistant.components.valve import SERVICE_STOP_VALVE, ValveEntityFeature
from homeassistant.config import async_process_ha_core_config
from homeassistant.const import STATE_UNKNOWN, UnitOfTemperature
from homeassistant.const import (
SERVICE_CLOSE_VALVE,
SERVICE_OPEN_VALVE,
STATE_UNKNOWN,
UnitOfTemperature,
)
from homeassistant.core import Context, Event, HomeAssistant
from homeassistant.helpers import entityfilter
from homeassistant.setup import async_setup_component
@ -156,7 +162,7 @@ def assert_endpoint_capabilities(endpoint, *interfaces):
capabilities = endpoint["capabilities"]
supported = {feature["interface"] for feature in capabilities}
assert supported == set(interfaces)
assert supported == {interface for interface in interfaces if interface is not None}
return capabilities
@ -2069,6 +2075,216 @@ async def test_cover_position(
assert properties["value"] == position
@pytest.mark.parametrize(
(
"position",
"position_attr_in_service_call",
"supported_features",
"service_call",
"has_toggle_controller",
),
[
(
30,
30,
ValveEntityFeature.SET_POSITION
| ValveEntityFeature.OPEN
| ValveEntityFeature.CLOSE
| ValveEntityFeature.STOP,
"valve.set_valve_position",
True,
),
(
0,
None,
ValveEntityFeature.SET_POSITION
| ValveEntityFeature.OPEN
| ValveEntityFeature.CLOSE,
"valve.close_valve",
False,
),
(
99,
99,
ValveEntityFeature.SET_POSITION
| ValveEntityFeature.OPEN
| ValveEntityFeature.CLOSE,
"valve.set_valve_position",
False,
),
(
100,
None,
ValveEntityFeature.SET_POSITION
| ValveEntityFeature.OPEN
| ValveEntityFeature.CLOSE,
"valve.open_valve",
False,
),
(
0,
0,
ValveEntityFeature.SET_POSITION,
"valve.set_valve_position",
False,
),
(
60,
60,
ValveEntityFeature.SET_POSITION,
"valve.set_valve_position",
False,
),
(
60,
60,
ValveEntityFeature.SET_POSITION | ValveEntityFeature.STOP,
"valve.set_valve_position",
True,
),
(
100,
100,
ValveEntityFeature.SET_POSITION,
"valve.set_valve_position",
False,
),
(
0,
0,
ValveEntityFeature.SET_POSITION | ValveEntityFeature.OPEN,
"valve.set_valve_position",
False,
),
(
100,
100,
ValveEntityFeature.SET_POSITION | ValveEntityFeature.CLOSE,
"valve.set_valve_position",
False,
),
],
ids=[
"position_30_open_close_stop",
"position_0_open_close",
"position_99_open_close",
"position_100_open_close",
"position_0_no_open_close",
"position_60_no_open_close",
"position_60_stop_no_open_close",
"position_100_no_open_close",
"position_0_no_close",
"position_100_no_open",
],
)
async def test_valve_position(
hass: HomeAssistant,
position: int,
position_attr_in_service_call: int | None,
supported_features: CoverEntityFeature,
service_call: str,
has_toggle_controller: bool,
) -> None:
"""Test cover discovery and position using rangeController."""
device = (
"valve.test_range",
"open",
{
"friendly_name": "Test valve range",
"device_class": "water",
"supported_features": supported_features,
"position": position,
},
)
appliance = await discovery_test(device, hass)
assert appliance["endpointId"] == "valve#test_range"
assert appliance["displayCategories"][0] == "OTHER"
assert appliance["friendlyName"] == "Test valve range"
capabilities = assert_endpoint_capabilities(
appliance,
"Alexa.RangeController",
"Alexa.EndpointHealth",
"Alexa.ToggleController" if has_toggle_controller else None,
"Alexa",
)
range_capability = get_capability(capabilities, "Alexa.RangeController")
assert range_capability is not None
assert range_capability["instance"] == "valve.position"
properties = range_capability["properties"]
assert properties["nonControllable"] is False
assert {"name": "rangeValue"} in properties["supported"]
capability_resources = range_capability["capabilityResources"]
assert capability_resources is not None
assert {
"@type": "text",
"value": {"text": "Opening", "locale": "en-US"},
} in capability_resources["friendlyNames"]
assert {
"@type": "asset",
"value": {"assetId": "Alexa.Setting.Opening"},
} in capability_resources["friendlyNames"]
configuration = range_capability["configuration"]
assert configuration is not None
assert configuration["unitOfMeasure"] == "Alexa.Unit.Percent"
supported_range = configuration["supportedRange"]
assert supported_range["minimumValue"] == 0
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.Close"],
"directive": {"name": "SetRangeValue", "payload": {"rangeValue": 0}},
} in position_action_mappings
assert {
"@type": "ActionsToDirective",
"actions": ["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, msg = await assert_request_calls_service(
"Alexa.RangeController",
"SetRangeValue",
"valve#test_range",
service_call,
hass,
payload={"rangeValue": position},
instance="valve.position",
)
assert call.data.get("position") == position_attr_in_service_call
properties = msg["context"]["properties"][0]
assert properties["name"] == "rangeValue"
assert properties["namespace"] == "Alexa.RangeController"
assert properties["value"] == position
async def test_cover_position_range(
hass: HomeAssistant,
) -> None:
@ -2186,6 +2402,208 @@ async def test_cover_position_range(
)
async def test_valve_position_range(
hass: HomeAssistant,
) -> None:
"""Test valve discovery and position range using rangeController.
Also tests an invalid valve position being handled correctly.
"""
device = (
"valve.test_range",
"open",
{
"friendly_name": "Test valve range",
"device_class": "water",
"supported_features": 15,
"position": 30,
},
)
appliance = await discovery_test(device, hass)
assert appliance["endpointId"] == "valve#test_range"
assert appliance["displayCategories"][0] == "OTHER"
assert appliance["friendlyName"] == "Test valve range"
capabilities = assert_endpoint_capabilities(
appliance,
"Alexa.RangeController",
"Alexa.EndpointHealth",
"Alexa.ToggleController",
"Alexa",
)
range_capability = get_capability(capabilities, "Alexa.RangeController")
assert range_capability is not None
assert range_capability["instance"] == "valve.position"
properties = range_capability["properties"]
assert properties["nonControllable"] is False
assert {"name": "rangeValue"} in properties["supported"]
capability_resources = range_capability["capabilityResources"]
assert capability_resources is not None
assert {
"@type": "text",
"value": {"text": "Opening", "locale": "en-US"},
} in capability_resources["friendlyNames"]
assert {
"@type": "asset",
"value": {"assetId": "Alexa.Setting.Opening"},
} in capability_resources["friendlyNames"]
configuration = range_capability["configuration"]
assert configuration is not None
assert configuration["unitOfMeasure"] == "Alexa.Unit.Percent"
supported_range = configuration["supportedRange"]
assert supported_range["minimumValue"] == 0
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.Close"],
"directive": {"name": "SetRangeValue", "payload": {"rangeValue": 0}},
} in position_action_mappings
assert {
"@type": "ActionsToDirective",
"actions": ["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, msg = await assert_request_calls_service(
"Alexa.RangeController",
"AdjustRangeValue",
"valve#test_range",
"valve.open_valve",
hass,
payload={"rangeValueDelta": 101, "rangeValueDeltaDefault": False},
instance="valve.position",
)
properties = msg["context"]["properties"][0]
assert properties["name"] == "rangeValue"
assert properties["namespace"] == "Alexa.RangeController"
assert properties["value"] == 100
assert call.service == SERVICE_OPEN_VALVE
call, msg = await assert_request_calls_service(
"Alexa.RangeController",
"AdjustRangeValue",
"valve#test_range",
"valve.close_valve",
hass,
payload={"rangeValueDelta": -99, "rangeValueDeltaDefault": False},
instance="valve.position",
)
properties = msg["context"]["properties"][0]
assert properties["name"] == "rangeValue"
assert properties["namespace"] == "Alexa.RangeController"
assert properties["value"] == 0
assert call.service == SERVICE_CLOSE_VALVE
await assert_range_changes(
hass,
[(25, -5, False), (35, 5, False), (50, 1, True), (10, -1, True)],
"Alexa.RangeController",
"AdjustRangeValue",
"valve#test_range",
"valve.set_valve_position",
"position",
instance="valve.position",
)
@pytest.mark.parametrize(
("supported_features", "state_controller"),
[
(
ValveEntityFeature.SET_POSITION | ValveEntityFeature.STOP,
"Alexa.RangeController",
),
(
ValveEntityFeature.OPEN
| ValveEntityFeature.CLOSE
| ValveEntityFeature.STOP,
"Alexa.ModeController",
),
],
)
async def test_stop_valve(
hass: HomeAssistant, supported_features: ValveEntityFeature, state_controller: str
) -> None:
"""Test stop valve ToggleController."""
device = (
"valve.test",
"opening",
{
"friendly_name": "Test valve",
"supported_features": supported_features,
"current_position": 30,
},
)
appliance = await discovery_test(device, hass)
assert appliance["endpointId"] == "valve#test"
assert appliance["displayCategories"][0] == "OTHER"
assert appliance["friendlyName"] == "Test valve"
capabilities = assert_endpoint_capabilities(
appliance,
state_controller,
"Alexa.ToggleController",
"Alexa.EndpointHealth",
"Alexa",
)
toggle_capability = get_capability(capabilities, "Alexa.ToggleController")
assert toggle_capability is not None
assert toggle_capability["instance"] == "valve.stop"
properties = toggle_capability["properties"]
assert properties["nonControllable"] is False
assert {"name": "toggleState"} in properties["supported"]
capability_resources = toggle_capability["capabilityResources"]
assert capability_resources is not None
assert {
"@type": "text",
"value": {"text": "Stop", "locale": "en-US"},
} in capability_resources["friendlyNames"]
call, _ = await assert_request_calls_service(
"Alexa.ToggleController",
"TurnOn",
"valve#test",
"valve.stop_valve",
hass,
payload={},
instance="valve.stop",
)
assert call.data["entity_id"] == "valve.test"
assert call.service == SERVICE_STOP_VALVE
async def assert_percentage_changes(
hass, adjustments, namespace, name, endpoint, parameter, service, changed_parameter
):
@ -3667,6 +4085,137 @@ async def test_cover_position_mode(hass: HomeAssistant) -> None:
assert properties["value"] == "position.custom"
async def test_valve_position_mode(hass: HomeAssistant) -> None:
"""Test valve discovery and position using modeController."""
device = (
"valve.test_mode",
"open",
{
"friendly_name": "Test valve mode",
"device_class": "water",
"supported_features": ValveEntityFeature.OPEN
| ValveEntityFeature.CLOSE
| ValveEntityFeature.STOP,
},
)
appliance = await discovery_test(device, hass)
assert appliance["endpointId"] == "valve#test_mode"
assert appliance["displayCategories"][0] == "OTHER"
assert appliance["friendlyName"] == "Test valve mode"
capabilities = assert_endpoint_capabilities(
appliance,
"Alexa.ModeController",
"Alexa.EndpointHealth",
"Alexa.ToggleController",
"Alexa",
)
mode_capability = get_capability(capabilities, "Alexa.ModeController")
assert mode_capability is not None
assert mode_capability["instance"] == "valve.state"
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": "text",
"value": {"text": "Preset", "locale": "en-US"},
} in capability_resources["friendlyNames"]
assert {
"@type": "asset",
"value": {"assetId": "Alexa.Setting.Preset"},
} 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": "state.open",
"modeResources": {
"friendlyNames": [
{"@type": "text", "value": {"text": "Open", "locale": "en-US"}},
{"@type": "asset", "value": {"assetId": "Alexa.Setting.Preset"}},
]
},
} in supported_modes
assert {
"value": "state.closed",
"modeResources": {
"friendlyNames": [
{"@type": "text", "value": {"text": "Closed", "locale": "en-US"}},
{"@type": "asset", "value": {"assetId": "Alexa.Setting.Preset"}},
]
},
} in supported_modes
# Assert for Position Semantics
position_semantics = mode_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.Close"],
"directive": {"name": "SetMode", "payload": {"mode": "state.closed"}},
} in position_action_mappings
assert {
"@type": "ActionsToDirective",
"actions": ["Alexa.Actions.Open"],
"directive": {"name": "SetMode", "payload": {"mode": "state.open"}},
} 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": "state.closed",
} in position_state_mappings
assert {
"@type": "StatesToValue",
"states": ["Alexa.States.Open"],
"value": "state.open",
} in position_state_mappings
_, msg = await assert_request_calls_service(
"Alexa.ModeController",
"SetMode",
"valve#test_mode",
"valve.close_valve",
hass,
payload={"mode": "state.closed"},
instance="valve.state",
)
properties = msg["context"]["properties"][0]
assert properties["name"] == "mode"
assert properties["namespace"] == "Alexa.ModeController"
assert properties["value"] == "state.closed"
_, msg = await assert_request_calls_service(
"Alexa.ModeController",
"SetMode",
"valve#test_mode",
"valve.open_valve",
hass,
payload={"mode": "state.open"},
instance="valve.state",
)
properties = msg["context"]["properties"][0]
assert properties["name"] == "mode"
assert properties["namespace"] == "Alexa.ModeController"
assert properties["value"] == "state.open"
async def test_image_processing(hass: HomeAssistant) -> None:
"""Test image_processing discovery as event detection."""
device = (