Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 30 additions & 19 deletions homeassistant/components/google_assistant/trait.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,10 @@
cover.DOMAIN: cover.SERVICE_CLOSE_COVER,
valve.DOMAIN: valve.SERVICE_CLOSE_VALVE,
}
SERVICE_TOGGLE_COVER_VALVE = {
cover.DOMAIN: cover.SERVICE_TOGGLE,
valve.DOMAIN: valve.SERVICE_TOGGLE,
}
SERVICE_SET_POSITION_COVER_VALVE = {
cover.DOMAIN: cover.SERVICE_SET_COVER_POSITION,
valve.DOMAIN: valve.SERVICE_SET_VALVE_POSITION,
Expand All @@ -228,6 +232,10 @@
cover.DOMAIN: CoverEntityFeature.SET_POSITION,
valve.DOMAIN: ValveEntityFeature.SET_POSITION,
}
COVER_VALVE_STOP_FEATURE = {
cover.DOMAIN: CoverEntityFeature.STOP,
valve.DOMAIN: ValveEntityFeature.STOP,
}

COVER_VALVE_DOMAINS = {cover.DOMAIN, valve.DOMAIN}

Expand Down Expand Up @@ -846,10 +854,10 @@ def supported(domain, features, device_class, _):
if domain == vacuum.DOMAIN:
return True

if domain == cover.DOMAIN and features & CoverEntityFeature.STOP:
return True

if domain == valve.DOMAIN and features & ValveEntityFeature.STOP:
if (
domain in COVER_VALVE_DOMAINS
and features & COVER_VALVE_STOP_FEATURE[domain]
):
return True

return False
Expand Down Expand Up @@ -877,10 +885,14 @@ def query_attributes(self):
"isPaused": state == vacuum.STATE_PAUSED,
}

if domain == cover.DOMAIN:
return {"isRunning": state in (cover.STATE_CLOSING, cover.STATE_OPENING)}
if domain == valve.DOMAIN:
return {"isRunning": True}
if domain in COVER_VALVE_DOMAINS:
return {
"isRunning": state
in (
COVER_VALVE_STATES[domain]["closing"],
COVER_VALVE_STATES[domain]["opening"],
)
}

