mirror of
https://github.com/home-assistant/core.git
synced 2025-07-08 13:57:10 +00:00
Add support for Mode trait in Google Assistant. (#18772)
* Add support for Mode trait in Google Assistant. * Simplify supported logic. * Fix SUPPORTED_MODE_SETTINGS to correct rip failures. * more stray commas * update tests.
This commit is contained in:
parent
5c026b1fa2
commit
e50a6ef8af
@ -43,6 +43,7 @@ TRAIT_SCENE = PREFIX_TRAITS + 'Scene'
|
|||||||
TRAIT_TEMPERATURE_SETTING = PREFIX_TRAITS + 'TemperatureSetting'
|
TRAIT_TEMPERATURE_SETTING = PREFIX_TRAITS + 'TemperatureSetting'
|
||||||
TRAIT_LOCKUNLOCK = PREFIX_TRAITS + 'LockUnlock'
|
TRAIT_LOCKUNLOCK = PREFIX_TRAITS + 'LockUnlock'
|
||||||
TRAIT_FANSPEED = PREFIX_TRAITS + 'FanSpeed'
|
TRAIT_FANSPEED = PREFIX_TRAITS + 'FanSpeed'
|
||||||
|
TRAIT_MODES = PREFIX_TRAITS + 'Modes'
|
||||||
|
|
||||||
PREFIX_COMMANDS = 'action.devices.commands.'
|
PREFIX_COMMANDS = 'action.devices.commands.'
|
||||||
COMMAND_ONOFF = PREFIX_COMMANDS + 'OnOff'
|
COMMAND_ONOFF = PREFIX_COMMANDS + 'OnOff'
|
||||||
@ -59,7 +60,7 @@ COMMAND_THERMOSTAT_TEMPERATURE_SET_RANGE = (
|
|||||||
COMMAND_THERMOSTAT_SET_MODE = PREFIX_COMMANDS + 'ThermostatSetMode'
|
COMMAND_THERMOSTAT_SET_MODE = PREFIX_COMMANDS + 'ThermostatSetMode'
|
||||||
COMMAND_LOCKUNLOCK = PREFIX_COMMANDS + 'LockUnlock'
|
COMMAND_LOCKUNLOCK = PREFIX_COMMANDS + 'LockUnlock'
|
||||||
COMMAND_FANSPEED = PREFIX_COMMANDS + 'SetFanSpeed'
|
COMMAND_FANSPEED = PREFIX_COMMANDS + 'SetFanSpeed'
|
||||||
|
COMMAND_MODES = PREFIX_COMMANDS + 'SetModes'
|
||||||
|
|
||||||
TRAITS = []
|
TRAITS = []
|
||||||
|
|
||||||
@ -752,3 +753,188 @@ class FanSpeedTrait(_Trait):
|
|||||||
ATTR_ENTITY_ID: self.state.entity_id,
|
ATTR_ENTITY_ID: self.state.entity_id,
|
||||||
fan.ATTR_SPEED: params['fanSpeed']
|
fan.ATTR_SPEED: params['fanSpeed']
|
||||||
}, blocking=True)
|
}, blocking=True)
|
||||||
|
|
||||||
|
|
||||||
|
@register_trait
|
||||||
|
class ModesTrait(_Trait):
|
||||||
|
"""Trait to set modes.
|
||||||
|
|
||||||
|
https://developers.google.com/actions/smarthome/traits/modes
|
||||||
|
"""
|
||||||
|
|
||||||
|
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']
|
||||||
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def supported(domain, features):
|
||||||
|
"""Test if state is supported."""
|
||||||
|
if domain != media_player.DOMAIN:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return features & media_player.SUPPORT_SELECT_SOURCE
|
||||||
|
|
||||||
|
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"
|
||||||
|
}],
|
||||||
|
"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(
|
||||||
|
{
|
||||||
|
"setting_name": src,
|
||||||
|
"setting_values": [{
|
||||||
|
"setting_synonym": synonyms,
|
||||||
|
"lang": "en"
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if sources:
|
||||||
|
modes.append(sources)
|
||||||
|
payload = {'availableModes': modes}
|
||||||
|
|
||||||
|
return payload
|
||||||
|
|
||||||
|
def query_attributes(self):
|
||||||
|
"""Return current modes."""
|
||||||
|
attrs = self.state.attributes
|
||||||
|
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 mode_settings:
|
||||||
|
response['on'] = self.state.state != STATE_OFF
|
||||||
|
response['online'] = True
|
||||||
|
response['currentModeSettings'] = mode_settings
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
async def execute(self, command, params):
|
||||||
|
"""Execute an SetModes command."""
|
||||||
|
settings = params.get('updateModeSettings')
|
||||||
|
requested_source = settings.get(
|
||||||
|
self.HA_TO_GOOGLE.get(media_player.ATTR_INPUT_SOURCE))
|
||||||
|
|
||||||
|
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: source
|
||||||
|
}, blocking=True)
|
||||||
|
@ -4,6 +4,7 @@ Demo implementation of the media player.
|
|||||||
For more details about this platform, please refer to the documentation
|
For more details about this platform, please refer to the documentation
|
||||||
https://home-assistant.io/components/demo/
|
https://home-assistant.io/components/demo/
|
||||||
"""
|
"""
|
||||||
|
import homeassistant.util.dt as dt_util
|
||||||
from homeassistant.components.media_player import (
|
from homeassistant.components.media_player import (
|
||||||
MEDIA_TYPE_MOVIE, MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW,
|
MEDIA_TYPE_MOVIE, MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW,
|
||||||
SUPPORT_CLEAR_PLAYLIST, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY,
|
SUPPORT_CLEAR_PLAYLIST, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY,
|
||||||
@ -12,7 +13,6 @@ from homeassistant.components.media_player import (
|
|||||||
SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET,
|
SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET,
|
||||||
MediaPlayerDevice)
|
MediaPlayerDevice)
|
||||||
from homeassistant.const import STATE_OFF, STATE_PAUSED, STATE_PLAYING
|
from homeassistant.const import STATE_OFF, STATE_PAUSED, STATE_PLAYING
|
||||||
import homeassistant.util.dt as dt_util
|
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||||
@ -34,7 +34,7 @@ DEFAULT_SOUND_MODE = 'Dummy Music'
|
|||||||
YOUTUBE_PLAYER_SUPPORT = \
|
YOUTUBE_PLAYER_SUPPORT = \
|
||||||
SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
|
SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
|
||||||
SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_PLAY_MEDIA | SUPPORT_PLAY | \
|
SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_PLAY_MEDIA | SUPPORT_PLAY | \
|
||||||
SUPPORT_SHUFFLE_SET | SUPPORT_SELECT_SOUND_MODE
|
SUPPORT_SHUFFLE_SET | SUPPORT_SELECT_SOUND_MODE | SUPPORT_SELECT_SOURCE
|
||||||
|
|
||||||
MUSIC_PLAYER_SUPPORT = \
|
MUSIC_PLAYER_SUPPORT = \
|
||||||
SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
|
SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
|
||||||
|
@ -141,7 +141,10 @@ DEMO_DEVICES = [{
|
|||||||
'name': 'Bedroom'
|
'name': 'Bedroom'
|
||||||
},
|
},
|
||||||
'traits':
|
'traits':
|
||||||
['action.devices.traits.OnOff', 'action.devices.traits.Brightness'],
|
[
|
||||||
|
'action.devices.traits.OnOff', 'action.devices.traits.Brightness',
|
||||||
|
'action.devices.traits.Modes'
|
||||||
|
],
|
||||||
'type':
|
'type':
|
||||||
'action.devices.types.SWITCH',
|
'action.devices.types.SWITCH',
|
||||||
'willReportState':
|
'willReportState':
|
||||||
@ -153,7 +156,10 @@ DEMO_DEVICES = [{
|
|||||||
'name': 'Living Room'
|
'name': 'Living Room'
|
||||||
},
|
},
|
||||||
'traits':
|
'traits':
|
||||||
['action.devices.traits.OnOff', 'action.devices.traits.Brightness'],
|
[
|
||||||
|
'action.devices.traits.OnOff', 'action.devices.traits.Brightness',
|
||||||
|
'action.devices.traits.Modes'
|
||||||
|
],
|
||||||
'type':
|
'type':
|
||||||
'action.devices.types.SWITCH',
|
'action.devices.types.SWITCH',
|
||||||
'willReportState':
|
'willReportState':
|
||||||
@ -163,7 +169,7 @@ DEMO_DEVICES = [{
|
|||||||
'name': {
|
'name': {
|
||||||
'name': 'Lounge room'
|
'name': 'Lounge room'
|
||||||
},
|
},
|
||||||
'traits': ['action.devices.traits.OnOff'],
|
'traits': ['action.devices.traits.OnOff', 'action.devices.traits.Modes'],
|
||||||
'type': 'action.devices.types.SWITCH',
|
'type': 'action.devices.types.SWITCH',
|
||||||
'willReportState': False
|
'willReportState': False
|
||||||
}, {
|
}, {
|
||||||
|
@ -916,3 +916,91 @@ async def test_fan_speed(hass):
|
|||||||
'entity_id': 'fan.living_room_fan',
|
'entity_id': 'fan.living_room_fan',
|
||||||
'speed': 'medium'
|
'speed': 'medium'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_modes(hass):
|
||||||
|
"""Test Mode trait."""
|
||||||
|
assert trait.ModesTrait.supported(
|
||||||
|
media_player.DOMAIN, media_player.SUPPORT_SELECT_SOURCE)
|
||||||
|
|
||||||
|
trt = trait.ModesTrait(
|
||||||
|
hass, State(
|
||||||
|
'media_player.living_room', media_player.STATE_PLAYING,
|
||||||
|
attributes={
|
||||||
|
media_player.ATTR_INPUT_SOURCE_LIST: [
|
||||||
|
'media', 'game', 'chromecast', 'plex'
|
||||||
|
],
|
||||||
|
media_player.ATTR_INPUT_SOURCE: 'game'
|
||||||
|
}),
|
||||||
|
BASIC_CONFIG)
|
||||||
|
|
||||||
|
attribs = trt.sync_attributes()
|
||||||
|
assert attribs == {
|
||||||
|
'availableModes': [
|
||||||
|
{
|
||||||
|
'name': 'input source',
|
||||||
|
'name_values': [
|
||||||
|
{
|
||||||
|
'name_synonym': ['input source'],
|
||||||
|
'lang': 'en'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'settings': [
|
||||||
|
{
|
||||||
|
'setting_name': 'media',
|
||||||
|
'setting_values': [
|
||||||
|
{
|
||||||
|
'setting_synonym': ['media', 'media mode'],
|
||||||
|
'lang': 'en'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'setting_name': 'game',
|
||||||
|
'setting_values': [
|
||||||
|
{
|
||||||
|
'setting_synonym': ['game', 'game mode'],
|
||||||
|
'lang': 'en'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'setting_name': 'chromecast',
|
||||||
|
'setting_values': [
|
||||||
|
{
|
||||||
|
'setting_synonym': ['chromecast'],
|
||||||
|
'lang': 'en'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'ordered': False
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
assert trt.query_attributes() == {
|
||||||
|
'currentModeSettings': {'source': 'game'},
|
||||||
|
'on': True,
|
||||||
|
'online': True
|
||||||
|
}
|
||||||
|
|
||||||
|
assert trt.can_execute(
|
||||||
|
trait.COMMAND_MODES, params={
|
||||||
|
'updateModeSettings': {
|
||||||
|
trt.HA_TO_GOOGLE.get(media_player.ATTR_INPUT_SOURCE): 'media'
|
||||||
|
}})
|
||||||
|
|
||||||
|
calls = async_mock_service(
|
||||||
|
hass, media_player.DOMAIN, media_player.SERVICE_SELECT_SOURCE)
|
||||||
|
await trt.execute(
|
||||||
|
trait.COMMAND_MODES, params={
|
||||||
|
'updateModeSettings': {
|
||||||
|
trt.HA_TO_GOOGLE.get(media_player.ATTR_INPUT_SOURCE): 'media'
|
||||||
|
}})
|
||||||
|
|
||||||
|
assert len(calls) == 1
|
||||||
|
assert calls[0].data == {
|
||||||
|
'entity_id': 'media_player.living_room',
|
||||||
|
'source': 'media'
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user