diff --git a/homeassistant/components/alexa/smart_home.py b/homeassistant/components/alexa/smart_home.py index 61db142ac42..03aa5249d91 100644 --- a/homeassistant/components/alexa/smart_home.py +++ b/homeassistant/components/alexa/smart_home.py @@ -1,11 +1,13 @@ """Support for alexa Smart Home Skill API.""" import asyncio import logging +import math from uuid import uuid4 from homeassistant.const import ( ATTR_SUPPORTED_FEATURES, ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF) from homeassistant.components import switch, light +import homeassistant.util.color as color_util from homeassistant.util.decorator import Registry HANDLERS = Registry() @@ -22,7 +24,9 @@ MAPPING_COMPONENT = { switch.DOMAIN: ['SWITCH', ('Alexa.PowerController',), None], light.DOMAIN: [ 'LIGHT', ('Alexa.PowerController',), { - light.SUPPORT_BRIGHTNESS: 'Alexa.BrightnessController' + light.SUPPORT_BRIGHTNESS: 'Alexa.BrightnessController', + light.SUPPORT_RGB_COLOR: 'Alexa.ColorController', + light.SUPPORT_COLOR_TEMP: 'Alexa.ColorTemperatureController', } ], } @@ -193,11 +197,104 @@ def async_api_turn_off(hass, request, entity): @asyncio.coroutine def async_api_set_brightness(hass, request, entity): """Process a set brightness request.""" - brightness = request[API_PAYLOAD]['brightness'] + brightness = int(request[API_PAYLOAD]['brightness']) yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, { ATTR_ENTITY_ID: entity.entity_id, - light.ATTR_BRIGHTNESS: brightness, + light.ATTR_BRIGHTNESS_PCT: brightness, + }, blocking=True) + + return api_message(request) + + +@HANDLERS.register(('Alexa.BrightnessController', 'AdjustBrightness')) +@extract_entity +@asyncio.coroutine +def async_api_adjust_brightness(hass, request, entity): + """Process a adjust brightness request.""" + brightness_delta = int(request[API_PAYLOAD]['brightnessDelta']) + + # read current state + try: + current = math.floor( + int(entity.attributes.get(light.ATTR_BRIGHTNESS)) / 255 * 100) + except ZeroDivisionError: + return api_error(request, error_type='INVALID_VALUE') + + # set brightness + brightness = brightness_delta + current + yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, { + ATTR_ENTITY_ID: entity.entity_id, + light.ATTR_BRIGHTNESS_PCT: brightness, + }, blocking=True) + + return api_message(request) + + +@HANDLERS.register(('Alexa.ColorController', 'SetColor')) +@extract_entity +@asyncio.coroutine +def async_api_set_color(hass, request, entity): + """Process a set color request.""" + hue = float(request[API_PAYLOAD]['color']['hue']) + saturation = float(request[API_PAYLOAD]['color']['saturation']) + brightness = float(request[API_PAYLOAD]['color']['brightness']) + + rgb = color_util.color_hsb_to_RGB(hue, saturation, brightness) + yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, { + ATTR_ENTITY_ID: entity.entity_id, + light.ATTR_RGB_COLOR: rgb, + }, blocking=True) + + return api_message(request) + + +@HANDLERS.register(('Alexa.ColorTemperatureController', 'SetColorTemperature')) +@extract_entity +@asyncio.coroutine +def async_api_set_color_temperature(hass, request, entity): + """Process a set color temperature request.""" + kelvin = int(request[API_PAYLOAD]['colorTemperatureInKelvin']) + + yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, { + ATTR_ENTITY_ID: entity.entity_id, + light.ATTR_KELVIN: kelvin, + }, blocking=True) + + return api_message(request) + + +@HANDLERS.register( + ('Alexa.ColorTemperatureController', 'DecreaseColorTemperature')) +@extract_entity +@asyncio.coroutine +def async_api_decrease_color_temp(hass, request, entity): + """Process a decrease color temperature request.""" + current = int(entity.attributes.get(light.ATTR_COLOR_TEMP)) + max_mireds = int(entity.attributes.get(light.ATTR_MAX_MIREDS)) + + value = min(max_mireds, current + 50) + yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, { + ATTR_ENTITY_ID: entity.entity_id, + light.ATTR_COLOR_TEMP: value, + }, blocking=True) + + return api_message(request) + + +@HANDLERS.register( + ('Alexa.ColorTemperatureController', 'IncreaseColorTemperature')) +@extract_entity +@asyncio.coroutine +def async_api_increase_color_temp(hass, request, entity): + """Process a increase color temperature request.""" + current = int(entity.attributes.get(light.ATTR_COLOR_TEMP)) + min_mireds = int(entity.attributes.get(light.ATTR_MIN_MIREDS)) + + value = max(min_mireds, current - 50) + yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, { + ATTR_ENTITY_ID: entity.entity_id, + light.ATTR_COLOR_TEMP: value, }, blocking=True) return api_message(request) diff --git a/homeassistant/util/color.py b/homeassistant/util/color.py index 9616774c623..794f6546113 100644 --- a/homeassistant/util/color.py +++ b/homeassistant/util/color.py @@ -257,6 +257,48 @@ def color_xy_brightness_to_RGB(vX: float, vY: float, return (ir, ig, ib) +# pylint: disable=invalid-sequence-index +def color_hsb_to_RGB(fH: float, fS: float, fB: float) -> Tuple[int, int, int]: + """Convert a hsb into its rgb representation.""" + if fS == 0: + fV = fB * 255 + return (fV, fV, fV) + + r = g = b = 0 + h = fH / 60 + f = h - float(math.floor(h)) + p = fB * (1 - fS) + q = fB * (1 - fS * f) + t = fB * (1 - (fS * (1 - f))) + + if int(h) == 0: + r = int(fB * 255) + g = int(t * 255) + b = int(p * 255) + elif int(h) == 1: + r = int(q * 255) + g = int(fB * 255) + b = int(p * 255) + elif int(h) == 2: + r = int(p * 255) + g = int(fB * 255) + b = int(t * 255) + elif int(h) == 3: + r = int(p * 255) + g = int(q * 255) + b = int(fB * 255) + elif int(h) == 4: + r = int(t * 255) + g = int(p * 255) + b = int(fB * 255) + elif int(h) == 5: + r = int(fB * 255) + g = int(p * 255) + b = int(q * 255) + + return (r, g, b) + + # pylint: disable=invalid-sequence-index def color_RGB_to_hsv(iR: int, iG: int, iB: int) -> Tuple[int, int, int]: """Convert an rgb color to its hsv representation.""" @@ -392,9 +434,9 @@ def _get_blue(temperature: float) -> float: def color_temperature_mired_to_kelvin(mired_temperature): """Convert absolute mired shift to degrees kelvin.""" - return 1000000 / mired_temperature + return math.floor(1000000 / mired_temperature) def color_temperature_kelvin_to_mired(kelvin_temperature): """Convert degrees kelvin to mired shift.""" - return 1000000 / kelvin_temperature + return math.floor(1000000 / kelvin_temperature) diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 1c1fcfb7594..3d42add8ae8 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -109,13 +109,17 @@ def test_discovery_request(hass): 'light.test_2', 'on', { 'friendly_name': "Test light 2", 'supported_features': 1 }) + hass.states.async_set( + 'light.test_3', 'on', { + 'friendly_name': "Test light 3", 'supported_features': 19 + }) msg = yield from smart_home.async_handle_message(hass, request) assert 'event' in msg msg = msg['event'] - assert len(msg['payload']['endpoints']) == 3 + assert len(msg['payload']['endpoints']) == 4 assert msg['header']['name'] == 'Discover.Response' assert msg['header']['namespace'] == 'Alexa.Discovery' @@ -150,6 +154,22 @@ def test_discovery_request(hass): continue + if appliance['endpointId'] == 'light#test_3': + assert appliance['displayCategories'][0] == "LIGHT" + assert appliance['friendlyName'] == "Test light 3" + assert len(appliance['capabilities']) == 4 + + caps = set() + for feature in appliance['capabilities']: + caps.add(feature['interface']) + + assert 'Alexa.BrightnessController' in caps + assert 'Alexa.PowerController' in caps + assert 'Alexa.ColorController' in caps + assert 'Alexa.ColorTemperatureController' in caps + + continue + raise AssertionError("Unknown appliance!") @@ -257,5 +277,147 @@ def test_api_set_brightness(hass): assert len(call_light) == 1 assert call_light[0].data['entity_id'] == 'light.test' - assert call_light[0].data['brightness'] == '50' + assert call_light[0].data['brightness_pct'] == 50 + assert msg['header']['name'] == 'Response' + + +@asyncio.coroutine +@pytest.mark.parametrize("result,adjust", [(25, '-5'), (35, '5')]) +def test_api_adjust_brightness(hass, result, adjust): + """Test api adjust brightness process.""" + request = get_new_request( + 'Alexa.BrightnessController', 'AdjustBrightness', 'light#test') + + # add payload + request['directive']['payload']['brightnessDelta'] = adjust + + # settup test devices + hass.states.async_set( + 'light.test', 'off', { + 'friendly_name': "Test light", 'brightness': '77' + }) + + call_light = async_mock_service(hass, 'light', 'turn_on') + + msg = yield from smart_home.async_handle_message(hass, request) + + assert 'event' in msg + msg = msg['event'] + + assert len(call_light) == 1 + assert call_light[0].data['entity_id'] == 'light.test' + assert call_light[0].data['brightness_pct'] == result + assert msg['header']['name'] == 'Response' + + +@asyncio.coroutine +def test_api_set_color(hass): + """Test api set color process.""" + request = get_new_request( + 'Alexa.ColorController', 'SetColor', 'light#test') + + # add payload + request['directive']['payload']['color'] = { + 'hue': '120', + 'saturation': '0.612', + 'brightness': '0.342', + } + + # settup test devices + hass.states.async_set( + 'light.test', 'off', {'friendly_name': "Test light"}) + + call_light = async_mock_service(hass, 'light', 'turn_on') + + msg = yield from smart_home.async_handle_message(hass, request) + + assert 'event' in msg + msg = msg['event'] + + assert len(call_light) == 1 + assert call_light[0].data['entity_id'] == 'light.test' + assert call_light[0].data['rgb_color'] == (33, 87, 33) + assert msg['header']['name'] == 'Response' + + +@asyncio.coroutine +def test_api_set_color_temperature(hass): + """Test api set color temperature process.""" + request = get_new_request( + 'Alexa.ColorTemperatureController', 'SetColorTemperature', + 'light#test') + + # add payload + request['directive']['payload']['colorTemperatureInKelvin'] = '7500' + + # settup test devices + hass.states.async_set( + 'light.test', 'off', {'friendly_name': "Test light"}) + + call_light = async_mock_service(hass, 'light', 'turn_on') + + msg = yield from smart_home.async_handle_message(hass, request) + + assert 'event' in msg + msg = msg['event'] + + assert len(call_light) == 1 + assert call_light[0].data['entity_id'] == 'light.test' + assert call_light[0].data['kelvin'] == 7500 + assert msg['header']['name'] == 'Response' + + +@asyncio.coroutine +@pytest.mark.parametrize("result,initial", [(383, '333'), (500, '500')]) +def test_api_decrease_color_temp(hass, result, initial): + """Test api decrease color temp process.""" + request = get_new_request( + 'Alexa.ColorTemperatureController', 'DecreaseColorTemperature', + 'light#test') + + # settup test devices + hass.states.async_set( + 'light.test', 'off', { + 'friendly_name': "Test light", 'color_temp': initial, + 'max_mireds': 500, + }) + + call_light = async_mock_service(hass, 'light', 'turn_on') + + msg = yield from smart_home.async_handle_message(hass, request) + + assert 'event' in msg + msg = msg['event'] + + assert len(call_light) == 1 + 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("result,initial", [(283, '333'), (142, '142')]) +def test_api_increase_color_temp(hass, result, initial): + """Test api increase color temp process.""" + request = get_new_request( + 'Alexa.ColorTemperatureController', 'IncreaseColorTemperature', + 'light#test') + + # settup test devices + hass.states.async_set( + 'light.test', 'off', { + 'friendly_name': "Test light", 'color_temp': initial, + 'min_mireds': 142, + }) + + call_light = async_mock_service(hass, 'light', 'turn_on') + + msg = yield from smart_home.async_handle_message(hass, request) + + assert 'event' in msg + msg = msg['event'] + + assert len(call_light) == 1 + assert call_light[0].data['entity_id'] == 'light.test' + assert call_light[0].data['color_temp'] == result assert msg['header']['name'] == 'Response' diff --git a/tests/util/test_color.py b/tests/util/test_color.py index dfb2cd0733c..4c14258f2f2 100644 --- a/tests/util/test_color.py +++ b/tests/util/test_color.py @@ -57,7 +57,7 @@ class TestColorUtil(unittest.TestCase): color_util.color_RGB_to_hsv(255, 0, 0)) def test_color_hsv_to_RGB(self): - """Test color_RGB_to_hsv.""" + """Test color_hsv_to_RGB.""" self.assertEqual((0, 0, 0), color_util.color_hsv_to_RGB(0, 0, 0)) @@ -73,6 +73,23 @@ class TestColorUtil(unittest.TestCase): self.assertEqual((255, 0, 0), color_util.color_hsv_to_RGB(0, 255, 255)) + def test_color_hsb_to_RGB(self): + """Test color_hsb_to_RGB.""" + self.assertEqual((0, 0, 0), + color_util.color_hsb_to_RGB(0, 0, 0)) + + self.assertEqual((255, 255, 255), + color_util.color_hsb_to_RGB(0, 0, 1.0)) + + self.assertEqual((0, 0, 255), + color_util.color_hsb_to_RGB(240, 1.0, 1.0)) + + self.assertEqual((0, 255, 0), + color_util.color_hsb_to_RGB(120, 1.0, 1.0)) + + self.assertEqual((255, 0, 0), + color_util.color_hsb_to_RGB(0, 1.0, 1.0)) + def test_color_xy_to_hs(self): """Test color_xy_to_hs.""" self.assertEqual((8609, 255),