Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
188 changes: 187 additions & 1 deletion homeassistant/components/google_assistant/trait.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
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'
Expand All @@ -59,7 +60,7 @@
COMMAND_THERMOSTAT_SET_MODE = PREFIX_COMMANDS + 'ThermostatSetMode'
COMMAND_LOCKUNLOCK = PREFIX_COMMANDS + 'LockUnlock'
COMMAND_FANSPEED = PREFIX_COMMANDS + 'SetFanSpeed'

COMMAND_MODES = PREFIX_COMMANDS + 'SetModes'

TRAITS = []

Expand Down Expand Up @@ -750,3 +751,188 @@ async def execute(self, command, params):
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 changes: 2 additions & 2 deletions homeassistant/components/media_player/demo.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
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,
Expand All @@ -12,7 +13,6 @@
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):
Expand All @@ -34,7 +34,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
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 | \
Expand Down
12 changes: 9 additions & 3 deletions tests/components/google_assistant/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,10 @@
'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':
Expand All @@ -153,7 +156,10 @@
'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':
Expand All @@ -163,7 +169,7 @@
'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
}, {
Expand Down
88 changes: 88 additions & 0 deletions tests/components/google_assistant/test_trait.py
Original file line number Diff line number Diff line change
Expand Up @@ -875,3 +875,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'
}