Add valve platform support to google_assistant (#106139)

* Add valve platform to google_assistant

* Use constant for domains set
This commit is contained in:
Jan Bouwhuis 2023-12-23 16:46:25 +01:00 committed by GitHub
parent c126022d4f
commit e311a6835e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 355 additions and 76 deletions

View File

@ -22,6 +22,7 @@ from homeassistant.components import (
sensor, sensor,
switch, switch,
vacuum, vacuum,
valve,
water_heater, water_heater,
) )
@ -65,6 +66,7 @@ DEFAULT_EXPOSED_DOMAINS = [
"sensor", "sensor",
"switch", "switch",
"vacuum", "vacuum",
"valve",
"water_heater", "water_heater",
] ]
@ -95,6 +97,7 @@ TYPE_THERMOSTAT = f"{PREFIX_TYPES}THERMOSTAT"
TYPE_TV = f"{PREFIX_TYPES}TV" TYPE_TV = f"{PREFIX_TYPES}TV"
TYPE_WINDOW = f"{PREFIX_TYPES}WINDOW" TYPE_WINDOW = f"{PREFIX_TYPES}WINDOW"
TYPE_VACUUM = f"{PREFIX_TYPES}VACUUM" TYPE_VACUUM = f"{PREFIX_TYPES}VACUUM"
TYPE_VALVE = f"{PREFIX_TYPES}VALVE"
TYPE_WATERHEATER = f"{PREFIX_TYPES}WATERHEATER" TYPE_WATERHEATER = f"{PREFIX_TYPES}WATERHEATER"
SERVICE_REQUEST_SYNC = "request_sync" SERVICE_REQUEST_SYNC = "request_sync"
@ -150,6 +153,7 @@ DOMAIN_TO_GOOGLE_TYPES = {
sensor.DOMAIN: TYPE_SENSOR, sensor.DOMAIN: TYPE_SENSOR,
switch.DOMAIN: TYPE_SWITCH, switch.DOMAIN: TYPE_SWITCH,
vacuum.DOMAIN: TYPE_VACUUM, vacuum.DOMAIN: TYPE_VACUUM,
valve.DOMAIN: TYPE_VALVE,
water_heater.DOMAIN: TYPE_WATERHEATER, water_heater.DOMAIN: TYPE_WATERHEATER,
} }

View File

