Alexa SmartHome API extend (#10251)

* Implement adjustment

* Add color support

* fix lint

* Fix lint & use only RGB

* fix HSB + Test

* Add tests & fix bugs

* add rgb test

* add setColorTemperature

* Add color light support + tests

* Fix color temp

* use kelvin for converting

* use correct calculation
This commit is contained in:
Pascal Vizeli 2017-11-01 04:28:17 +01:00 committed by Paulus Schoutsen
parent 5043b85c58
commit 8c266f9266
4 changed files with 326 additions and 8 deletions

View File

@ -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)

View File

@ -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)

View File

@ -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'

View File

@ -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),