diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index d49632755cd..14839066ebe 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -1121,98 +1121,9 @@ class ModesTrait(_Trait): name = TRAIT_MODES commands = [COMMAND_MODES] - # Google requires specific mode names and settings. Here is the full list. - # https://developers.google.com/actions/reference/smarthome/traits/modes - # All settings are mapped here as of 2018-11-28 and can be used for other - # entity types. - - HA_TO_GOOGLE = {media_player.ATTR_INPUT_SOURCE: "input source"} - SUPPORTED_MODE_SETTINGS = { - "xsmall": ["xsmall", "extra small", "min", "minimum", "tiny", "xs"], - "small": ["small", "half"], - "large": ["large", "big", "full"], - "xlarge": ["extra large", "xlarge", "xl"], - "Cool": ["cool", "rapid cool", "rapid cooling"], - "Heat": ["heat"], - "Low": ["low"], - "Medium": ["medium", "med", "mid", "half"], - "High": ["high"], - "Auto": ["auto", "automatic"], - "Bake": ["bake"], - "Roast": ["roast"], - "Convection Bake": ["convection bake", "convect bake"], - "Convection Roast": ["convection roast", "convect roast"], - "Favorite": ["favorite"], - "Broil": ["broil"], - "Warm": ["warm"], - "Off": ["off"], - "On": ["on"], - "Normal": [ - "normal", - "normal mode", - "normal setting", - "standard", - "schedule", - "original", - "default", - "old settings", - ], - "None": ["none"], - "Tap Cold": ["tap cold"], - "Cold Warm": ["cold warm"], - "Hot": ["hot"], - "Extra Hot": ["extra hot"], - "Eco": ["eco"], - "Wool": ["wool", "fleece"], - "Turbo": ["turbo"], - "Rinse": ["rinse", "rinsing", "rinse wash"], - "Away": ["away", "holiday"], - "maximum": ["maximum"], - "media player": ["media player"], - "chromecast": ["chromecast"], - "tv": [ - "tv", - "television", - "tv position", - "television position", - "watching tv", - "watching tv position", - "entertainment", - "entertainment position", - ], - "am fm": ["am fm", "am radio", "fm radio"], - "internet radio": ["internet radio"], - "satellite": ["satellite"], - "game console": ["game console"], - "antifrost": ["antifrost", "anti-frost"], - "boost": ["boost"], - "Clock": ["clock"], - "Message": ["message"], - "Messages": ["messages"], - "News": ["news"], - "Disco": ["disco"], - "antifreeze": ["antifreeze", "anti-freeze", "anti freeze"], - "balanced": ["balanced", "normal"], - "swing": ["swing"], - "media": ["media", "media mode"], - "panic": ["panic"], - "ring": ["ring"], - "frozen": ["frozen", "rapid frozen", "rapid freeze"], - "cotton": ["cotton", "cottons"], - "blend": ["blend", "mix"], - "baby wash": ["baby wash"], - "synthetics": ["synthetic", "synthetics", "compose"], - "hygiene": ["hygiene", "sterilization"], - "smart": ["smart", "intelligent", "intelligence"], - "comfortable": ["comfortable", "comfort"], - "manual": ["manual"], - "energy saving": ["energy saving"], - "sleep": ["sleep"], - "quick wash": ["quick wash", "fast wash"], - "cold": ["cold"], - "airsupply": ["airsupply", "air supply"], - "dehumidification": ["dehumidication", "dehumidify"], - "game": ["game", "game mode"], + SYNONYMS = { + "input source": ["input source", "input", "source"], + "sound mode": ["sound mode", "effects"], } @staticmethod @@ -1221,42 +1132,51 @@ class ModesTrait(_Trait): if domain != media_player.DOMAIN: return False - return features & media_player.SUPPORT_SELECT_SOURCE + return ( + features & media_player.SUPPORT_SELECT_SOURCE + or features & media_player.SUPPORT_SELECT_SOUND_MODE + ) def sync_attributes(self): """Return mode attributes for a sync request.""" - sources_list = self.state.attributes.get( - media_player.ATTR_INPUT_SOURCE_LIST, [] - ) - modes = [] - sources = {} - if sources_list: - sources = { - "name": self.HA_TO_GOOGLE.get(media_player.ATTR_INPUT_SOURCE), - "name_values": [{"name_synonym": ["input source"], "lang": "en"}], + def _generate(name, settings): + mode = { + "name": name, + "name_values": [ + {"name_synonym": self.SYNONYMS.get(name, [name]), "lang": "en"} + ], "settings": [], "ordered": False, } - for source in sources_list: - if source in self.SUPPORTED_MODE_SETTINGS: - src = source - synonyms = self.SUPPORTED_MODE_SETTINGS.get(src) - elif source.lower() in self.SUPPORTED_MODE_SETTINGS: - src = source.lower() - synonyms = self.SUPPORTED_MODE_SETTINGS.get(src) - - else: - continue - - sources["settings"].append( + for setting in settings: + mode["settings"].append( { - "setting_name": src, - "setting_values": [{"setting_synonym": synonyms, "lang": "en"}], + "setting_name": setting, + "setting_values": [ + { + "setting_synonym": self.SYNONYMS.get( + setting, [setting] + ), + "lang": "en", + } + ], } ) - if sources: - modes.append(sources) + return mode + + attrs = self.state.attributes + modes = [] + 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]) + ) + payload = {"availableModes": modes} return payload @@ -1267,14 +1187,12 @@ class ModesTrait(_Trait): response = {} mode_settings = {} - if attrs.get(media_player.ATTR_INPUT_SOURCE_LIST): - mode_settings.update( - { - media_player.ATTR_INPUT_SOURCE: attrs.get( - media_player.ATTR_INPUT_SOURCE - ) - } - ) + 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) + if mode_settings: response["on"] = self.state.state != STATE_OFF response["online"] = True @@ -1285,25 +1203,32 @@ class ModesTrait(_Trait): async def execute(self, command, data, params, challenge): """Execute an SetModes command.""" settings = params.get("updateModeSettings") - requested_source = settings.get( - self.HA_TO_GOOGLE.get(media_player.ATTR_INPUT_SOURCE) - ) + requested_source = settings.get("input source") + sound_mode = settings.get("sound mode") if requested_source: - for src in self.state.attributes.get(media_player.ATTR_INPUT_SOURCE_LIST): - if src.lower() == requested_source.lower(): - source = src + 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, + ) - 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: source, - }, - blocking=True, - context=data.context, - ) + if sound_mode: + await self.hass.services.async_call( + media_player.DOMAIN, + media_player.SERVICE_SELECT_SOUND_MODE, + { + ATTR_ENTITY_ID: self.state.entity_id, + media_player.ATTR_SOUND_MODE: sound_mode, + }, + blocking=True, + context=data.context, + ) @register_trait diff --git a/tests/components/google_assistant/__init__.py b/tests/components/google_assistant/__init__.py index db3b2e68f20..b00b690cba5 100644 --- a/tests/components/google_assistant/__init__.py +++ b/tests/components/google_assistant/__init__.py @@ -200,7 +200,11 @@ DEMO_DEVICES = [ { "id": "media_player.walkman", "name": {"name": "Walkman"}, - "traits": ["action.devices.traits.OnOff", "action.devices.traits.Volume"], + "traits": [ + "action.devices.traits.OnOff", + "action.devices.traits.Volume", + "action.devices.traits.Modes", + ], "type": "action.devices.types.SWITCH", "willReportState": False, }, diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index 98e5149de1d..f59d4006d29 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -1266,19 +1266,19 @@ async def test_modes(hass): "availableModes": [ { "name": "input source", - "name_values": [{"name_synonym": ["input source"], "lang": "en"}], + "name_values": [ + {"name_synonym": ["input source", "input", "source"], "lang": "en"} + ], "settings": [ { "setting_name": "media", "setting_values": [ - {"setting_synonym": ["media", "media mode"], "lang": "en"} + {"setting_synonym": ["media"], "lang": "en"} ], }, { "setting_name": "game", - "setting_values": [ - {"setting_synonym": ["game", "game mode"], "lang": "en"} - ], + "setting_values": [{"setting_synonym": ["game"], "lang": "en"}], }, { "setting_name": "chromecast", @@ -1286,6 +1286,81 @@ async def test_modes(hass): {"setting_synonym": ["chromecast"], "lang": "en"} ], }, + { + "setting_name": "plex", + "setting_values": [{"setting_synonym": ["plex"], "lang": "en"}], + }, + ], + "ordered": False, + } + ] + } + + assert trt.query_attributes() == { + "currentModeSettings": {"input source": "game"}, + "on": True, + "online": True, + } + + assert trt.can_execute( + trait.COMMAND_MODES, params={"updateModeSettings": {"input source": "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"}}, + {}, + ) + + assert len(calls) == 1 + assert calls[0].data == {"entity_id": "media_player.living_room", "source": "media"} + + +async def test_sound_modes(hass): + """Test Mode trait.""" + assert helpers.get_google_type(media_player.DOMAIN, None) is not None + assert trait.ModesTrait.supported( + media_player.DOMAIN, media_player.SUPPORT_SELECT_SOUND_MODE, None + ) + + trt = trait.ModesTrait( + hass, + State( + "media_player.living_room", + media_player.STATE_PLAYING, + attributes={ + media_player.ATTR_SOUND_MODE_LIST: ["stereo", "prologic"], + media_player.ATTR_SOUND_MODE: "stereo", + }, + ), + BASIC_CONFIG, + ) + + attribs = trt.sync_attributes() + assert attribs == { + "availableModes": [ + { + "name": "sound mode", + "name_values": [ + {"name_synonym": ["sound mode", "effects"], "lang": "en"} + ], + "settings": [ + { + "setting_name": "stereo", + "setting_values": [ + {"setting_synonym": ["stereo"], "lang": "en"} + ], + }, + { + "setting_name": "prologic", + "setting_values": [ + {"setting_synonym": ["prologic"], "lang": "en"} + ], + }, ], "ordered": False, } @@ -1293,36 +1368,30 @@ async def test_modes(hass): } assert trt.query_attributes() == { - "currentModeSettings": {"source": "game"}, + "currentModeSettings": {"sound mode": "stereo"}, "on": True, "online": True, } assert trt.can_execute( - trait.COMMAND_MODES, - params={ - "updateModeSettings": { - trt.HA_TO_GOOGLE.get(media_player.ATTR_INPUT_SOURCE): "media" - } - }, + trait.COMMAND_MODES, params={"updateModeSettings": {"sound mode": "stereo"}}, ) calls = async_mock_service( - hass, media_player.DOMAIN, media_player.SERVICE_SELECT_SOURCE + hass, media_player.DOMAIN, media_player.SERVICE_SELECT_SOUND_MODE ) await trt.execute( trait.COMMAND_MODES, BASIC_DATA, - { - "updateModeSettings": { - trt.HA_TO_GOOGLE.get(media_player.ATTR_INPUT_SOURCE): "media" - } - }, + {"updateModeSettings": {"sound mode": "stereo"}}, {}, ) assert len(calls) == 1 - assert calls[0].data == {"entity_id": "media_player.living_room", "source": "media"} + assert calls[0].data == { + "entity_id": "media_player.living_room", + "sound_mode": "stereo", + } async def test_openclose_cover(hass):