From 2b60fca08d417912b39063ad315ec7f695aa2e2e Mon Sep 17 00:00:00 2001 From: Robbie Trencheny Date: Fri, 17 Nov 2017 09:14:22 -0800 Subject: [PATCH] Alexa improvements (#10632) * Initial scene support * Initial fan support * ordering * Initial lock support * Scenes cant be deactivated; Correct the scene display category * Initial input_boolean support * Support customization of Alexa discovered entities * Initial media player support * Add input_boolean to tests * Add play/pause/stop/next/previous to media player * Add missing functions and pylint * Set manufacturerName to Home Assistant since the value is displayed in app * Add scene test * Add fan tests * Add lock test * Fix volume logic * Add volume tests * settup -> setup * Remove unused variable * Set required scene description as per docs * Allow setting scene category (ACTIVITY_TRIGGER/SCENE_TRIGGER) * Add alert, automation and group support/tests * Change display categories to match docs * simplify down the display category props into a single prop which can be used on any entity * Fix tests to expect proper display categories * Add cover support * sort things * Use generic homeassistant domain for turn on/off --- homeassistant/components/alexa/smart_home.py | 334 +++++++++++- tests/components/alexa/test_smart_home.py | 546 ++++++++++++++++++- 2 files changed, 853 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/alexa/smart_home.py b/homeassistant/components/alexa/smart_home.py index a96386cbdf9..c5a849ad560 100644 --- a/homeassistant/components/alexa/smart_home.py +++ b/homeassistant/components/alexa/smart_home.py @@ -4,9 +4,16 @@ import logging import math from uuid import uuid4 +import homeassistant.core as ha from homeassistant.const import ( - ATTR_SUPPORTED_FEATURES, ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF) -from homeassistant.components import switch, light, script + ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, SERVICE_LOCK, + SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY, + SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_STOP, + SERVICE_SET_COVER_POSITION, SERVICE_TURN_OFF, SERVICE_TURN_ON, + SERVICE_UNLOCK, SERVICE_VOLUME_SET) +from homeassistant.components import ( + alert, automation, cover, fan, group, input_boolean, light, lock, + media_player, scene, script, switch) import homeassistant.util.color as color_util from homeassistant.util.decorator import Registry @@ -14,15 +21,32 @@ HANDLERS = Registry() _LOGGER = logging.getLogger(__name__) API_DIRECTIVE = 'directive' +API_ENDPOINT = 'endpoint' API_EVENT = 'event' API_HEADER = 'header' API_PAYLOAD = 'payload' -API_ENDPOINT = 'endpoint' + +ATTR_ALEXA_DESCRIPTION = 'alexa_description' +ATTR_ALEXA_DISPLAY_CATEGORIES = 'alexa_display_categories' +ATTR_ALEXA_HIDDEN = 'alexa_hidden' +ATTR_ALEXA_NAME = 'alexa_name' MAPPING_COMPONENT = { - script.DOMAIN: ['SWITCH', ('Alexa.PowerController',), None], - switch.DOMAIN: ['SWITCH', ('Alexa.PowerController',), None], + alert.DOMAIN: ['OTHER', ('Alexa.PowerController',), None], + automation.DOMAIN: ['OTHER', ('Alexa.PowerController',), None], + cover.DOMAIN: [ + 'DOOR', ('Alexa.PowerController',), { + cover.SUPPORT_SET_POSITION: 'Alexa.PercentageController', + } + ], + fan.DOMAIN: [ + 'OTHER', ('Alexa.PowerController',), { + fan.SUPPORT_SET_SPEED: 'Alexa.PercentageController', + } + ], + group.DOMAIN: ['OTHER', ('Alexa.PowerController',), None], + input_boolean.DOMAIN: ['OTHER', ('Alexa.PowerController',), None], light.DOMAIN: [ 'LIGHT', ('Alexa.PowerController',), { light.SUPPORT_BRIGHTNESS: 'Alexa.BrightnessController', @@ -31,6 +55,20 @@ MAPPING_COMPONENT = { light.SUPPORT_COLOR_TEMP: 'Alexa.ColorTemperatureController', } ], + lock.DOMAIN: ['SMARTLOCK', ('Alexa.LockController',), None], + media_player.DOMAIN: [ + 'TV', ('Alexa.PowerController',), { + media_player.SUPPORT_VOLUME_SET: 'Alexa.Speaker', + media_player.SUPPORT_PLAY: 'Alexa.PlaybackController', + media_player.SUPPORT_PAUSE: 'Alexa.PlaybackController', + media_player.SUPPORT_STOP: 'Alexa.PlaybackController', + media_player.SUPPORT_NEXT_TRACK: 'Alexa.PlaybackController', + media_player.SUPPORT_PREVIOUS_TRACK: 'Alexa.PlaybackController', + } + ], + scene.DOMAIN: ['ACTIVITY_TRIGGER', ('Alexa.SceneController',), None], + script.DOMAIN: ['OTHER', ('Alexa.PowerController',), None], + switch.DOMAIN: ['SWITCH', ('Alexa.PowerController',), None], } @@ -108,18 +146,33 @@ def async_api_discovery(hass, request): discovery_endpoints = [] for entity in hass.states.async_all(): + if entity.attributes.get(ATTR_ALEXA_HIDDEN, False): + continue + class_data = MAPPING_COMPONENT.get(entity.domain) if not class_data: continue + friendly_name = entity.attributes.get(ATTR_ALEXA_NAME, entity.name) + description = entity.attributes.get(ATTR_ALEXA_DESCRIPTION, + entity.entity_id) + + # Required description as per Amazon Scene docs + if entity.domain == scene.DOMAIN: + scene_fmt = '%s (Scene connected via Home Assistant)' + description = scene_fmt.format(description) + + cat_key = ATTR_ALEXA_DISPLAY_CATEGORIES + display_categories = entity.attributes.get(cat_key, class_data[0]) + endpoint = { - 'displayCategories': [class_data[0]], + 'displayCategories': [display_categories], 'additionalApplianceDetails': {}, 'endpointId': entity.entity_id.replace('.', '#'), - 'friendlyName': entity.name, - 'description': '', - 'manufacturerName': 'Unknown', + 'friendlyName': friendly_name, + 'description': description, + 'manufacturerName': 'Home Assistant', } actions = set() @@ -175,7 +228,7 @@ def extract_entity(funct): @asyncio.coroutine def async_api_turn_on(hass, request, entity): """Process a turn on request.""" - yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, { + yield from hass.services.async_call(ha.DOMAIN, SERVICE_TURN_ON, { ATTR_ENTITY_ID: entity.entity_id }, blocking=True) @@ -187,7 +240,7 @@ def async_api_turn_on(hass, request, entity): @asyncio.coroutine def async_api_turn_off(hass, request, entity): """Process a turn off request.""" - yield from hass.services.async_call(entity.domain, SERVICE_TURN_OFF, { + yield from hass.services.async_call(ha.DOMAIN, SERVICE_TURN_OFF, { ATTR_ENTITY_ID: entity.entity_id }, blocking=True) @@ -310,3 +363,262 @@ def async_api_increase_color_temp(hass, request, entity): }, blocking=True) return api_message(request) + + +@HANDLERS.register(('Alexa.SceneController', 'Activate')) +@extract_entity +@asyncio.coroutine +def async_api_activate(hass, request, entity): + """Process a activate request.""" + yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, { + ATTR_ENTITY_ID: entity.entity_id + }, blocking=True) + + return api_message(request) + + +@HANDLERS.register(('Alexa.PercentageController', 'SetPercentage')) +@extract_entity +@asyncio.coroutine +def async_api_set_percentage(hass, request, entity): + """Process a set percentage request.""" + percentage = int(request[API_PAYLOAD]['percentage']) + service = None + data = {ATTR_ENTITY_ID: entity.entity_id} + + if entity.domain == fan.DOMAIN: + service = fan.SERVICE_SET_SPEED + speed = "off" + + if percentage <= 33: + speed = "low" + elif percentage <= 66: + speed = "medium" + elif percentage <= 100: + speed = "high" + data[fan.ATTR_SPEED] = speed + + elif entity.domain == cover.DOMAIN: + service = SERVICE_SET_COVER_POSITION + data[cover.ATTR_POSITION] = percentage + + yield from hass.services.async_call(entity.domain, service, + data, blocking=True) + + return api_message(request) + + +@HANDLERS.register(('Alexa.PercentageController', 'AdjustPercentage')) +@extract_entity +@asyncio.coroutine +def async_api_adjust_percentage(hass, request, entity): + """Process a adjust percentage request.""" + percentage_delta = int(request[API_PAYLOAD]['percentageDelta']) + service = None + data = {ATTR_ENTITY_ID: entity.entity_id} + + if entity.domain == fan.DOMAIN: + service = fan.SERVICE_SET_SPEED + speed = entity.attributes.get(fan.ATTR_SPEED) + + if speed == "off": + current = 0 + elif speed == "low": + current = 33 + elif speed == "medium": + current = 66 + elif speed == "high": + current = 100 + + # set percentage + percentage = max(0, percentage_delta + current) + speed = "off" + + if percentage <= 33: + speed = "low" + elif percentage <= 66: + speed = "medium" + elif percentage <= 100: + speed = "high" + + data[fan.ATTR_SPEED] = speed + + elif entity.domain == cover.DOMAIN: + service = SERVICE_SET_COVER_POSITION + + current = entity.attributes.get(cover.ATTR_POSITION) + + data[cover.ATTR_POSITION] = max(0, percentage_delta + current) + + yield from hass.services.async_call(entity.domain, service, + data, blocking=True) + + return api_message(request) + + +@HANDLERS.register(('Alexa.LockController', 'Lock')) +@extract_entity +@asyncio.coroutine +def async_api_lock(hass, request, entity): + """Process a lock request.""" + yield from hass.services.async_call(entity.domain, SERVICE_LOCK, { + ATTR_ENTITY_ID: entity.entity_id + }, blocking=True) + + return api_message(request) + + +# Not supported by Alexa yet +@HANDLERS.register(('Alexa.LockController', 'Unlock')) +@extract_entity +@asyncio.coroutine +def async_api_unlock(hass, request, entity): + """Process a unlock request.""" + yield from hass.services.async_call(entity.domain, SERVICE_UNLOCK, { + ATTR_ENTITY_ID: entity.entity_id + }, blocking=True) + + return api_message(request) + + +@HANDLERS.register(('Alexa.Speaker', 'SetVolume')) +@extract_entity +@asyncio.coroutine +def async_api_set_volume(hass, request, entity): + """Process a set volume request.""" + volume = round(float(request[API_PAYLOAD]['volume'] / 100), 2) + + data = { + ATTR_ENTITY_ID: entity.entity_id, + media_player.ATTR_MEDIA_VOLUME_LEVEL: volume, + } + + yield from hass.services.async_call(entity.domain, SERVICE_VOLUME_SET, + data, blocking=True) + + return api_message(request) + + +@HANDLERS.register(('Alexa.Speaker', 'AdjustVolume')) +@extract_entity +@asyncio.coroutine +def async_api_adjust_volume(hass, request, entity): + """Process a adjust volume request.""" + volume_delta = int(request[API_PAYLOAD]['volume']) + + current_level = entity.attributes.get(media_player.ATTR_MEDIA_VOLUME_LEVEL) + + # read current state + try: + current = math.floor(int(current_level * 100)) + except ZeroDivisionError: + current = 0 + + volume = float(max(0, volume_delta + current) / 100) + + data = { + ATTR_ENTITY_ID: entity.entity_id, + media_player.ATTR_MEDIA_VOLUME_LEVEL: volume, + } + + yield from hass.services.async_call(entity.domain, + media_player.SERVICE_VOLUME_SET, + data, blocking=True) + + return api_message(request) + + +@HANDLERS.register(('Alexa.Speaker', 'SetMute')) +@extract_entity +@asyncio.coroutine +def async_api_set_mute(hass, request, entity): + """Process a set mute request.""" + mute = bool(request[API_PAYLOAD]['mute']) + + data = { + ATTR_ENTITY_ID: entity.entity_id, + media_player.ATTR_MEDIA_VOLUME_MUTED: mute, + } + + yield from hass.services.async_call(entity.domain, + media_player.SERVICE_VOLUME_MUTE, + data, blocking=True) + + return api_message(request) + + +@HANDLERS.register(('Alexa.PlaybackController', 'Play')) +@extract_entity +@asyncio.coroutine +def async_api_play(hass, request, entity): + """Process a play request.""" + data = { + ATTR_ENTITY_ID: entity.entity_id + } + + yield from hass.services.async_call(entity.domain, SERVICE_MEDIA_PLAY, + data, blocking=True) + + return api_message(request) + + +@HANDLERS.register(('Alexa.PlaybackController', 'Pause')) +@extract_entity +@asyncio.coroutine +def async_api_pause(hass, request, entity): + """Process a pause request.""" + data = { + ATTR_ENTITY_ID: entity.entity_id + } + + yield from hass.services.async_call(entity.domain, SERVICE_MEDIA_PAUSE, + data, blocking=True) + + return api_message(request) + + +@HANDLERS.register(('Alexa.PlaybackController', 'Stop')) +@extract_entity +@asyncio.coroutine +def async_api_stop(hass, request, entity): + """Process a stop request.""" + data = { + ATTR_ENTITY_ID: entity.entity_id + } + + yield from hass.services.async_call(entity.domain, SERVICE_MEDIA_STOP, + data, blocking=True) + + return api_message(request) + + +@HANDLERS.register(('Alexa.PlaybackController', 'Next')) +@extract_entity +@asyncio.coroutine +def async_api_next(hass, request, entity): + """Process a next request.""" + data = { + ATTR_ENTITY_ID: entity.entity_id + } + + yield from hass.services.async_call(entity.domain, + SERVICE_MEDIA_NEXT_TRACK, + data, blocking=True) + + return api_message(request) + + +@HANDLERS.register(('Alexa.PlaybackController', 'Previous')) +@extract_entity +@asyncio.coroutine +def async_api_previous(hass, request, entity): + """Process a previous request.""" + data = { + ATTR_ENTITY_ID: entity.entity_id + } + + yield from hass.services.async_call(entity.domain, + SERVICE_MEDIA_PREVIOUS_TRACK, + data, blocking=True) + + return api_message(request) diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index eadb72f91c0..3fe9145f2d6 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -99,7 +99,7 @@ def test_discovery_request(hass): """Test alexa discovery request.""" request = get_new_request('Alexa.Discovery', 'Discover') - # settup test devices + # setup test devices hass.states.async_set( 'switch.test', 'on', {'friendly_name': "Test switch"}) @@ -117,12 +117,52 @@ def test_discovery_request(hass): hass.states.async_set( 'script.test', 'off', {'friendly_name': "Test script"}) + hass.states.async_set( + 'input_boolean.test', 'off', {'friendly_name': "Test input boolean"}) + + hass.states.async_set( + 'scene.test', 'off', {'friendly_name': "Test scene"}) + + hass.states.async_set( + 'fan.test_1', 'off', {'friendly_name': "Test fan 1"}) + + hass.states.async_set( + 'fan.test_2', 'off', { + 'friendly_name': "Test fan 2", 'supported_features': 1, + 'speed_list': ['low', 'medium', 'high'] + }) + + hass.states.async_set( + 'lock.test', 'off', {'friendly_name': "Test lock"}) + + hass.states.async_set( + 'media_player.test', 'off', { + 'friendly_name': "Test media player", + 'supported_features': 20925, + 'volume_level': 1 + }) + + hass.states.async_set( + 'alert.test', 'off', {'friendly_name': "Test alert"}) + + hass.states.async_set( + 'automation.test', 'off', {'friendly_name': "Test automation"}) + + hass.states.async_set( + 'group.test', 'off', {'friendly_name': "Test group"}) + + hass.states.async_set( + 'cover.test', 'off', { + 'friendly_name': "Test cover", 'supported_features': 255, + 'position': 85 + }) + msg = yield from smart_home.async_handle_message(hass, request) assert 'event' in msg msg = msg['event'] - assert len(msg['payload']['endpoints']) == 5 + assert len(msg['payload']['endpoints']) == 15 assert msg['header']['name'] == 'Discover.Response' assert msg['header']['namespace'] == 'Alexa.Discovery' @@ -174,13 +214,108 @@ def test_discovery_request(hass): continue if appliance['endpointId'] == 'script#test': - assert appliance['displayCategories'][0] == "SWITCH" + assert appliance['displayCategories'][0] == "OTHER" assert appliance['friendlyName'] == "Test script" assert len(appliance['capabilities']) == 1 assert appliance['capabilities'][-1]['interface'] == \ 'Alexa.PowerController' continue + if appliance['endpointId'] == 'input_boolean#test': + assert appliance['displayCategories'][0] == "OTHER" + assert appliance['friendlyName'] == "Test input boolean" + assert len(appliance['capabilities']) == 1 + assert appliance['capabilities'][-1]['interface'] == \ + 'Alexa.PowerController' + continue + + if appliance['endpointId'] == 'scene#test': + assert appliance['displayCategories'][0] == "ACTIVITY_TRIGGER" + assert appliance['friendlyName'] == "Test scene" + assert len(appliance['capabilities']) == 1 + assert appliance['capabilities'][-1]['interface'] == \ + 'Alexa.SceneController' + continue + + if appliance['endpointId'] == 'fan#test_1': + assert appliance['displayCategories'][0] == "OTHER" + assert appliance['friendlyName'] == "Test fan 1" + assert len(appliance['capabilities']) == 1 + assert appliance['capabilities'][-1]['interface'] == \ + 'Alexa.PowerController' + continue + + if appliance['endpointId'] == 'fan#test_2': + assert appliance['displayCategories'][0] == "OTHER" + assert appliance['friendlyName'] == "Test fan 2" + assert len(appliance['capabilities']) == 2 + + caps = set() + for feature in appliance['capabilities']: + caps.add(feature['interface']) + + assert 'Alexa.PercentageController' in caps + assert 'Alexa.PowerController' in caps + continue + + if appliance['endpointId'] == 'lock#test': + assert appliance['displayCategories'][0] == "SMARTLOCK" + assert appliance['friendlyName'] == "Test lock" + assert len(appliance['capabilities']) == 1 + assert appliance['capabilities'][-1]['interface'] == \ + 'Alexa.LockController' + continue + + if appliance['endpointId'] == 'media_player#test': + assert appliance['displayCategories'][0] == "TV" + assert appliance['friendlyName'] == "Test media player" + assert len(appliance['capabilities']) == 3 + caps = set() + for feature in appliance['capabilities']: + caps.add(feature['interface']) + + assert 'Alexa.PowerController' in caps + assert 'Alexa.Speaker' in caps + assert 'Alexa.PlaybackController' in caps + continue + + if appliance['endpointId'] == 'alert#test': + assert appliance['displayCategories'][0] == "OTHER" + assert appliance['friendlyName'] == "Test alert" + assert len(appliance['capabilities']) == 1 + assert appliance['capabilities'][-1]['interface'] == \ + 'Alexa.PowerController' + continue + + if appliance['endpointId'] == 'automation#test': + assert appliance['displayCategories'][0] == "OTHER" + assert appliance['friendlyName'] == "Test automation" + assert len(appliance['capabilities']) == 1 + assert appliance['capabilities'][-1]['interface'] == \ + 'Alexa.PowerController' + continue + + if appliance['endpointId'] == 'group#test': + assert appliance['displayCategories'][0] == "OTHER" + assert appliance['friendlyName'] == "Test group" + assert len(appliance['capabilities']) == 1 + assert appliance['capabilities'][-1]['interface'] == \ + 'Alexa.PowerController' + continue + + if appliance['endpointId'] == 'cover#test': + assert appliance['displayCategories'][0] == "DOOR" + assert appliance['friendlyName'] == "Test cover" + assert len(appliance['capabilities']) == 2 + + caps = set() + for feature in appliance['capabilities']: + caps.add(feature['interface']) + + assert 'Alexa.PercentageController' in caps + assert 'Alexa.PowerController' in caps + continue + raise AssertionError("Unknown appliance!") @@ -217,19 +352,21 @@ def test_api_function_not_implemented(hass): @asyncio.coroutine -@pytest.mark.parametrize("domain", ['light', 'switch', 'script']) +@pytest.mark.parametrize("domain", ['alert', 'automation', 'group', + 'input_boolean', 'light', 'script', + 'switch']) def test_api_turn_on(hass, domain): """Test api turn on process.""" request = get_new_request( 'Alexa.PowerController', 'TurnOn', '{}#test'.format(domain)) - # settup test devices + # setup test devices hass.states.async_set( '{}.test'.format(domain), 'off', { 'friendly_name': "Test {}".format(domain) }) - call = async_mock_service(hass, domain, 'turn_on') + call = async_mock_service(hass, 'homeassistant', 'turn_on') msg = yield from smart_home.async_handle_message(hass, request) @@ -242,19 +379,21 @@ def test_api_turn_on(hass, domain): @asyncio.coroutine -@pytest.mark.parametrize("domain", ['light', 'switch', 'script']) +@pytest.mark.parametrize("domain", ['alert', 'automation', 'group', + 'input_boolean', 'light', 'script', + 'switch']) def test_api_turn_off(hass, domain): """Test api turn on process.""" request = get_new_request( 'Alexa.PowerController', 'TurnOff', '{}#test'.format(domain)) - # settup test devices + # setup test devices hass.states.async_set( '{}.test'.format(domain), 'on', { 'friendly_name': "Test {}".format(domain) }) - call = async_mock_service(hass, domain, 'turn_off') + call = async_mock_service(hass, 'homeassistant', 'turn_off') msg = yield from smart_home.async_handle_message(hass, request) @@ -275,7 +414,7 @@ def test_api_set_brightness(hass): # add payload request['directive']['payload']['brightness'] = '50' - # settup test devices + # setup test devices hass.states.async_set( 'light.test', 'off', {'friendly_name': "Test light"}) @@ -303,7 +442,7 @@ def test_api_adjust_brightness(hass, result, adjust): # add payload request['directive']['payload']['brightnessDelta'] = adjust - # settup test devices + # setup test devices hass.states.async_set( 'light.test', 'off', { 'friendly_name': "Test light", 'brightness': '77' @@ -335,7 +474,7 @@ def test_api_set_color_rgb(hass): 'brightness': '0.342', } - # settup test devices + # setup test devices hass.states.async_set( 'light.test', 'off', { 'friendly_name': "Test light", @@ -368,7 +507,7 @@ def test_api_set_color_xy(hass): 'brightness': '0.342', } - # settup test devices + # setup test devices hass.states.async_set( 'light.test', 'off', { 'friendly_name': "Test light", @@ -399,7 +538,7 @@ def test_api_set_color_temperature(hass): # add payload request['directive']['payload']['colorTemperatureInKelvin'] = '7500' - # settup test devices + # setup test devices hass.states.async_set( 'light.test', 'off', {'friendly_name': "Test light"}) @@ -424,7 +563,7 @@ def test_api_decrease_color_temp(hass, result, initial): 'Alexa.ColorTemperatureController', 'DecreaseColorTemperature', 'light#test') - # settup test devices + # setup test devices hass.states.async_set( 'light.test', 'off', { 'friendly_name': "Test light", 'color_temp': initial, @@ -452,7 +591,7 @@ def test_api_increase_color_temp(hass, result, initial): 'Alexa.ColorTemperatureController', 'IncreaseColorTemperature', 'light#test') - # settup test devices + # setup test devices hass.states.async_set( 'light.test', 'off', { 'friendly_name': "Test light", 'color_temp': initial, @@ -470,3 +609,378 @@ def test_api_increase_color_temp(hass, result, initial): assert call_light[0].data['entity_id'] == 'light.test' assert call_light[0].data['color_temp'] == result assert msg['header']['name'] == 'Response' + + +@asyncio.coroutine +@pytest.mark.parametrize("domain", ['scene']) +def test_api_activate(hass, domain): + """Test api activate process.""" + request = get_new_request( + 'Alexa.SceneController', 'Activate', '{}#test'.format(domain)) + + # setup test devices + hass.states.async_set( + '{}.test'.format(domain), 'off', { + 'friendly_name': "Test {}".format(domain) + }) + + call = async_mock_service(hass, domain, 'turn_on') + + msg = yield from smart_home.async_handle_message(hass, request) + + assert 'event' in msg + msg = msg['event'] + + assert len(call) == 1 + assert call[0].data['entity_id'] == '{}.test'.format(domain) + assert msg['header']['name'] == 'Response' + + +@asyncio.coroutine +def test_api_set_percentage_fan(hass): + """Test api set percentage for fan process.""" + request = get_new_request( + 'Alexa.PercentageController', 'SetPercentage', 'fan#test_2') + + # add payload + request['directive']['payload']['percentage'] = '50' + + # setup test devices + hass.states.async_set( + 'fan.test_2', 'off', {'friendly_name': "Test fan"}) + + call_fan = async_mock_service(hass, 'fan', 'set_speed') + + msg = yield from smart_home.async_handle_message(hass, request) + + assert 'event' in msg + msg = msg['event'] + + assert len(call_fan) == 1 + assert call_fan[0].data['entity_id'] == 'fan.test_2' + assert call_fan[0].data['speed'] == 'medium' + assert msg['header']['name'] == 'Response' + + +@asyncio.coroutine +def test_api_set_percentage_cover(hass): + """Test api set percentage for cover process.""" + request = get_new_request( + 'Alexa.PercentageController', 'SetPercentage', 'cover#test') + + # add payload + request['directive']['payload']['percentage'] = '50' + + # setup test devices + hass.states.async_set( + 'cover.test', 'closed', { + 'friendly_name': "Test cover" + }) + + call_cover = async_mock_service(hass, 'cover', 'set_cover_position') + + msg = yield from smart_home.async_handle_message(hass, request) + + assert 'event' in msg + msg = msg['event'] + + assert len(call_cover) == 1 + assert call_cover[0].data['entity_id'] == 'cover.test' + assert call_cover[0].data['position'] == 50 + assert msg['header']['name'] == 'Response' + + +@asyncio.coroutine +@pytest.mark.parametrize( + "result,adjust", [('high', '-5'), ('off', '5'), ('low', '-80')]) +def test_api_adjust_percentage_fan(hass, result, adjust): + """Test api adjust percentage for fan process.""" + request = get_new_request( + 'Alexa.PercentageController', 'AdjustPercentage', 'fan#test_2') + + # add payload + request['directive']['payload']['percentageDelta'] = adjust + + # setup test devices + hass.states.async_set( + 'fan.test_2', 'on', { + 'friendly_name': "Test fan 2", 'speed': 'high' + }) + + call_fan = async_mock_service(hass, 'fan', 'set_speed') + + msg = yield from smart_home.async_handle_message(hass, request) + + assert 'event' in msg + msg = msg['event'] + + assert len(call_fan) == 1 + assert call_fan[0].data['entity_id'] == 'fan.test_2' + assert call_fan[0].data['speed'] == result + assert msg['header']['name'] == 'Response' + + +@asyncio.coroutine +@pytest.mark.parametrize( + "result,adjust", [(25, '-5'), (35, '5'), (0, '-80')]) +def test_api_adjust_percentage_cover(hass, result, adjust): + """Test api adjust percentage for cover process.""" + request = get_new_request( + 'Alexa.PercentageController', 'AdjustPercentage', 'cover#test') + + # add payload + request['directive']['payload']['percentageDelta'] = adjust + + # setup test devices + hass.states.async_set( + 'cover.test', 'closed', { + 'friendly_name': "Test cover", + 'position': 30 + }) + + call_cover = async_mock_service(hass, 'cover', 'set_cover_position') + + msg = yield from smart_home.async_handle_message(hass, request) + + assert 'event' in msg + msg = msg['event'] + + assert len(call_cover) == 1 + assert call_cover[0].data['entity_id'] == 'cover.test' + assert call_cover[0].data['position'] == result + assert msg['header']['name'] == 'Response' + + +@asyncio.coroutine +@pytest.mark.parametrize("domain", ['lock']) +def test_api_lock(hass, domain): + """Test api lock process.""" + request = get_new_request( + 'Alexa.LockController', 'Lock', '{}#test'.format(domain)) + + # setup test devices + hass.states.async_set( + '{}.test'.format(domain), 'off', { + 'friendly_name': "Test {}".format(domain) + }) + + call = async_mock_service(hass, domain, 'lock') + + msg = yield from smart_home.async_handle_message(hass, request) + + assert 'event' in msg + msg = msg['event'] + + assert len(call) == 1 + assert call[0].data['entity_id'] == '{}.test'.format(domain) + assert msg['header']['name'] == 'Response' + + +@asyncio.coroutine +@pytest.mark.parametrize("domain", ['media_player']) +def test_api_play(hass, domain): + """Test api play process.""" + request = get_new_request( + 'Alexa.PlaybackController', 'Play', '{}#test'.format(domain)) + + # setup test devices + hass.states.async_set( + '{}.test'.format(domain), 'off', { + 'friendly_name': "Test {}".format(domain) + }) + + call = async_mock_service(hass, domain, 'media_play') + + msg = yield from smart_home.async_handle_message(hass, request) + + assert 'event' in msg + msg = msg['event'] + + assert len(call) == 1 + assert call[0].data['entity_id'] == '{}.test'.format(domain) + assert msg['header']['name'] == 'Response' + + +@asyncio.coroutine +@pytest.mark.parametrize("domain", ['media_player']) +def test_api_pause(hass, domain): + """Test api pause process.""" + request = get_new_request( + 'Alexa.PlaybackController', 'Pause', '{}#test'.format(domain)) + + # setup test devices + hass.states.async_set( + '{}.test'.format(domain), 'off', { + 'friendly_name': "Test {}".format(domain) + }) + + call = async_mock_service(hass, domain, 'media_pause') + + msg = yield from smart_home.async_handle_message(hass, request) + + assert 'event' in msg + msg = msg['event'] + + assert len(call) == 1 + assert call[0].data['entity_id'] == '{}.test'.format(domain) + assert msg['header']['name'] == 'Response' + + +@asyncio.coroutine +@pytest.mark.parametrize("domain", ['media_player']) +def test_api_stop(hass, domain): + """Test api stop process.""" + request = get_new_request( + 'Alexa.PlaybackController', 'Stop', '{}#test'.format(domain)) + + # setup test devices + hass.states.async_set( + '{}.test'.format(domain), 'off', { + 'friendly_name': "Test {}".format(domain) + }) + + call = async_mock_service(hass, domain, 'media_stop') + + msg = yield from smart_home.async_handle_message(hass, request) + + assert 'event' in msg + msg = msg['event'] + + assert len(call) == 1 + assert call[0].data['entity_id'] == '{}.test'.format(domain) + assert msg['header']['name'] == 'Response' + + +@asyncio.coroutine +@pytest.mark.parametrize("domain", ['media_player']) +def test_api_next(hass, domain): + """Test api next process.""" + request = get_new_request( + 'Alexa.PlaybackController', 'Next', '{}#test'.format(domain)) + + # setup test devices + hass.states.async_set( + '{}.test'.format(domain), 'off', { + 'friendly_name': "Test {}".format(domain) + }) + + call = async_mock_service(hass, domain, 'media_next_track') + + msg = yield from smart_home.async_handle_message(hass, request) + + assert 'event' in msg + msg = msg['event'] + + assert len(call) == 1 + assert call[0].data['entity_id'] == '{}.test'.format(domain) + assert msg['header']['name'] == 'Response' + + +@asyncio.coroutine +@pytest.mark.parametrize("domain", ['media_player']) +def test_api_previous(hass, domain): + """Test api previous process.""" + request = get_new_request( + 'Alexa.PlaybackController', 'Previous', '{}#test'.format(domain)) + + # setup test devices + hass.states.async_set( + '{}.test'.format(domain), 'off', { + 'friendly_name': "Test {}".format(domain) + }) + + call = async_mock_service(hass, domain, 'media_previous_track') + + msg = yield from smart_home.async_handle_message(hass, request) + + assert 'event' in msg + msg = msg['event'] + + assert len(call) == 1 + assert call[0].data['entity_id'] == '{}.test'.format(domain) + assert msg['header']['name'] == 'Response' + + +@asyncio.coroutine +def test_api_set_volume(hass): + """Test api set volume process.""" + request = get_new_request( + 'Alexa.Speaker', 'SetVolume', 'media_player#test') + + # add payload + request['directive']['payload']['volume'] = 50 + + # setup test devices + hass.states.async_set( + 'media_player.test', 'off', { + 'friendly_name': "Test media player", 'volume_level': 0 + }) + + call_media_player = async_mock_service(hass, 'media_player', 'volume_set') + + msg = yield from smart_home.async_handle_message(hass, request) + + assert 'event' in msg + msg = msg['event'] + + assert len(call_media_player) == 1 + assert call_media_player[0].data['entity_id'] == 'media_player.test' + assert call_media_player[0].data['volume_level'] == 0.5 + assert msg['header']['name'] == 'Response' + + +@asyncio.coroutine +@pytest.mark.parametrize( + "result,adjust", [(0.7, '-5'), (0.8, '5'), (0, '-80')]) +def test_api_adjust_volume(hass, result, adjust): + """Test api adjust volume process.""" + request = get_new_request( + 'Alexa.Speaker', 'AdjustVolume', 'media_player#test') + + # add payload + request['directive']['payload']['volume'] = adjust + + # setup test devices + hass.states.async_set( + 'media_player.test', 'off', { + 'friendly_name': "Test media player", 'volume_level': 0.75 + }) + + call_media_player = async_mock_service(hass, 'media_player', 'volume_set') + + msg = yield from smart_home.async_handle_message(hass, request) + + assert 'event' in msg + msg = msg['event'] + + assert len(call_media_player) == 1 + assert call_media_player[0].data['entity_id'] == 'media_player.test' + assert call_media_player[0].data['volume_level'] == result + assert msg['header']['name'] == 'Response' + + +@asyncio.coroutine +@pytest.mark.parametrize("domain", ['media_player']) +def test_api_mute(hass, domain): + """Test api mute process.""" + request = get_new_request( + 'Alexa.Speaker', 'SetMute', '{}#test'.format(domain)) + + request['directive']['payload']['mute'] = True + + # setup test devices + hass.states.async_set( + '{}.test'.format(domain), 'off', { + 'friendly_name': "Test {}".format(domain) + }) + + call = async_mock_service(hass, domain, 'volume_mute') + + msg = yield from smart_home.async_handle_message(hass, request) + + assert 'event' in msg + msg = msg['event'] + + assert len(call) == 1 + assert call[0].data['entity_id'] == '{}.test'.format(domain) + assert msg['header']['name'] == 'Response'