diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index 06d10c5372b..e89ccaf80c4 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -74,6 +74,7 @@ from .const import ( ERR_ALREADY_DISARMED, ERR_ALREADY_STOPPED, ERR_CHALLENGE_NOT_SETUP, + ERR_FUNCTION_NOT_SUPPORTED, ERR_NO_AVAILABLE_CHANNEL, ERR_NOT_SUPPORTED, ERR_UNSUPPORTED_INPUT, @@ -104,6 +105,7 @@ TRAIT_HUMIDITY_SETTING = f"{PREFIX_TRAITS}HumiditySetting" TRAIT_TRANSPORT_CONTROL = f"{PREFIX_TRAITS}TransportControl" TRAIT_MEDIA_STATE = f"{PREFIX_TRAITS}MediaState" TRAIT_CHANNEL = f"{PREFIX_TRAITS}Channel" +TRAIT_LOCATOR = f"{PREFIX_TRAITS}Locator" PREFIX_COMMANDS = "action.devices.commands." COMMAND_ONOFF = f"{PREFIX_COMMANDS}OnOff" @@ -145,6 +147,7 @@ COMMAND_MEDIA_STOP = f"{PREFIX_COMMANDS}mediaStop" COMMAND_REVERSE = f"{PREFIX_COMMANDS}Reverse" COMMAND_SET_HUMIDITY = f"{PREFIX_COMMANDS}SetHumidity" COMMAND_SELECT_CHANNEL = f"{PREFIX_COMMANDS}selectChannel" +COMMAND_LOCATE = f"{PREFIX_COMMANDS}Locate" TRAITS = [] @@ -566,6 +569,46 @@ class DockTrait(_Trait): ) +@register_trait +class LocatorTrait(_Trait): + """Trait to offer locate functionality. + + https://developers.google.com/actions/smarthome/traits/locator + """ + + name = TRAIT_LOCATOR + commands = [COMMAND_LOCATE] + + @staticmethod + def supported(domain, features, device_class, _): + """Test if state is supported.""" + return domain == vacuum.DOMAIN and features & vacuum.SUPPORT_LOCATE + + def sync_attributes(self): + """Return locator attributes for a sync request.""" + return {} + + def query_attributes(self): + """Return locator query attributes.""" + return {} + + async def execute(self, command, data, params, challenge): + """Execute a locate command.""" + if params.get("silence", False): + raise SmartHomeError( + ERR_FUNCTION_NOT_SUPPORTED, + "Silencing a Locate request is not yet supported", + ) + + await self.hass.services.async_call( + self.state.domain, + vacuum.SERVICE_LOCATE, + {ATTR_ENTITY_ID: self.state.entity_id}, + blocking=True, + context=data.context, + ) + + @register_trait class StartStopTrait(_Trait): """Trait to offer StartStop functionality. diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index c57d894c36d..4ee9ee2b035 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -356,6 +356,37 @@ async def test_dock_vacuum(hass): assert calls[0].data == {ATTR_ENTITY_ID: "vacuum.bla"} +async def test_locate_vacuum(hass): + """Test locate trait support for vacuum domain.""" + assert helpers.get_google_type(vacuum.DOMAIN, None) is not None + assert trait.LocatorTrait.supported( + vacuum.DOMAIN, vacuum.SUPPORT_LOCATE, None, None + ) + + trt = trait.LocatorTrait( + hass, + State( + "vacuum.bla", + vacuum.STATE_IDLE, + {ATTR_SUPPORTED_FEATURES: vacuum.SUPPORT_LOCATE}, + ), + BASIC_CONFIG, + ) + + assert trt.sync_attributes() == {} + + assert trt.query_attributes() == {} + + calls = async_mock_service(hass, vacuum.DOMAIN, vacuum.SERVICE_LOCATE) + await trt.execute(trait.COMMAND_LOCATE, BASIC_DATA, {"silence": False}, {}) + assert len(calls) == 1 + assert calls[0].data == {ATTR_ENTITY_ID: "vacuum.bla"} + + with pytest.raises(helpers.SmartHomeError) as err: + await trt.execute(trait.COMMAND_LOCATE, BASIC_DATA, {"silence": True}, {}) + assert err.value.code == const.ERR_FUNCTION_NOT_SUPPORTED + + async def test_startstop_vacuum(hass): """Test startStop trait support for vacuum domain.""" assert helpers.get_google_type(vacuum.DOMAIN, None) is not None