1
mirror of https://github.com/home-assistant/core synced 2024-07-15 09:42:11 +02:00

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

@ -19,6 +19,7 @@ from homeassistant.components import (
number,
timer,
vacuum,
valve,
water_heater,
)
from homeassistant.components.alarm_control_panel import (
@ -1444,6 +1445,19 @@ class AlexaModeController(AlexaCapability):
):
return f"{cover.ATTR_POSITION}.{mode}"
# Valve position state
if self.instance == f"{valve.DOMAIN}.state":
# Return state instead of position when using ModeController.
state = self.entity.state
if state in (
valve.STATE_OPEN,
valve.STATE_OPENING,
valve.STATE_CLOSED,
valve.STATE_CLOSING,
STATE_UNKNOWN,
):
return f"state.{state}"
return None
def configuration(self) -> dict[str, Any] | None:
@ -1540,6 +1554,32 @@ class AlexaModeController(AlexaCapability):
)
return self._resource.serialize_capability_resources()
# Valve position resources
if self.instance == f"{valve.DOMAIN}.state":
supported_features = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
self._resource = AlexaModeResource(
["Preset", AlexaGlobalCatalog.SETTING_PRESET], False
)
modes = 0
if supported_features & valve.ValveEntityFeature.OPEN:
self._resource.add_mode(
f"state.{valve.STATE_OPEN}",
["Open", AlexaGlobalCatalog.SETTING_PRESET],
)
modes += 1
if supported_features & valve.ValveEntityFeature.CLOSE:
self._resource.add_mode(
f"state.{valve.STATE_CLOSED}",
["Closed", AlexaGlobalCatalog.SETTING_PRESET],
)
modes += 1
# Alexa requiers at least 2 modes
if modes == 1:
self._resource.add_mode(f"state.{PRESET_MODE_NA}", [PRESET_MODE_NA])
return self._resource.serialize_capability_resources()
return {}
def semantics(self) -> dict[str, Any] | None:
@ -1578,6 +1618,34 @@ class AlexaModeController(AlexaCapability):
return self._semantics.serialize_semantics()
# Valve Position
if self.instance == f"{valve.DOMAIN}.state":
close_labels = [AlexaSemantics.ACTION_CLOSE]
open_labels = [AlexaSemantics.ACTION_OPEN]
self._semantics = AlexaSemantics()
self._semantics.add_states_to_value(
[AlexaSemantics.STATES_CLOSED],
f"state.{valve.STATE_CLOSED}",
)
self._semantics.add_states_to_value(
[AlexaSemantics.STATES_OPEN],
f"state.{valve.STATE_OPEN}",
)
self._semantics.add_action_to_directive(
close_labels,
"SetMode",
{"mode": f"state.{valve.STATE_CLOSED}"},
)
self._semantics.add_action_to_directive(
open_labels,
"SetMode",
{"mode": f"state.{valve.STATE_OPEN}"},
)
return self._semantics.serialize_semantics()
return None
@ -1691,6 +1759,10 @@ class AlexaRangeController(AlexaCapability):
)
return speed_index
# Valve Position
if self.instance == f"{valve.DOMAIN}.{valve.ATTR_POSITION}":
return self.entity.attributes.get(valve.ATTR_CURRENT_POSITION)
return None
def configuration(self) -> dict[str, Any] | None:
@ -1814,6 +1886,17 @@ class AlexaRangeController(AlexaCapability):
return self._resource.serialize_capability_resources()
# Valve Position Resources
if self.instance == f"{valve.DOMAIN}.{valve.ATTR_POSITION}":
self._resource = AlexaPresetResource(
["Opening", AlexaGlobalCatalog.SETTING_OPENING],
min_value=0,
max_value=100,
precision=1,
unit=AlexaGlobalCatalog.UNIT_PERCENT,
)
return self._resource.serialize_capability_resources()
return {}
def semantics(self) -> dict[str, Any] | None:
@ -1890,6 +1973,25 @@ class AlexaRangeController(AlexaCapability):
)
return self._semantics.serialize_semantics()
# Valve Position
if self.instance == f"{valve.DOMAIN}.{valve.ATTR_POSITION}":
close_labels = [AlexaSemantics.ACTION_CLOSE]
open_labels = [AlexaSemantics.ACTION_OPEN]
self._semantics = AlexaSemantics()
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(
close_labels, "SetRangeValue", {"rangeValue": 0}
)
self._semantics.add_action_to_directive(
open_labels, "SetRangeValue", {"rangeValue": 100}
)
return self._semantics.serialize_semantics()
return None
@ -1963,6 +2065,10 @@ class AlexaToggleController(AlexaCapability):
is_on = bool(self.entity.attributes.get(fan.ATTR_OSCILLATING))
return "ON" if is_on else "OFF"
# Stop Valve
if self.instance == f"{valve.DOMAIN}.stop":
return "OFF"
return None
def capability_resources(self) -> dict[str, list[dict[str, Any]]]:
@ -1975,6 +2081,10 @@ class AlexaToggleController(AlexaCapability):
)
return self._resource.serialize_capability_resources()
if self.instance == f"{valve.DOMAIN}.stop":
self._resource = AlexaCapabilityResource(["Stop"])
return self._resource.serialize_capability_resources()
return {}

