mirror of
https://github.com/home-assistant/core.git
synced 2025-07-13 08:17:08 +00:00
Allow all sources and sound modes to be selected in google assistant (#30418)
* Simplify media player source list Google don't need a whitelisted set of modes anymore. So let's just forward any mode that we have. * Report current mode with the setting name, not a synonym * Refactor mode generation to support other modes * Support sound mode as mode as well * Adjust failing test now with sound modes
This commit is contained in:
parent
d6e230e66b
commit
8dc57a3700
@ -1121,98 +1121,9 @@ class ModesTrait(_Trait):
|
|||||||
name = TRAIT_MODES
|
name = TRAIT_MODES
|
||||||
commands = [COMMAND_MODES]
|
commands = [COMMAND_MODES]
|
||||||
|
|
||||||
# Google requires specific mode names and settings. Here is the full list.
|
SYNONYMS = {
|
||||||
# https://developers.google.com/actions/reference/smarthome/traits/modes
|
"input source": ["input source", "input", "source"],
|
||||||
# All settings are mapped here as of 2018-11-28 and can be used for other
|
"sound mode": ["sound mode", "effects"],
|
||||||
# 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"],
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@ -1221,42 +1132,51 @@ class ModesTrait(_Trait):
|
|||||||
if domain != media_player.DOMAIN:
|
if domain != media_player.DOMAIN:
|
||||||
return False
|
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):
|
def sync_attributes(self):
|
||||||
"""Return mode attributes for a sync request."""
|
"""Return mode attributes for a sync request."""
|
||||||
sources_list = self.state.attributes.get(
|
|
||||||
media_player.ATTR_INPUT_SOURCE_LIST, []
|
|
||||||
)
|
|
||||||
modes = []
|
|
||||||
sources = {}
|
|
||||||
|
|
||||||
if sources_list:
|
def _generate(name, settings):
|
||||||
sources = {
|
mode = {
|
||||||
"name": self.HA_TO_GOOGLE.get(media_player.ATTR_INPUT_SOURCE),
|
"name": name,
|
||||||
"name_values": [{"name_synonym": ["input source"], "lang": "en"}],
|
"name_values": [
|
||||||
|
{"name_synonym": self.SYNONYMS.get(name, [name]), "lang": "en"}
|
||||||
|
],
|
||||||
"settings": [],
|
"settings": [],
|
||||||
"ordered": False,
|
"ordered": False,
|
||||||
}
|
}
|
||||||
for source in sources_list:
|
for setting in settings:
|
||||||
if source in self.SUPPORTED_MODE_SETTINGS:
|
mode["settings"].append(
|
||||||
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(
|
|
||||||
{
|
{
|
||||||
"setting_name": src,
|
"setting_name": setting,
|
||||||
"setting_values": [{"setting_synonym": synonyms, "lang": "en"}],
|
"setting_values": [
|
||||||
|
{
|
||||||
|
"setting_synonym": self.SYNONYMS.get(
|
||||||
|
setting, [setting]
|
||||||
|
),
|
||||||
|
"lang": "en",
|
||||||
|
}
|
||||||
|
],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
if sources:
|
return mode
|
||||||
modes.append(sources)
|
|
||||||
|
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}
|
payload = {"availableModes": modes}
|
||||||
|
|
||||||
return payload
|
return payload
|
||||||
@ -1267,14 +1187,12 @@ class ModesTrait(_Trait):
|
|||||||
response = {}
|
response = {}
|
||||||
mode_settings = {}
|
mode_settings = {}
|
||||||
|
|
||||||
if attrs.get(media_player.ATTR_INPUT_SOURCE_LIST):
|
if media_player.ATTR_INPUT_SOURCE_LIST in attrs:
|
||||||
mode_settings.update(
|
mode_settings["input source"] = attrs.get(media_player.ATTR_INPUT_SOURCE)
|
||||||
{
|
|
||||||
media_player.ATTR_INPUT_SOURCE: attrs.get(
|
if media_player.ATTR_SOUND_MODE_LIST in attrs:
|
||||||
media_player.ATTR_INPUT_SOURCE
|
mode_settings["sound mode"] = attrs.get(media_player.ATTR_SOUND_MODE)
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
if mode_settings:
|
if mode_settings:
|
||||||
response["on"] = self.state.state != STATE_OFF
|
response["on"] = self.state.state != STATE_OFF
|
||||||
response["online"] = True
|
response["online"] = True
|
||||||
@ -1285,25 +1203,32 @@ class ModesTrait(_Trait):
|
|||||||
async def execute(self, command, data, params, challenge):
|
async def execute(self, command, data, params, challenge):
|
||||||
"""Execute an SetModes command."""
|
"""Execute an SetModes command."""
|
||||||
settings = params.get("updateModeSettings")
|
settings = params.get("updateModeSettings")
|
||||||
requested_source = settings.get(
|
requested_source = settings.get("input source")
|
||||||
self.HA_TO_GOOGLE.get(media_player.ATTR_INPUT_SOURCE)
|
sound_mode = settings.get("sound mode")
|
||||||
)
|
|
||||||
|
|
||||||
if requested_source:
|
if requested_source:
|
||||||
for src in self.state.attributes.get(media_player.ATTR_INPUT_SOURCE_LIST):
|
await self.hass.services.async_call(
|
||||||
if src.lower() == requested_source.lower():
|
media_player.DOMAIN,
|
||||||
source = src
|
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(
|
if sound_mode:
|
||||||
media_player.DOMAIN,
|
await self.hass.services.async_call(
|
||||||
media_player.SERVICE_SELECT_SOURCE,
|
media_player.DOMAIN,
|
||||||
{
|
media_player.SERVICE_SELECT_SOUND_MODE,
|
||||||
ATTR_ENTITY_ID: self.state.entity_id,
|
{
|
||||||
media_player.ATTR_INPUT_SOURCE: source,
|
ATTR_ENTITY_ID: self.state.entity_id,
|
||||||
},
|
media_player.ATTR_SOUND_MODE: sound_mode,
|
||||||
blocking=True,
|
},
|
||||||
context=data.context,
|
blocking=True,
|
||||||
)
|
context=data.context,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@register_trait
|
@register_trait
|
||||||
|
@ -200,7 +200,11 @@ DEMO_DEVICES = [
|
|||||||
{
|
{
|
||||||
"id": "media_player.walkman",
|
"id": "media_player.walkman",
|
||||||
"name": {"name": "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",
|
"type": "action.devices.types.SWITCH",
|
||||||
"willReportState": False,
|
"willReportState": False,
|
||||||
},
|
},
|
||||||
|
@ -1266,19 +1266,19 @@ async def test_modes(hass):
|
|||||||
"availableModes": [
|
"availableModes": [
|
||||||
{
|
{
|
||||||
"name": "input source",
|
"name": "input source",
|
||||||
"name_values": [{"name_synonym": ["input source"], "lang": "en"}],
|
"name_values": [
|
||||||
|
{"name_synonym": ["input source", "input", "source"], "lang": "en"}
|
||||||
|
],
|
||||||
"settings": [
|
"settings": [
|
||||||
{
|
{
|
||||||
"setting_name": "media",
|
"setting_name": "media",
|
||||||
"setting_values": [
|
"setting_values": [
|
||||||
{"setting_synonym": ["media", "media mode"], "lang": "en"}
|
{"setting_synonym": ["media"], "lang": "en"}
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"setting_name": "game",
|
"setting_name": "game",
|
||||||
"setting_values": [
|
"setting_values": [{"setting_synonym": ["game"], "lang": "en"}],
|
||||||
{"setting_synonym": ["game", "game mode"], "lang": "en"}
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"setting_name": "chromecast",
|
"setting_name": "chromecast",
|
||||||
@ -1286,6 +1286,81 @@ async def test_modes(hass):
|
|||||||
{"setting_synonym": ["chromecast"], "lang": "en"}
|
{"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,
|
"ordered": False,
|
||||||
}
|
}
|
||||||
@ -1293,36 +1368,30 @@ async def test_modes(hass):
|
|||||||
}
|
}
|
||||||
|
|
||||||
assert trt.query_attributes() == {
|
assert trt.query_attributes() == {
|
||||||
"currentModeSettings": {"source": "game"},
|
"currentModeSettings": {"sound mode": "stereo"},
|
||||||
"on": True,
|
"on": True,
|
||||||
"online": True,
|
"online": True,
|
||||||
}
|
}
|
||||||
|
|
||||||
assert trt.can_execute(
|
assert trt.can_execute(
|
||||||
trait.COMMAND_MODES,
|
trait.COMMAND_MODES, params={"updateModeSettings": {"sound mode": "stereo"}},
|
||||||
params={
|
|
||||||
"updateModeSettings": {
|
|
||||||
trt.HA_TO_GOOGLE.get(media_player.ATTR_INPUT_SOURCE): "media"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
calls = async_mock_service(
|
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(
|
await trt.execute(
|
||||||
trait.COMMAND_MODES,
|
trait.COMMAND_MODES,
|
||||||
BASIC_DATA,
|
BASIC_DATA,
|
||||||
{
|
{"updateModeSettings": {"sound mode": "stereo"}},
|
||||||
"updateModeSettings": {
|
|
||||||
trt.HA_TO_GOOGLE.get(media_player.ATTR_INPUT_SOURCE): "media"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{},
|
{},
|
||||||
)
|
)
|
||||||
|
|
||||||
assert len(calls) == 1
|
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):
|
async def test_openclose_cover(hass):
|
||||||
|
Loading…
x
Reference in New Issue
Block a user