Skip to content

Commit 2db7b5c

Browse files
authored
Assume cover or valve is always "running" in google assistant when the state is assumed or the position is reported to allow it to be be stopped (home-assistant#158919)
1 parent 78af3ac commit 2db7b5c

File tree

2 files changed

+198
-8
lines changed

2 files changed

+198
-8
lines changed

homeassistant/components/google_assistant/trait.py

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -908,12 +908,21 @@ def query_attributes(self) -> dict[str, Any]:
908908
}
909909

910910
if domain in COVER_VALVE_DOMAINS:
911+
assumed_state_or_set_position = bool(
912+
(
913+
self.state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
914+
& COVER_VALVE_SET_POSITION_FEATURE[domain]
915+
)
916+
or self.state.attributes.get(ATTR_ASSUMED_STATE)
917+
)
918+
911919
return {
912920
"isRunning": state
913921
in (
914922
COVER_VALVE_STATES[domain]["closing"],
915923
COVER_VALVE_STATES[domain]["opening"],
916924
)
925+
or assumed_state_or_set_position
917926
}
918927

919928
raise NotImplementedError(f"Unsupported domain {domain}")
@@ -975,11 +984,23 @@ async def _execute_cover_or_valve(self, command, data, params, challenge):
975984
"""Execute a StartStop command."""
976985
domain = self.state.domain
977986
if command == COMMAND_START_STOP:
987+
assumed_state_or_set_position = bool(
988+
(
989+
self.state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
990+
& COVER_VALVE_SET_POSITION_FEATURE[domain]
991+
)
992+
or self.state.attributes.get(ATTR_ASSUMED_STATE)
993+
)
994+
978995
if params["start"] is False:
979-
if self.state.state in (
980-
COVER_VALVE_STATES[domain]["closing"],
981-
COVER_VALVE_STATES[domain]["opening"],
982-
) or self.state.attributes.get(ATTR_ASSUMED_STATE):
996+
if (
997+
self.state.state
998+
in (
999+
COVER_VALVE_STATES[domain]["closing"],
1000+
COVER_VALVE_STATES[domain]["opening"],
1001+
)
1002+
or assumed_state_or_set_position
1003+
):
9831004
await self.hass.services.async_call(
9841005
domain,
9851006
SERVICE_STOP_COVER_VALVE[domain],
@@ -992,7 +1013,14 @@ async def _execute_cover_or_valve(self, command, data, params, challenge):
9921013
ERR_ALREADY_STOPPED,
9931014
f"{FRIENDLY_DOMAIN[domain]} is already stopped",
9941015
)
995-
else:
1016+
elif (
1017+
self.state.state
1018+
in (
1019+
COVER_VALVE_STATES[domain]["open"],
1020+
COVER_VALVE_STATES[domain]["closed"],
1021+
)
1022+
or assumed_state_or_set_position
1023+
):
9961024
await self.hass.services.async_call(
9971025
domain,
9981026
SERVICE_TOGGLE_COVER_VALVE[domain],

tests/components/google_assistant/test_trait.py

Lines changed: 165 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -693,7 +693,7 @@ async def test_startstop_lawn_mower(hass: HomeAssistant) -> None:
693693
),
694694
],
695695
)
696-
async def test_startstop_cover_valve(
696+
async def test_startstop_cover_valve_no_assumed_state(
697697
hass: HomeAssistant,
698698
domain: str,
699699
state_open: str,
@@ -706,14 +706,14 @@ async def test_startstop_cover_valve(
706706
service_stop: str,
707707
service_toggle: str,
708708
) -> None:
709-
"""Test startStop trait support."""
709+
"""Test startStop trait support and no assumed state."""
710710
assert helpers.get_google_type(domain, None) is not None
711711
assert trait.StartStopTrait.supported(domain, supported_features, None, None)
712712

713713
state = State(
714714
f"{domain}.bla",
715715
state_closed,
716-
{ATTR_SUPPORTED_FEATURES: supported_features},
716+
{ATTR_SUPPORTED_FEATURES: supported_features, ATTR_ASSUMED_STATE: False},
717717
)
718718

719719
trt = trait.StartStopTrait(
@@ -773,6 +773,168 @@ async def test_startstop_cover_valve(
773773
await trt.execute(trait.COMMAND_PAUSE_UNPAUSE, BASIC_DATA, {"start": True}, {})
774774

775775

776+
@pytest.mark.parametrize(
777+
(
778+
"domain",
779+
"state_open",
780+
"state_closed",
781+
"state_opening",
782+
"state_closing",
783+
"supported_features",
784+
"service_close",
785+
"service_open",
786+
"service_stop",
787+
"service_toggle",
788+
"assumed_state",
789+
),
790+
[
791+
(
792+
cover.DOMAIN,
793+
cover.CoverState.OPEN,
794+
cover.CoverState.CLOSED,
795+
cover.CoverState.OPENING,
796+
cover.CoverState.CLOSING,
797+
CoverEntityFeature.STOP
798+
| CoverEntityFeature.OPEN
799+
| CoverEntityFeature.CLOSE,
800+
cover.SERVICE_OPEN_COVER,
801+
cover.SERVICE_CLOSE_COVER,
802+
cover.SERVICE_STOP_COVER,
803+
cover.SERVICE_TOGGLE,
804+
True,
805+
),
806+
(
807+
valve.DOMAIN,
808+
valve.ValveState.OPEN,
809+
valve.ValveState.CLOSED,
810+
valve.ValveState.OPENING,
811+
valve.ValveState.CLOSING,
812+
ValveEntityFeature.STOP
813+
| ValveEntityFeature.OPEN
814+
| ValveEntityFeature.CLOSE,
815+
valve.SERVICE_OPEN_VALVE,
816+
valve.SERVICE_CLOSE_VALVE,
817+
valve.SERVICE_STOP_VALVE,
818+
cover.SERVICE_TOGGLE,
819+
True,
820+
),
821+
(
822+
cover.DOMAIN,
823+
cover.CoverState.OPEN,
824+
cover.CoverState.CLOSED,
825+
cover.CoverState.OPENING,
826+
cover.CoverState.CLOSING,
827+
CoverEntityFeature.STOP
828+
| CoverEntityFeature.OPEN
829+
| CoverEntityFeature.CLOSE
830+
| CoverEntityFeature.SET_POSITION,
831+
cover.SERVICE_OPEN_COVER,
832+
cover.SERVICE_CLOSE_COVER,
833+
cover.SERVICE_STOP_COVER,
834+
cover.SERVICE_TOGGLE,
835+
False,
836+
),
837+
(
838+
valve.DOMAIN,
839+
valve.ValveState.OPEN,
840+
valve.ValveState.CLOSED,
841+
valve.ValveState.OPENING,
842+
valve.ValveState.CLOSING,
843+
ValveEntityFeature.STOP
844+
| ValveEntityFeature.OPEN
845+
| ValveEntityFeature.CLOSE
846+
| ValveEntityFeature.SET_POSITION,
847+
valve.SERVICE_OPEN_VALVE,
848+
valve.SERVICE_CLOSE_VALVE,
849+
valve.SERVICE_STOP_VALVE,
850+
cover.SERVICE_TOGGLE,
851+
False,
852+
),
853+
],
854+
)
855+
async def test_startstop_cover_valve_with_assumed_state_or_reports_position(
856+
hass: HomeAssistant,
857+
domain: str,
858+
state_open: str,
859+
state_closed: str,
860+
state_opening: str,
861+
state_closing: str,
862+
supported_features: str,
863+
service_open: str,
864+
service_close: str,
865+
service_stop: str,
866+
service_toggle: str,
867+
assumed_state: bool,
868+
) -> None:
869+
"""Test startStop trait support without an assumed state or reporting position."""
870+
assert helpers.get_google_type(domain, None) is not None
871+
assert trait.StartStopTrait.supported(domain, supported_features, None, None)
872+
873+
state = State(
874+
f"{domain}.bla",
875+
state_closed,
876+
{
877+
ATTR_SUPPORTED_FEATURES: supported_features,
878+
ATTR_ASSUMED_STATE: assumed_state,
879+
},
880+
)
881+
882+
trt = trait.StartStopTrait(
883+
hass,
884+
state,
885+
BASIC_CONFIG,
886+
)
887+
888+
assert trt.sync_attributes() == {}
889+
890+
for state_value in (state_closing, state_opening):
891+
state.state = state_value
892+
assert trt.query_attributes()["isRunning"] is True
893+
894+
stop_calls = async_mock_service(hass, domain, service_stop)
895+
open_calls = async_mock_service(hass, domain, service_open)
896+
close_calls = async_mock_service(hass, domain, service_close)
897+
toggle_calls = async_mock_service(hass, domain, service_toggle)
898+
await trt.execute(trait.COMMAND_START_STOP, BASIC_DATA, {"start": False}, {})
899+
assert len(stop_calls) == 1
900+
assert stop_calls[0].data == {ATTR_ENTITY_ID: f"{domain}.bla"}
901+
902+
# Trait attr isRunning always returns True,
903+
# so the cover or valve can always be stopped
904+
for state_value in (state_closing, state_opening, state_closed, state_open):
905+
state.state = state_value
906+
assert trt.query_attributes()["isRunning"] is True
907+
908+
state.state = state_open
909+
910+
# Stop does not raise because we assume the state
911+
# or the position is reported
912+
await trt.execute(trait.COMMAND_START_STOP, BASIC_DATA, {"start": False}, {})
913+
assert len(stop_calls) == 2
914+
915+
# Start triggers toggle open
916+
state.state = state_closed
917+
await trt.execute(trait.COMMAND_START_STOP, BASIC_DATA, {"start": True}, {})
918+
assert len(open_calls) == 0
919+
assert len(close_calls) == 0
920+
assert len(toggle_calls) == 1
921+
assert toggle_calls[0].data == {ATTR_ENTITY_ID: f"{domain}.bla"}
922+
# Second start triggers toggle close
923+
state.state = state_open
924+
await trt.execute(trait.COMMAND_START_STOP, BASIC_DATA, {"start": True}, {})
925+
assert len(open_calls) == 0
926+
assert len(close_calls) == 0
927+
assert len(toggle_calls) == 2
928+
assert toggle_calls[1].data == {ATTR_ENTITY_ID: f"{domain}.bla"}
929+
930+
state.state = state_closed
931+
with pytest.raises(
932+
SmartHomeError,
933+
match="Command action.devices.commands.PauseUnpause is not supported",
934+
):
935+
await trt.execute(trait.COMMAND_PAUSE_UNPAUSE, BASIC_DATA, {"start": True}, {})
936+
937+
776938
@pytest.mark.parametrize(
777939
(
778940
"domain",

0 commit comments

Comments
 (0)