View File

@ -32,6 +32,7 @@ from homeassistant.components import (
switch,
timer,
vacuum,
valve,
water_heater,
)
from homeassistant.const import (
@ -976,6 +977,31 @@ class VacuumCapabilities(AlexaEntity):
yield Alexa(self.entity)
@ENTITY_ADAPTERS.register(valve.DOMAIN)
class ValveCapabilities(AlexaEntity):
"""Class to represent Valve capabilities."""
def default_display_categories(self) -> list[str]:
"""Return the display categories for this entity."""
return [DisplayCategory.OTHER]
def interfaces(self) -> Generator[AlexaCapability, None, None]:
"""Yield the supported interfaces."""
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
if supported & valve.ValveEntityFeature.SET_POSITION:
yield AlexaRangeController(
self.entity, instance=f"{valve.DOMAIN}.{valve.ATTR_POSITION}"
)
elif supported & (
valve.ValveEntityFeature.CLOSE | valve.ValveEntityFeature.OPEN
):
yield AlexaModeController(self.entity, instance=f"{valve.DOMAIN}.state")
if supported & valve.ValveEntityFeature.STOP:
yield AlexaToggleController(self.entity, instance=f"{valve.DOMAIN}.stop")
yield AlexaEndpointHealth(self.hass, self.entity)
yield Alexa(self.entity)
@ENTITY_ADAPTERS.register(camera.DOMAIN)
class CameraCapabilities(AlexaEntity):
"""Class to represent Camera capabilities."""

View File

@ -22,6 +22,7 @@ from homeassistant.components import (
number,
timer,
vacuum,
valve,
water_heater,
)
from homeassistant.const import (
@ -1216,6 +1217,15 @@ async def async_api_set_mode(
elif position == "custom":
service = cover.SERVICE_STOP_COVER
# Valve position state
elif instance == f"{valve.DOMAIN}.state":
position = mode.split(".")[1]
if position == valve.STATE_CLOSED:
service = valve.SERVICE_CLOSE_VALVE
elif position == valve.STATE_OPEN:
service = valve.SERVICE_OPEN_VALVE
if not service:
raise AlexaInvalidDirectiveError(DIRECTIVE_NOT_SUPPORTED)
@ -1266,15 +1276,22 @@ async def async_api_toggle_on(
instance = directive.instance
domain = entity.domain
# Fan Oscillating
if instance != f"{fan.DOMAIN}.{fan.ATTR_OSCILLATING}":
raise AlexaInvalidDirectiveError(DIRECTIVE_NOT_SUPPORTED)
data: dict[str, Any]
service = fan.SERVICE_OSCILLATE
data: dict[str, Any] = {
ATTR_ENTITY_ID: entity.entity_id,
fan.ATTR_OSCILLATING: True,
}
# Fan Oscillating
if instance == f"{fan.DOMAIN}.{fan.ATTR_OSCILLATING}":
service = fan.SERVICE_OSCILLATE
data = {
ATTR_ENTITY_ID: entity.entity_id,
fan.ATTR_OSCILLATING: True,
}
elif instance == f"{valve.DOMAIN}.stop":
service = valve.SERVICE_STOP_VALVE
data = {
ATTR_ENTITY_ID: entity.entity_id,
}
else:
raise AlexaInvalidDirectiveError(DIRECTIVE_NOT_SUPPORTED)
await hass.services.async_call(
domain, service, data, blocking=False, context=context
@ -1417,6 +1434,17 @@ async def async_api_set_range(
data[vacuum.ATTR_FAN_SPEED] = speed
# Valve Position
elif instance == f"{valve.DOMAIN}.{valve.ATTR_POSITION}":
range_value = int(range_value)
if supported & valve.ValveEntityFeature.CLOSE and range_value == 0:
service = valve.SERVICE_CLOSE_VALVE
elif supported & valve.ValveEntityFeature.OPEN and range_value == 100:
service = valve.SERVICE_OPEN_VALVE
else:
service = valve.SERVICE_SET_VALVE_POSITION
data[valve.ATTR_POSITION] = range_value
else:
raise AlexaInvalidDirectiveError(DIRECTIVE_NOT_SUPPORTED)
@ -1562,6 +1590,21 @@ async def async_api_adjust_range(
)
data[vacuum.ATTR_FAN_SPEED] = response_value = speed
# Valve Position
elif instance == f"{valve.DOMAIN}.{valve.ATTR_POSITION}":
range_delta = int(range_delta * 20) if range_delta_default else int(range_delta)
service = valve.SERVICE_SET_VALVE_POSITION
if not (current := entity.attributes.get(valve.ATTR_POSITION)):
msg = f"Unable to determine {entity.entity_id} current position"
raise AlexaInvalidValueError(msg)
position = response_value = min(100, max(0, range_delta + current))
if position == 100:
service = valve.SERVICE_OPEN_VALVE
elif position == 0:
service = valve.SERVICE_CLOSE_VALVE
else:
data[valve.ATTR_POSITION] = position
else:
raise AlexaInvalidDirectiveError(DIRECTIVE_NOT_SUPPORTED)

View File

@ -8,6 +8,7 @@ from homeassistant.components.alexa import smart_home
from homeassistant.components.climate import ATTR_CURRENT_TEMPERATURE, HVACMode
from homeassistant.components.lock import STATE_JAMMED, STATE_LOCKING, STATE_UNLOCKING
from homeassistant.components.media_player import MediaPlayerEntityFeature
from homeassistant.components.valve import ValveEntityFeature
from homeassistant.components.water_heater import (
ATTR_OPERATION_LIST,
ATTR_OPERATION_MODE,
@ -653,6 +654,143 @@ async def test_report_cover_range_value(hass: HomeAssistant) -> None:
properties.assert_equal("Alexa.RangeController", "rangeValue", 0)
async def test_report_valve_range_value(hass: HomeAssistant) -> None:
"""Test RangeController reports valve position correctly."""
all_valve_features = (
ValveEntityFeature.OPEN
| ValveEntityFeature.CLOSE
| ValveEntityFeature.STOP
| ValveEntityFeature.SET_POSITION
)
hass.states.async_set(
"valve.fully_open",
"open",
{
"friendly_name": "Fully open valve",
"current_position": 100,
"supported_features": all_valve_features,
},
)
hass.states.async_set(
"valve.half_open",
"open",
{
"friendly_name": "Half open valve",
"current_position": 50,
"supported_features": all_valve_features,
},
)
hass.states.async_set(
"valve.closed",
"closed",
{
"friendly_name": "Closed valve",
"current_position": 0,
"supported_features": all_valve_features,
},
)
properties = await reported_properties(hass, "valve.fully_open")
properties.assert_equal("Alexa.RangeController", "rangeValue", 100)
properties = await reported_properties(hass, "valve.half_open")
properties.assert_equal("Alexa.RangeController", "rangeValue", 50)
properties = await reported_properties(hass, "valve.closed")
properties.assert_equal("Alexa.RangeController", "rangeValue", 0)
@pytest.mark.parametrize(
(
"supported_features",
"has_mode_controller",
"has_range_controller",
"has_toggle_controller",
),
[
(ValveEntityFeature(0), False, False, False),
(
ValveEntityFeature.OPEN
| ValveEntityFeature.CLOSE
| ValveEntityFeature.STOP,
True,
False,
True,
),
(
ValveEntityFeature.OPEN,
True,
False,
False,
),
(
ValveEntityFeature.CLOSE,
True,
False,
False,
),
(
ValveEntityFeature.STOP,
False,
False,
True,
),
(
ValveEntityFeature.SET_POSITION,
False,
True,
False,
),
(
ValveEntityFeature.STOP | ValveEntityFeature.SET_POSITION,
False,
True,
True,
),
(
ValveEntityFeature.OPEN
| ValveEntityFeature.CLOSE
| ValveEntityFeature.SET_POSITION,
False,
True,
False,
),
],
)
async def test_report_valve_controllers(
hass: HomeAssistant,
supported_features: ValveEntityFeature,
has_mode_controller: bool,
has_range_controller: bool,
has_toggle_controller: bool,
) -> None:
"""Test valve controllers are reported correctly."""
hass.states.async_set(
"valve.custom",
"opening",
{
"friendly_name": "Custom valve",
"current_position": 0,
"supported_features": supported_features,
},
)
properties = await reported_properties(hass, "valve.custom")
if has_mode_controller:
properties.assert_equal("Alexa.ModeController", "mode", "state.opening")
else:
properties.assert_not_has_property("Alexa.ModeController", "mode")
if has_range_controller:
properties.assert_equal("Alexa.RangeController", "rangeValue", 0)
else:
properties.assert_not_has_property("Alexa.RangeController", "rangeValue")
if has_toggle_controller:
properties.assert_equal("Alexa.ToggleController", "toggleState", "OFF")
else:
properties.assert_not_has_property("Alexa.ToggleController", "toggleState")
async def test_report_climate_state(hass: HomeAssistant) -> None:
"""Test ThermostatController reports state correctly."""
for auto_modes in (HVACMode.AUTO, HVACMode.HEAT_COOL):

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 = (