@ -29,6 +29,7 @@ from homeassistant.components import (
sensor, sensor,
switch, switch,
vacuum, vacuum,
valve,
water_heater, water_heater,
) )
from homeassistant.components.alarm_control_panel import AlarmControlPanelEntityFeature from homeassistant.components.alarm_control_panel import AlarmControlPanelEntityFeature
@ -41,6 +42,7 @@ from homeassistant.components.light import LightEntityFeature
from homeassistant.components.lock import STATE_JAMMED, STATE_UNLOCKING from homeassistant.components.lock import STATE_JAMMED, STATE_UNLOCKING
from homeassistant.components.media_player import MediaPlayerEntityFeature, MediaType from homeassistant.components.media_player import MediaPlayerEntityFeature, MediaType
from homeassistant.components.vacuum import VacuumEntityFeature from homeassistant.components.vacuum import VacuumEntityFeature
from homeassistant.components.valve import ValveEntityFeature
from homeassistant.components.water_heater import WaterHeaterEntityFeature from homeassistant.components.water_heater import WaterHeaterEntityFeature
from homeassistant.const import ( from homeassistant.const import (
ATTR_ASSUMED_STATE, ATTR_ASSUMED_STATE,
@ -180,6 +182,57 @@ TRAITS: list[type[_Trait]] = []
FAN_SPEED_MAX_SPEED_COUNT = 5 FAN_SPEED_MAX_SPEED_COUNT = 5
COVER_VALVE_STATES = {
cover.DOMAIN: {
"closed": cover.STATE_CLOSED,
"closing": cover.STATE_CLOSING,
"open": cover.STATE_OPEN,
"opening": cover.STATE_OPENING,
},
valve.DOMAIN: {
"closed": valve.STATE_CLOSED,
"closing": valve.STATE_CLOSING,
"open": valve.STATE_OPEN,
"opening": valve.STATE_OPENING,
},
}
SERVICE_STOP_COVER_VALVE = {
cover.DOMAIN: cover.SERVICE_STOP_COVER,
valve.DOMAIN: valve.SERVICE_STOP_VALVE,
}
SERVICE_OPEN_COVER_VALVE = {
cover.DOMAIN: cover.SERVICE_OPEN_COVER,
valve.DOMAIN: valve.SERVICE_OPEN_VALVE,
}
SERVICE_CLOSE_COVER_VALVE = {
cover.DOMAIN: cover.SERVICE_CLOSE_COVER,
valve.DOMAIN: valve.SERVICE_CLOSE_VALVE,
}
SERVICE_SET_POSITION_COVER_VALVE = {
cover.DOMAIN: cover.SERVICE_SET_COVER_POSITION,
valve.DOMAIN: valve.SERVICE_SET_VALVE_POSITION,
}
COVER_VALVE_CURRENT_POSITION = {
cover.DOMAIN: cover.ATTR_CURRENT_POSITION,
valve.DOMAIN: valve.ATTR_CURRENT_POSITION,
}
COVER_VALVE_POSITION = {
cover.DOMAIN: cover.ATTR_POSITION,
valve.DOMAIN: valve.ATTR_POSITION,
}
COVER_VALVE_SET_POSITION_FEATURE = {
cover.DOMAIN: CoverEntityFeature.SET_POSITION,
valve.DOMAIN: ValveEntityFeature.SET_POSITION,
}
COVER_VALVE_DOMAINS = {cover.DOMAIN, valve.DOMAIN}
FRIENDLY_DOMAIN = {cover.DOMAIN: "Cover", valve.DOMAIN: "Valve"}
_TraitT = TypeVar("_TraitT", bound="_Trait") _TraitT = TypeVar("_TraitT", bound="_Trait")
@ -796,6 +849,9 @@ class StartStopTrait(_Trait):
if domain == cover.DOMAIN and features & CoverEntityFeature.STOP: if domain == cover.DOMAIN and features & CoverEntityFeature.STOP:
return True return True
if domain == valve.DOMAIN and features & ValveEntityFeature.STOP:
return True
return False return False
def sync_attributes(self): def sync_attributes(self):
@ -807,7 +863,7 @@ class StartStopTrait(_Trait):
& VacuumEntityFeature.PAUSE & VacuumEntityFeature.PAUSE
!= 0 != 0
} }
if domain == cover.DOMAIN: if domain in COVER_VALVE_DOMAINS:
return {} return {}
def query_attributes(self): def query_attributes(self):
@ -823,14 +879,16 @@ class StartStopTrait(_Trait):
if domain == cover.DOMAIN: if domain == cover.DOMAIN:
return {"isRunning": state in (cover.STATE_CLOSING, cover.STATE_OPENING)} return {"isRunning": state in (cover.STATE_CLOSING, cover.STATE_OPENING)}
if domain == valve.DOMAIN:
return {"isRunning": True}
async def execute(self, command, data, params, challenge): async def execute(self, command, data, params, challenge):
"""Execute a StartStop command.""" """Execute a StartStop command."""
domain = self.state.domain domain = self.state.domain
if domain == vacuum.DOMAIN: if domain == vacuum.DOMAIN:
return await self._execute_vacuum(command, data, params, challenge) return await self._execute_vacuum(command, data, params, challenge)
if domain == cover.DOMAIN: if domain in COVER_VALVE_DOMAINS:
return await self._execute_cover(command, data, params, challenge) return await self._execute_cover_or_valve(command, data, params, challenge)
async def _execute_vacuum(self, command, data, params, challenge): async def _execute_vacuum(self, command, data, params, challenge):
"""Execute a StartStop command.""" """Execute a StartStop command."""
@ -869,28 +927,35 @@ class StartStopTrait(_Trait):
context=data.context, context=data.context,
) )
async def _execute_cover(self, command, data, params, challenge): async def _execute_cover_or_valve(self, command, data, params, challenge):
"""Execute a StartStop command.""" """Execute a StartStop command."""
domain = self.state.domain
if command == COMMAND_STARTSTOP: if command == COMMAND_STARTSTOP:
if params["start"] is False: if params["start"] is False:
if self.state.state in ( if (
cover.STATE_CLOSING, self.state.state
cover.STATE_OPENING, in (
) or self.state.attributes.get(ATTR_ASSUMED_STATE): COVER_VALVE_STATES[domain]["closing"],
COVER_VALVE_STATES[domain]["opening"],
)
or domain == valve.DOMAIN
or self.state.attributes.get(ATTR_ASSUMED_STATE)
):
await self.hass.services.async_call( await self.hass.services.async_call(
self.state.domain, domain,
cover.SERVICE_STOP_COVER, SERVICE_STOP_COVER_VALVE[domain],
{ATTR_ENTITY_ID: self.state.entity_id}, {ATTR_ENTITY_ID: self.state.entity_id},
blocking=not self.config.should_report_state, blocking=not self.config.should_report_state,
context=data.context, context=data.context,
) )
else: else:
raise SmartHomeError( raise SmartHomeError(
ERR_ALREADY_STOPPED, "Cover is already stopped" ERR_ALREADY_STOPPED,
f"{FRIENDLY_DOMAIN[domain]} is already stopped",
) )
else: else:
raise SmartHomeError( raise SmartHomeError(
ERR_NOT_SUPPORTED, "Starting a cover is not supported" ERR_NOT_SUPPORTED, f"Starting a {domain} is not supported"
) )
else: else:
raise SmartHomeError( raise SmartHomeError(
@ -2081,7 +2146,7 @@ class OpenCloseTrait(_Trait):
@staticmethod @staticmethod
def supported(domain, features, device_class, _): def supported(domain, features, device_class, _):
"""Test if state is supported.""" """Test if state is supported."""
if domain == cover.DOMAIN: if domain in COVER_VALVE_DOMAINS:
return True return True
return domain == binary_sensor.DOMAIN and device_class in ( return domain == binary_sensor.DOMAIN and device_class in (
@ -2116,6 +2181,17 @@ class OpenCloseTrait(_Trait):
and features & CoverEntityFeature.CLOSE == 0 and features & CoverEntityFeature.CLOSE == 0
): ):
response["queryOnlyOpenClose"] = True response["queryOnlyOpenClose"] = True
elif (
self.state.domain == valve.DOMAIN
and features & ValveEntityFeature.SET_POSITION == 0
):
response["discreteOnlyOpenClose"] = True
if (
features & ValveEntityFeature.OPEN == 0
and features & ValveEntityFeature.CLOSE == 0
):
response["queryOnlyOpenClose"] = True
if self.state.attributes.get(ATTR_ASSUMED_STATE): if self.state.attributes.get(ATTR_ASSUMED_STATE):
response["commandOnlyOpenClose"] = True response["commandOnlyOpenClose"] = True
@ -2134,17 +2210,17 @@ class OpenCloseTrait(_Trait):
if self.state.attributes.get(ATTR_ASSUMED_STATE): if self.state.attributes.get(ATTR_ASSUMED_STATE):
return response return response
if domain == cover.DOMAIN: if domain in COVER_VALVE_DOMAINS:
if self.state.state == STATE_UNKNOWN: if self.state.state == STATE_UNKNOWN:
raise SmartHomeError( raise SmartHomeError(
ERR_NOT_SUPPORTED, "Querying state is not supported" ERR_NOT_SUPPORTED, "Querying state is not supported"
) )
position = self.state.attributes.get(cover.ATTR_CURRENT_POSITION) position = self.state.attributes.get(COVER_VALVE_CURRENT_POSITION[domain])
if position is not None: if position is not None:
response["openPercent"] = position response["openPercent"] = position
elif self.state.state != cover.STATE_CLOSED: elif self.state.state != COVER_VALVE_STATES[domain]["closed"]:
response["openPercent"] = 100 response["openPercent"] = 100
else: else:
response["openPercent"] = 0 response["openPercent"] = 0
@ -2162,11 +2238,13 @@ class OpenCloseTrait(_Trait):
domain = self.state.domain domain = self.state.domain
features = self.state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) features = self.state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
if domain == cover.DOMAIN: if domain in COVER_VALVE_DOMAINS:
svc_params = {ATTR_ENTITY_ID: self.state.entity_id} svc_params = {ATTR_ENTITY_ID: self.state.entity_id}
should_verify = False should_verify = False
if command == COMMAND_OPENCLOSE_RELATIVE: if command == COMMAND_OPENCLOSE_RELATIVE:
position = self.state.attributes.get(cover.ATTR_CURRENT_POSITION) position = self.state.attributes.get(
COVER_VALVE_CURRENT_POSITION[domain]
)
if position is None: if position is None:
raise SmartHomeError( raise SmartHomeError(
ERR_NOT_SUPPORTED, ERR_NOT_SUPPORTED,
@ -2177,16 +2255,16 @@ class OpenCloseTrait(_Trait):
position = params["openPercent"] position = params["openPercent"]
if position == 0: if position == 0:
service = cover.SERVICE_CLOSE_COVER service = SERVICE_CLOSE_COVER_VALVE[domain]
should_verify = False should_verify = False
elif position == 100: elif position == 100:
service = cover.SERVICE_OPEN_COVER service = SERVICE_OPEN_COVER_VALVE[domain]
should_verify = True should_verify = True
elif features & CoverEntityFeature.SET_POSITION: elif features & COVER_VALVE_SET_POSITION_FEATURE[domain]:
service = cover.SERVICE_SET_COVER_POSITION service = SERVICE_SET_POSITION_COVER_VALVE[domain]
if position > 0: if position > 0:
should_verify = True should_verify = True
svc_params[cover.ATTR_POSITION] = position svc_params[COVER_VALVE_POSITION[domain]] = position
else: else:
raise SmartHomeError( raise SmartHomeError(
ERR_NOT_SUPPORTED, "No support for partial open close" ERR_NOT_SUPPORTED, "No support for partial open close"
@ -2200,7 +2278,7 @@ class OpenCloseTrait(_Trait):
_verify_pin_challenge(data, self.state, challenge) _verify_pin_challenge(data, self.state, challenge)
await self.hass.services.async_call( await self.hass.services.async_call(
cover.DOMAIN, domain,
service, service,
svc_params, svc_params,
blocking=not self.config.should_report_state, blocking=not self.config.should_report_state,

View File

@ -103,6 +103,7 @@
'sensor', 'sensor',
'switch', 'switch',
'vacuum', 'vacuum',
'valve',
'water_heater', 'water_heater',
]), ]),
'project_id': '1234', 'project_id': '1234',

View File

@ -28,6 +28,7 @@ from homeassistant.components import (
sensor, sensor,
switch, switch,
vacuum, vacuum,
valve,
water_heater, water_heater,
) )
from homeassistant.components.alarm_control_panel import AlarmControlPanelEntityFeature from homeassistant.components.alarm_control_panel import AlarmControlPanelEntityFeature
@ -46,6 +47,7 @@ from homeassistant.components.media_player import (
MediaType, MediaType,
) )
from homeassistant.components.vacuum import VacuumEntityFeature from homeassistant.components.vacuum import VacuumEntityFeature
from homeassistant.components.valve import ValveEntityFeature
from homeassistant.components.water_heater import WaterHeaterEntityFeature from homeassistant.components.water_heater import WaterHeaterEntityFeature
from homeassistant.config import async_process_ha_core_config from homeassistant.config import async_process_ha_core_config
from homeassistant.const import ( from homeassistant.const import (
@ -650,6 +652,48 @@ async def test_startstop_cover_assumed(hass: HomeAssistant) -> None:
assert stop_calls[0].data == {ATTR_ENTITY_ID: "cover.bla"} assert stop_calls[0].data == {ATTR_ENTITY_ID: "cover.bla"}
async def test_startstop_valve(hass: HomeAssistant) -> None:
"""Test startStop trait support for valve domain."""
assert helpers.get_google_type(valve.DOMAIN, None) is not None
assert trait.StartStopTrait.supported(
valve.DOMAIN, ValveEntityFeature.STOP, None, None
)
assert not trait.StartStopTrait.supported(
valve.DOMAIN, ValveEntityFeature.SET_POSITION, None, None
)
state = State(
"valve.water",
valve.STATE_CLOSED,
{ATTR_SUPPORTED_FEATURES: ValveEntityFeature.STOP},
)
trt = trait.StartStopTrait(
hass,
state,
BASIC_CONFIG,
)
assert trt.sync_attributes() == {}
for state_value in (
valve.STATE_CLOSED,
valve.STATE_CLOSING,
valve.STATE_OPENING,
valve.STATE_OPEN,
):
state.state = state_value
assert trt.query_attributes() == {"isRunning": True}
stop_calls = async_mock_service(hass, valve.DOMAIN, valve.SERVICE_STOP_VALVE)
await trt.execute(trait.COMMAND_STARTSTOP, BASIC_DATA, {"start": False}, {})
assert len(stop_calls) == 1
assert stop_calls[0].data == {ATTR_ENTITY_ID: "valve.water"}
with pytest.raises(SmartHomeError, match="Starting a valve is not supported"):
await trt.execute(trait.COMMAND_STARTSTOP, BASIC_DATA, {"start": True}, {})
@pytest.mark.parametrize("supported_color_modes", [["hs"], ["rgb"], ["xy"]]) @pytest.mark.parametrize("supported_color_modes", [["hs"], ["rgb"], ["xy"]])
async def test_color_setting_color_light( async def test_color_setting_color_light(
hass: HomeAssistant, supported_color_modes hass: HomeAssistant, supported_color_modes
@ -2823,21 +2867,59 @@ async def test_traits_unknown_domains(
caplog.clear() caplog.clear()
async def test_openclose_cover(hass: HomeAssistant) -> None: @pytest.mark.parametrize(
"""Test OpenClose trait support for cover domain.""" (
assert helpers.get_google_type(cover.DOMAIN, None) is not None "domain",
assert trait.OpenCloseTrait.supported( "set_position_service",
cover.DOMAIN, CoverEntityFeature.SET_POSITION, None, None "close_service",
) "open_service",
"set_position_feature",
"attr_position",
"attr_current_position",
),
[
(
cover.DOMAIN,
cover.SERVICE_SET_COVER_POSITION,
cover.SERVICE_CLOSE_COVER,
cover.SERVICE_OPEN_COVER,
CoverEntityFeature.SET_POSITION,
cover.ATTR_POSITION,
cover.ATTR_CURRENT_POSITION,
),
(
valve.DOMAIN,
valve.SERVICE_SET_VALVE_POSITION,
valve.SERVICE_CLOSE_VALVE,
valve.SERVICE_OPEN_VALVE,
ValveEntityFeature.SET_POSITION,
valve.ATTR_POSITION,
valve.ATTR_CURRENT_POSITION,
),
],
)
async def test_openclose_cover_valve(
hass: HomeAssistant,
domain: str,
set_position_service: str,
close_service: str,
open_service: str,
set_position_feature: int,
attr_position: str,
attr_current_position: str,
) -> None:
"""Test OpenClose trait support."""
assert helpers.get_google_type(domain, None) is not None
assert trait.OpenCloseTrait.supported(domain, set_position_service, None, None)
trt = trait.OpenCloseTrait( trt = trait.OpenCloseTrait(
hass, hass,
State( State(
"cover.bla", f"{domain}.bla",
cover.STATE_OPEN, "open",
{ {
cover.ATTR_CURRENT_POSITION: 75, attr_current_position: 75,
ATTR_SUPPORTED_FEATURES: CoverEntityFeature.SET_POSITION, ATTR_SUPPORTED_FEATURES: set_position_feature,
}, },
), ),
BASIC_CONFIG, BASIC_CONFIG,
@ -2846,34 +2928,74 @@ async def test_openclose_cover(hass: HomeAssistant) -> None:
assert trt.sync_attributes() == {} assert trt.sync_attributes() == {}
assert trt.query_attributes() == {"openPercent": 75} assert trt.query_attributes() == {"openPercent": 75}
calls_set = async_mock_service(hass, cover.DOMAIN, cover.SERVICE_SET_COVER_POSITION) calls_set = async_mock_service(hass, domain, set_position_service)
calls_open = async_mock_service(hass, cover.DOMAIN, cover.SERVICE_OPEN_COVER) calls_open = async_mock_service(hass, domain, open_service)
calls_close = async_mock_service(hass, domain, close_service)
await trt.execute(trait.COMMAND_OPENCLOSE, BASIC_DATA, {"openPercent": 50}, {}) await trt.execute(trait.COMMAND_OPENCLOSE, BASIC_DATA, {"openPercent": 50}, {})
await trt.execute( await trt.execute(
trait.COMMAND_OPENCLOSE_RELATIVE, BASIC_DATA, {"openRelativePercent": 50}, {} trait.COMMAND_OPENCLOSE_RELATIVE, BASIC_DATA, {"openRelativePercent": 50}, {}
) )
assert len(calls_set) == 1 assert len(calls_set) == 1
assert calls_set[0].data == {ATTR_ENTITY_ID: "cover.bla", cover.ATTR_POSITION: 50} assert calls_set[0].data == {
ATTR_ENTITY_ID: f"{domain}.bla",
attr_position: 50,
}
calls_set.pop(0)
assert len(calls_open) == 1 assert len(calls_open) == 1
assert calls_open[0].data == {ATTR_ENTITY_ID: "cover.bla"} assert calls_open[0].data == {ATTR_ENTITY_ID: f"{domain}.bla"}
calls_open.pop(0)
assert len(calls_close) == 0
await trt.execute(trait.COMMAND_OPENCLOSE, BASIC_DATA, {"openPercent": 0}, {})
await trt.execute(
trait.COMMAND_OPENCLOSE_RELATIVE, BASIC_DATA, {"openRelativePercent": 0}, {}
)
assert len(calls_set) == 1
assert len(calls_close) == 1
assert calls_close[0].data == {ATTR_ENTITY_ID: f"{domain}.bla"}
assert len(calls_open) == 0
async def test_openclose_cover_unknown_state(hass: HomeAssistant) -> None: @pytest.mark.parametrize(
"""Test OpenClose trait support for cover domain with unknown state.""" ("domain", "open_service", "set_position_feature", "open_feature"),
assert helpers.get_google_type(cover.DOMAIN, None) is not None [
(
cover.DOMAIN,
cover.SERVICE_OPEN_COVER,
CoverEntityFeature.SET_POSITION,
CoverEntityFeature.OPEN,
),
(
valve.DOMAIN,
valve.SERVICE_OPEN_VALVE,
ValveEntityFeature.SET_POSITION,
ValveEntityFeature.OPEN,
),
],
)
async def test_openclose_cover_valve_unknown_state(
hass: HomeAssistant,
open_service: str,
domain: str,
set_position_feature: int,
open_feature: int,
) -> None:
"""Test OpenClose trait support with unknown state."""
assert helpers.get_google_type(domain, None) is not None
assert trait.OpenCloseTrait.supported( assert trait.OpenCloseTrait.supported(
cover.DOMAIN, CoverEntityFeature.SET_POSITION, None, None cover.DOMAIN, set_position_feature, None, None
) )
# No state # No state
trt = trait.OpenCloseTrait( trt = trait.OpenCloseTrait(
hass, hass,
State( State(
"cover.bla", f"{domain}.bla",
STATE_UNKNOWN, STATE_UNKNOWN,
{ATTR_SUPPORTED_FEATURES: CoverEntityFeature.OPEN}, {ATTR_SUPPORTED_FEATURES: open_feature},
), ),
BASIC_CONFIG, BASIC_CONFIG,
) )
@ -2883,30 +3005,51 @@ async def test_openclose_cover_unknown_state(hass: HomeAssistant) -> None:
with pytest.raises(helpers.SmartHomeError): with pytest.raises(helpers.SmartHomeError):
trt.query_attributes() trt.query_attributes()
calls = async_mock_service(hass, cover.DOMAIN, cover.SERVICE_OPEN_COVER) calls = async_mock_service(hass, domain, open_service)
await trt.execute(trait.COMMAND_OPENCLOSE, BASIC_DATA, {"openPercent": 100}, {}) await trt.execute(trait.COMMAND_OPENCLOSE, BASIC_DATA, {"openPercent": 100}, {})
assert len(calls) == 1 assert len(calls) == 1
assert calls[0].data == {ATTR_ENTITY_ID: "cover.bla"} assert calls[0].data == {ATTR_ENTITY_ID: f"{domain}.bla"}
with pytest.raises(helpers.SmartHomeError): with pytest.raises(helpers.SmartHomeError):
trt.query_attributes() trt.query_attributes()
async def test_openclose_cover_assumed_state(hass: HomeAssistant) -> None: @pytest.mark.parametrize(
"""Test OpenClose trait support for cover domain.""" ("domain", "set_position_service", "set_position_feature", "state_open"),
assert helpers.get_google_type(cover.DOMAIN, None) is not None [
assert trait.OpenCloseTrait.supported( (
cover.DOMAIN, CoverEntityFeature.SET_POSITION, None, None cover.DOMAIN,
) cover.SERVICE_SET_COVER_POSITION,
CoverEntityFeature.SET_POSITION,
cover.STATE_OPEN,
),
(
valve.DOMAIN,
valve.SERVICE_SET_VALVE_POSITION,
ValveEntityFeature.SET_POSITION,
valve.STATE_OPEN,
),
],
)
async def test_openclose_cover_valve_assumed_state(
hass: HomeAssistant,
domain: str,
set_position_service: str,
set_position_feature: int,
state_open: str,
) -> None:
"""Test OpenClose trait support."""
assert helpers.get_google_type(domain, None) is not None
assert trait.OpenCloseTrait.supported(domain, set_position_feature, None, None)
trt = trait.OpenCloseTrait( trt = trait.OpenCloseTrait(
hass, hass,
State( State(
"cover.bla", f"{domain}.bla",
cover.STATE_OPEN, state_open,
{ {
ATTR_ASSUMED_STATE: True, ATTR_ASSUMED_STATE: True,
ATTR_SUPPORTED_FEATURES: CoverEntityFeature.SET_POSITION, ATTR_SUPPORTED_FEATURES: set_position_feature,
}, },
), ),
BASIC_CONFIG, BASIC_CONFIG,
@ -2916,20 +3059,37 @@ async def test_openclose_cover_assumed_state(hass: HomeAssistant) -> None:
assert trt.query_attributes() == {} assert trt.query_attributes() == {}
calls = async_mock_service(hass, cover.DOMAIN, cover.SERVICE_SET_COVER_POSITION) calls = async_mock_service(hass, domain, set_position_service)
await trt.execute(trait.COMMAND_OPENCLOSE, BASIC_DATA, {"openPercent": 40}, {}) await trt.execute(trait.COMMAND_OPENCLOSE, BASIC_DATA, {"openPercent": 40}, {})
assert len(calls) == 1 assert len(calls) == 1
assert calls[0].data == {ATTR_ENTITY_ID: "cover.bla", cover.ATTR_POSITION: 40} assert calls[0].data == {ATTR_ENTITY_ID: f"{domain}.bla", cover.ATTR_POSITION: 40}
async def test_openclose_cover_query_only(hass: HomeAssistant) -> None: @pytest.mark.parametrize(
"""Test OpenClose trait support for cover domain.""" ("domain", "state_open"),
assert helpers.get_google_type(cover.DOMAIN, None) is not None [
assert trait.OpenCloseTrait.supported(cover.DOMAIN, 0, None, None) (
cover.DOMAIN,
cover.STATE_OPEN,
),
(
valve.DOMAIN,
valve.STATE_OPEN,
),
],
)
async def test_openclose_cover_valve_query_only(
hass: HomeAssistant,
domain: str,
state_open: str,
) -> None:
"""Test OpenClose trait support."""
assert helpers.get_google_type(domain, None) is not None
assert trait.OpenCloseTrait.supported(domain, 0, None, None)
state = State( state = State(
"cover.bla", f"{domain}.bla",
cover.STATE_OPEN, state_open,
) )
trt = trait.OpenCloseTrait( trt = trait.OpenCloseTrait(
@ -2945,21 +3105,57 @@ async def test_openclose_cover_query_only(hass: HomeAssistant) -> None:
assert trt.query_attributes() == {"openPercent": 100} assert trt.query_attributes() == {"openPercent": 100}
async def test_openclose_cover_no_position(hass: HomeAssistant) -> None: @pytest.mark.parametrize(
"""Test OpenClose trait support for cover domain.""" (
assert helpers.get_google_type(cover.DOMAIN, None) is not None "domain",
"state_open",
"state_closed",
"supported_features",
"open_service",
"close_service",
),
[
(
cover.DOMAIN,
cover.STATE_OPEN,
cover.STATE_CLOSED,
CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE,
cover.SERVICE_OPEN_COVER,
cover.SERVICE_CLOSE_COVER,
),
(
valve.DOMAIN,
valve.STATE_OPEN,
valve.STATE_CLOSED,
ValveEntityFeature.OPEN | ValveEntityFeature.CLOSE,
valve.SERVICE_OPEN_VALVE,
valve.SERVICE_CLOSE_VALVE,
),
],
)
async def test_openclose_cover_valve_no_position(
hass: HomeAssistant,
domain: str,
state_open: str,
state_closed: str,
supported_features: int,
open_service: str,
close_service: str,
) -> None:
"""Test OpenClose trait support."""
assert helpers.get_google_type(domain, None) is not None
assert trait.OpenCloseTrait.supported( assert trait.OpenCloseTrait.supported(
cover.DOMAIN, domain,
CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE, supported_features,
None, None,
None, None,
) )
state = State( state = State(
"cover.bla", f"{domain}.bla",
cover.STATE_OPEN, state_open,
{ {
ATTR_SUPPORTED_FEATURES: CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE, ATTR_SUPPORTED_FEATURES: supported_features,
}, },
) )
@ -2972,20 +3168,20 @@ async def test_openclose_cover_no_position(hass: HomeAssistant) -> None:
assert trt.sync_attributes() == {"discreteOnlyOpenClose": True} assert trt.sync_attributes() == {"discreteOnlyOpenClose": True}
assert trt.query_attributes() == {"openPercent": 100} assert trt.query_attributes() == {"openPercent": 100}
state.state = cover.STATE_CLOSED state.state = state_closed
assert trt.sync_attributes() == {"discreteOnlyOpenClose": True} assert trt.sync_attributes() == {"discreteOnlyOpenClose": True}
assert trt.query_attributes() == {"openPercent": 0} assert trt.query_attributes() == {"openPercent": 0}
calls = async_mock_service(hass, cover.DOMAIN, cover.SERVICE_CLOSE_COVER) calls = async_mock_service(hass, domain, close_service)
await trt.execute(trait.COMMAND_OPENCLOSE, BASIC_DATA, {"openPercent": 0}, {}) await trt.execute(trait.COMMAND_OPENCLOSE, BASIC_DATA, {"openPercent": 0}, {})
assert len(calls) == 1 assert len(calls) == 1
assert calls[0].data == {ATTR_ENTITY_ID: "cover.bla"} assert calls[0].data == {ATTR_ENTITY_ID: f"{domain}.bla"}
calls = async_mock_service(hass, cover.DOMAIN, cover.SERVICE_OPEN_COVER) calls = async_mock_service(hass, domain, open_service)
await trt.execute(trait.COMMAND_OPENCLOSE, BASIC_DATA, {"openPercent": 100}, {}) await trt.execute(trait.COMMAND_OPENCLOSE, BASIC_DATA, {"openPercent": 100}, {})
assert len(calls) == 1 assert len(calls) == 1
assert calls[0].data == {ATTR_ENTITY_ID: "cover.bla"} assert calls[0].data == {ATTR_ENTITY_ID: f"{domain}.bla"}
with pytest.raises( with pytest.raises(
SmartHomeError, match=r"Current position not know for relative command" SmartHomeError, match=r"Current position not know for relative command"