From 6fa04aa3e3ef299287e97d43107ca50b9f75928a Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Sun, 19 Jul 2020 01:07:32 +0200 Subject: [PATCH] Add support for InputSelector trait (#35753) --- homeassistant/components/demo/media_player.py | 7 +- .../components/google_assistant/trait.py | 88 ++++++++++++------- tests/components/google_assistant/__init__.py | 1 + .../components/google_assistant/test_trait.py | 59 ++++--------- 4 files changed, 81 insertions(+), 74 deletions(-) diff --git a/homeassistant/components/demo/media_player.py b/homeassistant/components/demo/media_player.py index 9cfb5582acc..f00f1fb781e 100644 --- a/homeassistant/components/demo/media_player.py +++ b/homeassistant/components/demo/media_player.py @@ -61,7 +61,6 @@ YOUTUBE_PLAYER_SUPPORT = ( | SUPPORT_PLAY | SUPPORT_SHUFFLE_SET | SUPPORT_SELECT_SOUND_MODE - | SUPPORT_SELECT_SOURCE | SUPPORT_SEEK ) @@ -397,6 +396,7 @@ class DemoTVShowPlayer(AbstractDemoPlayer): self._cur_episode = 1 self._episode_count = 13 self._source = "dvd" + self._source_list = ["dvd", "youtube"] @property def media_content_id(self): @@ -448,6 +448,11 @@ class DemoTVShowPlayer(AbstractDemoPlayer): """Return the current input source.""" return self._source + @property + def source_list(self): + """List of available sources.""" + return self._source_list + @property def supported_features(self): """Flag media player features that are supported.""" diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index 70cc9bd9f52..da3363fb4d9 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -86,6 +86,7 @@ TRAIT_TEMPERATURE_SETTING = f"{PREFIX_TRAITS}TemperatureSetting" TRAIT_LOCKUNLOCK = f"{PREFIX_TRAITS}LockUnlock" TRAIT_FANSPEED = f"{PREFIX_TRAITS}FanSpeed" TRAIT_MODES = f"{PREFIX_TRAITS}Modes" +TRAIT_INPUTSELECTOR = f"{PREFIX_TRAITS}InputSelector" TRAIT_OPENCLOSE = f"{PREFIX_TRAITS}OpenClose" TRAIT_VOLUME = f"{PREFIX_TRAITS}Volume" TRAIT_ARMDISARM = f"{PREFIX_TRAITS}ArmDisarm" @@ -112,6 +113,7 @@ COMMAND_THERMOSTAT_SET_MODE = f"{PREFIX_COMMANDS}ThermostatSetMode" COMMAND_LOCKUNLOCK = f"{PREFIX_COMMANDS}LockUnlock" COMMAND_FANSPEED = f"{PREFIX_COMMANDS}SetFanSpeed" COMMAND_MODES = f"{PREFIX_COMMANDS}SetModes" +COMMAND_INPUT = f"{PREFIX_COMMANDS}SetInput" COMMAND_OPENCLOSE = f"{PREFIX_COMMANDS}OpenClose" COMMAND_SET_VOLUME = f"{PREFIX_COMMANDS}setVolume" COMMAND_VOLUME_RELATIVE = f"{PREFIX_COMMANDS}volumeRelative" @@ -1213,7 +1215,6 @@ class ModesTrait(_Trait): commands = [COMMAND_MODES] SYNONYMS = { - "input source": ["input source", "input", "source"], "sound mode": ["sound mode", "effects"], "option": ["option", "setting", "mode", "value"], } @@ -1230,10 +1231,7 @@ class ModesTrait(_Trait): if domain != media_player.DOMAIN: return False - return ( - features & media_player.SUPPORT_SELECT_SOURCE - or features & media_player.SUPPORT_SELECT_SOUND_MODE - ) + return features & media_player.SUPPORT_SELECT_SOUND_MODE def sync_attributes(self): """Return mode attributes for a sync request.""" @@ -1266,13 +1264,6 @@ class ModesTrait(_Trait): attrs = self.state.attributes modes = [] if self.state.domain == media_player.DOMAIN: - if media_player.ATTR_INPUT_SOURCE_LIST in attrs: - modes.append( - _generate( - "input source", attrs[media_player.ATTR_INPUT_SOURCE_LIST] - ) - ) - if media_player.ATTR_SOUND_MODE_LIST in attrs: modes.append( _generate("sound mode", attrs[media_player.ATTR_SOUND_MODE_LIST]) @@ -1294,11 +1285,6 @@ class ModesTrait(_Trait): mode_settings = {} if self.state.domain == media_player.DOMAIN: - if media_player.ATTR_INPUT_SOURCE_LIST in attrs: - mode_settings["input source"] = attrs.get( - media_player.ATTR_INPUT_SOURCE - ) - if media_player.ATTR_SOUND_MODE_LIST in attrs: mode_settings["sound mode"] = attrs.get(media_player.ATTR_SOUND_MODE) elif self.state.domain == input_select.DOMAIN: @@ -1352,21 +1338,8 @@ class ModesTrait(_Trait): ) return - requested_source = settings.get("input source") sound_mode = settings.get("sound mode") - if requested_source: - await self.hass.services.async_call( - media_player.DOMAIN, - media_player.SERVICE_SELECT_SOURCE, - { - ATTR_ENTITY_ID: self.state.entity_id, - media_player.ATTR_INPUT_SOURCE: requested_source, - }, - blocking=True, - context=data.context, - ) - if sound_mode: await self.hass.services.async_call( media_player.DOMAIN, @@ -1380,6 +1353,61 @@ class ModesTrait(_Trait): ) +@register_trait +class InputSelectorTrait(_Trait): + """Trait to set modes. + + https://developers.google.com/assistant/smarthome/traits/inputselector + """ + + name = TRAIT_INPUTSELECTOR + commands = [COMMAND_INPUT] + + SYNONYMS = {} + + @staticmethod + def supported(domain, features, device_class): + """Test if state is supported.""" + if domain == media_player.DOMAIN and ( + features & media_player.SUPPORT_SELECT_SOURCE + ): + return True + + return False + + def sync_attributes(self): + """Return mode attributes for a sync request.""" + attrs = self.state.attributes + inputs = [ + {"key": source, "names": [{"name_synonym": [source], "lang": "en"}]} + for source in attrs.get(media_player.ATTR_INPUT_SOURCE_LIST, []) + ] + + payload = {"availableInputs": inputs, "orderedInputs": True} + + return payload + + def query_attributes(self): + """Return current modes.""" + attrs = self.state.attributes + return {"currentInput": attrs.get(media_player.ATTR_INPUT_SOURCE, "")} + + async def execute(self, command, data, params, challenge): + """Execute an SetInputSource command.""" + requested_source = params.get("newInput") + + await self.hass.services.async_call( + media_player.DOMAIN, + media_player.SERVICE_SELECT_SOURCE, + { + ATTR_ENTITY_ID: self.state.entity_id, + media_player.ATTR_INPUT_SOURCE: requested_source, + }, + blocking=True, + context=data.context, + ) + + @register_trait class OpenCloseTrait(_Trait): """Trait to open and close a cover. diff --git a/tests/components/google_assistant/__init__.py b/tests/components/google_assistant/__init__.py index 45adc281524..a801a6c960f 100644 --- a/tests/components/google_assistant/__init__.py +++ b/tests/components/google_assistant/__init__.py @@ -190,6 +190,7 @@ DEMO_DEVICES = [ "id": "media_player.lounge_room", "name": {"name": "Lounge room"}, "traits": [ + "action.devices.traits.InputSelector", "action.devices.traits.OnOff", "action.devices.traits.Modes", "action.devices.traits.TransportControl", diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index adcdbd8291d..9d99736c87f 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -1313,14 +1313,14 @@ async def test_fan_speed(hass): assert calls[0].data == {"entity_id": "fan.living_room_fan", "speed": "medium"} -async def test_modes_media_player(hass): - """Test Media Player Mode trait.""" +async def test_inputselector(hass): + """Test input selector trait.""" assert helpers.get_google_type(media_player.DOMAIN, None) is not None - assert trait.ModesTrait.supported( + assert trait.InputSelectorTrait.supported( media_player.DOMAIN, media_player.SUPPORT_SELECT_SOURCE, None ) - trt = trait.ModesTrait( + trt = trait.InputSelectorTrait( hass, State( "media_player.living_room", @@ -1340,56 +1340,29 @@ async def test_modes_media_player(hass): attribs = trt.sync_attributes() assert attribs == { - "availableModes": [ + "availableInputs": [ + {"key": "media", "names": [{"name_synonym": ["media"], "lang": "en"}]}, + {"key": "game", "names": [{"name_synonym": ["game"], "lang": "en"}]}, { - "name": "input source", - "name_values": [ - {"name_synonym": ["input source", "input", "source"], "lang": "en"} - ], - "settings": [ - { - "setting_name": "media", - "setting_values": [ - {"setting_synonym": ["media"], "lang": "en"} - ], - }, - { - "setting_name": "game", - "setting_values": [{"setting_synonym": ["game"], "lang": "en"}], - }, - { - "setting_name": "chromecast", - "setting_values": [ - {"setting_synonym": ["chromecast"], "lang": "en"} - ], - }, - { - "setting_name": "plex", - "setting_values": [{"setting_synonym": ["plex"], "lang": "en"}], - }, - ], - "ordered": False, - } - ] + "key": "chromecast", + "names": [{"name_synonym": ["chromecast"], "lang": "en"}], + }, + {"key": "plex", "names": [{"name_synonym": ["plex"], "lang": "en"}]}, + ], + "orderedInputs": True, } assert trt.query_attributes() == { - "currentModeSettings": {"input source": "game"}, - "on": True, + "currentInput": "game", } - assert trt.can_execute( - trait.COMMAND_MODES, params={"updateModeSettings": {"input source": "media"}}, - ) + assert trt.can_execute(trait.COMMAND_INPUT, params={"newInput": "media"},) calls = async_mock_service( hass, media_player.DOMAIN, media_player.SERVICE_SELECT_SOURCE ) await trt.execute( - trait.COMMAND_MODES, - BASIC_DATA, - {"updateModeSettings": {"input source": "media"}}, - {}, + trait.COMMAND_INPUT, BASIC_DATA, {"newInput": "media"}, {}, ) assert len(calls) == 1