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
|
@ -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 {}
|
||||
|
||||
|
||||
|
|
|
@ -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."""
|
||||
|
|
|
@ -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]
|
||||
|
||||
# Fan Oscillating
|
||||
if instance == f"{fan.DOMAIN}.{fan.ATTR_OSCILLATING}":
|
||||
service = fan.SERVICE_OSCILLATE
|
||||
data: dict[str, Any] = {
|
||||
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)
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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