Add valve support to Amazon Alexa (#106053)
Add valve platform to Amazon Alexa
This commit is contained in:
parent
b4f8fe8d4d
commit
f536bc1d0c
5 changed files with 876 additions and 10 deletions
|
@ -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 = (
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue