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
4 changed files with 286 additions and 6 deletions
|
@ -43,6 +43,7 @@ TRAIT_SCENE = PREFIX_TRAITS + 'Scene'
|
|||
TRAIT_TEMPERATURE_SETTING = PREFIX_TRAITS + 'TemperatureSetting'
|
||||
TRAIT_LOCKUNLOCK = PREFIX_TRAITS + 'LockUnlock'
|
||||
TRAIT_FANSPEED = PREFIX_TRAITS + 'FanSpeed'
|
||||
TRAIT_MODES = PREFIX_TRAITS + 'Modes'
|
||||
|
||||
PREFIX_COMMANDS = 'action.devices.commands.'
|
||||
COMMAND_ONOFF = PREFIX_COMMANDS + 'OnOff'
|
||||
|
@ -59,7 +60,7 @@ COMMAND_THERMOSTAT_TEMPERATURE_SET_RANGE = (
|
|||
COMMAND_THERMOSTAT_SET_MODE = PREFIX_COMMANDS + 'ThermostatSetMode'
|
||||
COMMAND_LOCKUNLOCK = PREFIX_COMMANDS + 'LockUnlock'
|
||||
COMMAND_FANSPEED = PREFIX_COMMANDS + 'SetFanSpeed'
|
||||
|
||||
COMMAND_MODES = PREFIX_COMMANDS + 'SetModes'
|
||||
|
||||
TRAITS = []
|
||||
|
||||
|
@ -752,3 +753,188 @@ class FanSpeedTrait(_Trait):
|
|||
ATTR_ENTITY_ID: self.state.entity_id,
|
||||
fan.ATTR_SPEED: params['fanSpeed']
|
||||
}, 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
|
||||
https://home-assistant.io/components/demo/
|
||||
"""
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.components.media_player import (
|
||||
MEDIA_TYPE_MOVIE, MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW,
|
||||
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,
|
||||
MediaPlayerDevice)
|
||||
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):
|
||||
|
@ -34,7 +34,7 @@ DEFAULT_SOUND_MODE = 'Dummy Music'
|
|||
YOUTUBE_PLAYER_SUPPORT = \
|
||||
SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
|
||||
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 = \
|
||||
SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
|
||||
|
|
|
@ -141,7 +141,10 @@ DEMO_DEVICES = [{
|
|||
'name': 'Bedroom'
|
||||
},
|
||||
'traits':
|
||||
['action.devices.traits.OnOff', 'action.devices.traits.Brightness'],
|
||||
[
|
||||
'action.devices.traits.OnOff', 'action.devices.traits.Brightness',
|
||||
'action.devices.traits.Modes'
|
||||
],
|
||||
'type':
|
||||
'action.devices.types.SWITCH',
|
||||
'willReportState':
|
||||
|
@ -153,7 +156,10 @@ DEMO_DEVICES = [{
|
|||
'name': 'Living Room'
|
||||
},
|
||||
'traits':
|
||||
['action.devices.traits.OnOff', 'action.devices.traits.Brightness'],
|
||||
[
|
||||
'action.devices.traits.OnOff', 'action.devices.traits.Brightness',
|
||||
'action.devices.traits.Modes'
|
||||
],
|
||||
'type':
|
||||
'action.devices.types.SWITCH',
|
||||
'willReportState':
|
||||
|
@ -163,7 +169,7 @@ DEMO_DEVICES = [{
|
|||
'name': {
|
||||
'name': 'Lounge room'
|
||||
},
|
||||
'traits': ['action.devices.traits.OnOff'],
|
||||
'traits': ['action.devices.traits.OnOff', 'action.devices.traits.Modes'],
|
||||
'type': 'action.devices.types.SWITCH',
|
||||
'willReportState': False
|
||||
}, {
|
||||
|
|
|
@ -916,3 +916,91 @@ async def test_fan_speed(hass):
|
|||
'entity_id': 'fan.living_room_fan',
|
||||
'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…
Add table
Reference in a new issue