Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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,',
Comment thread
marchingphoenix marked this conversation as resolved.
Outdated
'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,', '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'],
Comment thread
marchingphoenix marked this conversation as resolved.
Outdated
'lang': 'en'
}
]
},
{
'setting_name': 'game',
'setting_values': [
{
'setting_synonym': ['game,', 'game', 'mode'],
Comment thread
marchingphoenix marked this conversation as resolved.
Outdated
'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'
}