async def execute(self, command, data, params, challenge):
"""Execute a StartStop command."""
Expand Down Expand Up @@ -932,15 +944,10 @@ async def _execute_cover_or_valve(self, command, data, params, challenge):
domain = self.state.domain
if command == COMMAND_STARTSTOP:
if params["start"] is False:
if (
self.state.state
in (
COVER_VALVE_STATES[domain]["closing"],
COVER_VALVE_STATES[domain]["opening"],
)
or domain == valve.DOMAIN
or self.state.attributes.get(ATTR_ASSUMED_STATE)
):
if self.state.state in (
COVER_VALVE_STATES[domain]["closing"],
COVER_VALVE_STATES[domain]["opening"],
) or self.state.attributes.get(ATTR_ASSUMED_STATE):
await self.hass.services.async_call(
domain,
SERVICE_STOP_COVER_VALVE[domain],
Expand All @@ -954,8 +961,12 @@ async def _execute_cover_or_valve(self, command, data, params, challenge):
f"{FRIENDLY_DOMAIN[domain]} is already stopped",
)
else:
raise SmartHomeError(
ERR_NOT_SUPPORTED, f"Starting a {domain} is not supported"
await self.hass.services.async_call(
domain,
SERVICE_TOGGLE_COVER_VALVE[domain],
{ATTR_ENTITY_ID: self.state.entity_id},
blocking=not self.config.should_report_state,
context=data.context,
)
else:
raise SmartHomeError(
Expand Down
225 changes: 162 additions & 63 deletions tests/components/google_assistant/test_trait.py
Original file line number Diff line number Diff line change
Expand Up @@ -584,17 +584,71 @@ async def test_startstop_vacuum(hass: HomeAssistant) -> None:
assert unpause_calls[0].data == {ATTR_ENTITY_ID: "vacuum.bla"}


async def test_startstop_cover(hass: HomeAssistant) -> None:
"""Test startStop trait support for cover domain."""
assert helpers.get_google_type(cover.DOMAIN, None) is not None
assert trait.StartStopTrait.supported(
cover.DOMAIN, CoverEntityFeature.STOP, None, None
)
@pytest.mark.parametrize(
(
"domain",
"state_open",
"state_closed",
"state_opening",
"state_closing",
"supported_features",
"service_close",
"service_open",
"service_stop",
"service_toggle",
),
[
(
cover.DOMAIN,
cover.STATE_OPEN,
cover.STATE_CLOSED,
cover.STATE_OPENING,
cover.STATE_CLOSING,
CoverEntityFeature.STOP
| CoverEntityFeature.OPEN
| CoverEntityFeature.CLOSE,
cover.SERVICE_OPEN_COVER,
cover.SERVICE_CLOSE_COVER,
cover.SERVICE_STOP_COVER,
cover.SERVICE_TOGGLE,
),
(
valve.DOMAIN,
valve.STATE_OPEN,
valve.STATE_CLOSED,
valve.STATE_OPENING,
valve.STATE_CLOSING,
ValveEntityFeature.STOP
| ValveEntityFeature.OPEN
| ValveEntityFeature.CLOSE,
valve.SERVICE_OPEN_VALVE,
valve.SERVICE_CLOSE_VALVE,
valve.SERVICE_STOP_VALVE,
cover.SERVICE_TOGGLE,
),
],
)
async def test_startstop_cover_valve(
hass: HomeAssistant,
domain: str,
state_open: str,
state_closed: str,
state_opening: str,
state_closing: str,
supported_features: str,
service_open: str,
service_close: str,
service_stop: str,
service_toggle: str,
) -> None:
"""Test startStop trait support."""
assert helpers.get_google_type(domain, None) is not None
assert trait.StartStopTrait.supported(domain, supported_features, None, None)

state = State(
"cover.bla",
cover.STATE_CLOSED,
{ATTR_SUPPORTED_FEATURES: CoverEntityFeature.STOP},
f"{domain}.bla",
state_closed,
{ATTR_SUPPORTED_FEATURES: supported_features},
)

trt = trait.StartStopTrait(
Expand All @@ -605,93 +659,138 @@ async def test_startstop_cover(hass: HomeAssistant) -> None:

assert trt.sync_attributes() == {}

for state_value in (cover.STATE_CLOSING, cover.STATE_OPENING):
for state_value in (state_closing, state_opening):
state.state = state_value
assert trt.query_attributes() == {"isRunning": True}

stop_calls = async_mock_service(hass, cover.DOMAIN, cover.SERVICE_STOP_COVER)
stop_calls = async_mock_service(hass, domain, service_stop)
open_calls = async_mock_service(hass, domain, service_open)
close_calls = async_mock_service(hass, domain, service_close)
toggle_calls = async_mock_service(hass, domain, service_toggle)
await trt.execute(trait.COMMAND_STARTSTOP, BASIC_DATA, {"start": False}, {})
assert len(stop_calls) == 1
assert stop_calls[0].data == {ATTR_ENTITY_ID: "cover.bla"}
assert stop_calls[0].data == {ATTR_ENTITY_ID: f"{domain}.bla"}

for state_value in (cover.STATE_CLOSED, cover.STATE_OPEN):
for state_value in (state_closed, state_open):
state.state = state_value
assert trt.query_attributes() == {"isRunning": False}

with pytest.raises(SmartHomeError, match="Cover is already stopped"):
for state_value in (state_closing, state_opening):
state.state = state_value
assert trt.query_attributes() == {"isRunning": True}

state.state = state_open
with pytest.raises(
SmartHomeError, match=f"{domain.capitalize()} is already stopped"
):
await trt.execute(trait.COMMAND_STARTSTOP, BASIC_DATA, {"start": False}, {})

with pytest.raises(SmartHomeError, match="Starting a cover is not supported"):
await trt.execute(trait.COMMAND_STARTSTOP, BASIC_DATA, {"start": True}, {})
# Start triggers toggle open
state.state = state_closed
await trt.execute(trait.COMMAND_STARTSTOP, BASIC_DATA, {"start": True}, {})
assert len(open_calls) == 0
assert len(close_calls) == 0
assert len(toggle_calls) == 1
assert toggle_calls[0].data == {ATTR_ENTITY_ID: f"{domain}.bla"}
# Second start triggers toggle close
state.state = state_open
await trt.execute(trait.COMMAND_STARTSTOP, BASIC_DATA, {"start": True}, {})
assert len(open_calls) == 0
assert len(close_calls) == 0
assert len(toggle_calls) == 2
assert toggle_calls[1].data == {ATTR_ENTITY_ID: f"{domain}.bla"}

state.state = state_closed
with pytest.raises(
SmartHomeError,
match="Command action.devices.commands.PauseUnpause is not supported",
):
await trt.execute(trait.COMMAND_PAUSEUNPAUSE, BASIC_DATA, {"start": True}, {})


async def test_startstop_cover_assumed(hass: HomeAssistant) -> None:
@pytest.mark.parametrize(
(
"domain",
"state_open",
"state_closed",
"state_opening",
"state_closing",
"supported_features",
"service_close",
"service_open",
"service_stop",
"service_toggle",
),
[
(
cover.DOMAIN,
cover.STATE_OPEN,
cover.STATE_CLOSED,
cover.STATE_OPENING,
cover.STATE_CLOSING,
CoverEntityFeature.STOP
| CoverEntityFeature.OPEN
| CoverEntityFeature.CLOSE,
cover.SERVICE_OPEN_COVER,
cover.SERVICE_CLOSE_COVER,
cover.SERVICE_STOP_COVER,
cover.SERVICE_TOGGLE,
),
(
valve.DOMAIN,
valve.STATE_OPEN,
valve.STATE_CLOSED,
valve.STATE_OPENING,
valve.STATE_CLOSING,
ValveEntityFeature.STOP
| ValveEntityFeature.OPEN
| ValveEntityFeature.CLOSE,
valve.SERVICE_OPEN_VALVE,
valve.SERVICE_CLOSE_VALVE,
valve.SERVICE_STOP_VALVE,
cover.SERVICE_TOGGLE,
),
],
)
async def test_startstop_cover_valve_assumed(
hass: HomeAssistant,
domain: str,
state_open: str,
state_closed: str,
state_opening: str,
state_closing: str,
supported_features: str,
service_open: str,
service_close: str,
service_stop: str,
service_toggle: str,
) -> None:
"""Test startStop trait support for cover domain of assumed state."""
trt = trait.StartStopTrait(
hass,
State(
"cover.bla",
cover.STATE_CLOSED,
f"{domain}.bla",
state_closed,
{
ATTR_SUPPORTED_FEATURES: CoverEntityFeature.STOP,
ATTR_SUPPORTED_FEATURES: supported_features,
ATTR_ASSUMED_STATE: True,
},
),
BASIC_CONFIG,
)

stop_calls = async_mock_service(hass, cover.DOMAIN, cover.SERVICE_STOP_COVER)
await trt.execute(trait.COMMAND_STARTSTOP, BASIC_DATA, {"start": False}, {})
assert len(stop_calls) == 1
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)
stop_calls = async_mock_service(hass, domain, service_stop)
toggle_calls = async_mock_service(hass, domain, service_toggle)
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"}
assert len(toggle_calls) == 0
assert stop_calls[0].data == {ATTR_ENTITY_ID: f"{domain}.bla"}

with pytest.raises(SmartHomeError, match="Starting a valve is not supported"):
await trt.execute(trait.COMMAND_STARTSTOP, BASIC_DATA, {"start": True}, {})
stop_calls.clear()
await trt.execute(trait.COMMAND_STARTSTOP, BASIC_DATA, {"start": True}, {})
assert len(stop_calls) == 0
assert len(toggle_calls) == 1
assert toggle_calls[0].data == {ATTR_ENTITY_ID: f"{domain}.bla"}


@pytest.mark.parametrize("supported_color_modes", [["hs"], ["rgb"], ["xy"]])
Expand Down