From aade4e63b8c5d74ee24e3159094e333be5443480 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Mon, 30 Nov 2020 09:34:34 +0100 Subject: [PATCH] Support asking covers to stop using google assistant (#43537) --- .../components/google_assistant/const.py | 1 + .../components/google_assistant/trait.py | 71 ++++++++++++++++--- tests/components/google_assistant/__init__.py | 15 +++- .../google_assistant/test_smart_home.py | 5 +- .../components/google_assistant/test_trait.py | 45 ++++++++++++ 5 files changed, 123 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/google_assistant/const.py b/homeassistant/components/google_assistant/const.py index be69f020190..47ceabb20e8 100644 --- a/homeassistant/components/google_assistant/const.py +++ b/homeassistant/components/google_assistant/const.py @@ -104,6 +104,7 @@ ERR_UNSUPPORTED_INPUT = "unsupportedInput" ERR_ALREADY_DISARMED = "alreadyDisarmed" ERR_ALREADY_ARMED = "alreadyArmed" +ERR_ALREADY_STOPPED = "alreadyStopped" ERR_CHALLENGE_NEEDED = "challengeNeeded" ERR_CHALLENGE_NOT_SETUP = "challengeFailedNotSetup" diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index b8d21c0c77b..8790c3c7402 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -67,6 +67,7 @@ from .const import ( CHALLENGE_PIN_NEEDED, ERR_ALREADY_ARMED, ERR_ALREADY_DISARMED, + ERR_ALREADY_STOPPED, ERR_CHALLENGE_NOT_SETUP, ERR_NOT_SUPPORTED, ERR_UNSUPPORTED_INPUT, @@ -564,24 +565,49 @@ class StartStopTrait(_Trait): @staticmethod def supported(domain, features, device_class): """Test if state is supported.""" - return domain == vacuum.DOMAIN + if domain == vacuum.DOMAIN: + return True + + if domain == cover.DOMAIN and features & cover.SUPPORT_STOP: + return True + + return False def sync_attributes(self): """Return StartStop attributes for a sync request.""" - return { - "pausable": self.state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) - & vacuum.SUPPORT_PAUSE - != 0 - } + domain = self.state.domain + if domain == vacuum.DOMAIN: + return { + "pausable": self.state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + & vacuum.SUPPORT_PAUSE + != 0 + } + if domain == cover.DOMAIN: + return {} def query_attributes(self): """Return StartStop query attributes.""" - return { - "isRunning": self.state.state == vacuum.STATE_CLEANING, - "isPaused": self.state.state == vacuum.STATE_PAUSED, - } + domain = self.state.domain + state = self.state.state + + if domain == vacuum.DOMAIN: + return { + "isRunning": state == vacuum.STATE_CLEANING, + "isPaused": state == vacuum.STATE_PAUSED, + } + + if domain == cover.DOMAIN: + return {"isRunning": state in (cover.STATE_CLOSING, cover.STATE_OPENING)} async def execute(self, command, data, params, challenge): + """Execute a StartStop command.""" + domain = self.state.domain + if domain == vacuum.DOMAIN: + return await self._execute_vacuum(command, data, params, challenge) + if domain == cover.DOMAIN: + return await self._execute_cover(command, data, params, challenge) + + async def _execute_vacuum(self, command, data, params, challenge): """Execute a StartStop command.""" if command == COMMAND_STARTSTOP: if params["start"]: @@ -618,6 +644,31 @@ class StartStopTrait(_Trait): context=data.context, ) + async def _execute_cover(self, command, data, params, challenge): + """Execute a StartStop command.""" + if command == COMMAND_STARTSTOP: + if params["start"] is False: + if self.state.state in (cover.STATE_CLOSING, cover.STATE_OPENING): + await self.hass.services.async_call( + self.state.domain, + cover.SERVICE_STOP_COVER, + {ATTR_ENTITY_ID: self.state.entity_id}, + blocking=True, + context=data.context, + ) + else: + raise SmartHomeError( + ERR_ALREADY_STOPPED, "Cover is already stopped" + ) + else: + raise SmartHomeError( + ERR_NOT_SUPPORTED, "Starting a cover is not supported" + ) + else: + raise SmartHomeError( + ERR_NOT_SUPPORTED, f"Command {command} is not supported" + ) + @register_trait class TemperatureSettingTrait(_Trait): diff --git a/tests/components/google_assistant/__init__.py b/tests/components/google_assistant/__init__.py index f4e26a77f48..bbc5b92615c 100644 --- a/tests/components/google_assistant/__init__.py +++ b/tests/components/google_assistant/__init__.py @@ -136,14 +136,20 @@ DEMO_DEVICES = [ { "id": "cover.living_room_window", "name": {"name": "Living Room Window"}, - "traits": ["action.devices.traits.OpenClose"], + "traits": [ + "action.devices.traits.StartStop", + "action.devices.traits.OpenClose", + ], "type": "action.devices.types.BLINDS", "willReportState": False, }, { "id": "cover.hall_window", "name": {"name": "Hall Window"}, - "traits": ["action.devices.traits.OpenClose"], + "traits": [ + "action.devices.traits.StartStop", + "action.devices.traits.OpenClose", + ], "type": "action.devices.types.BLINDS", "willReportState": False, }, @@ -157,7 +163,10 @@ DEMO_DEVICES = [ { "id": "cover.kitchen_window", "name": {"name": "Kitchen Window"}, - "traits": ["action.devices.traits.OpenClose"], + "traits": [ + "action.devices.traits.StartStop", + "action.devices.traits.OpenClose", + ], "type": "action.devices.types.BLINDS", "willReportState": False, }, diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py index 3fcab2dc2a2..27e62fafc73 100644 --- a/tests/components/google_assistant/test_smart_home.py +++ b/tests/components/google_assistant/test_smart_home.py @@ -842,7 +842,10 @@ async def test_device_class_cover(hass, device_class, google_type): "attributes": {"discreteOnlyOpenClose": True}, "id": "cover.demo_sensor", "name": {"name": "Demo Sensor"}, - "traits": ["action.devices.traits.OpenClose"], + "traits": [ + "action.devices.traits.StartStop", + "action.devices.traits.OpenClose", + ], "type": google_type, "willReportState": False, } diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index 2ca2e6c8e6c..4946416e1c8 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -384,6 +384,51 @@ async def test_startstop_vacuum(hass): assert unpause_calls[0].data == {ATTR_ENTITY_ID: "vacuum.bla"} +async def test_startstop_covert(hass): + """Test startStop trait support for vacuum domain.""" + assert helpers.get_google_type(cover.DOMAIN, None) is not None + assert trait.StartStopTrait.supported(cover.DOMAIN, cover.SUPPORT_STOP, None) + + state = State( + "cover.bla", + cover.STATE_CLOSED, + {ATTR_SUPPORTED_FEATURES: cover.SUPPORT_STOP}, + ) + + trt = trait.StartStopTrait( + hass, + state, + BASIC_CONFIG, + ) + + assert trt.sync_attributes() == {} + + for state_value in (cover.STATE_CLOSING, cover.STATE_OPENING): + state.state = state_value + assert trt.query_attributes() == {"isRunning": True} + + 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"} + + for state_value in (cover.STATE_CLOSED, cover.STATE_OPEN): + state.state = state_value + assert trt.query_attributes() == {"isRunning": False} + + with pytest.raises(SmartHomeError, match="Cover 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}, {}) + + 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_color_setting_color_light(hass): """Test ColorSpectrum trait support for light domain.""" assert helpers.get_google_type(light.DOMAIN, None) is not None