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.""" """Support for alexa Smart Home Skill API."""
import asyncio import asyncio
import logging import logging
import math
from uuid import uuid4 from uuid import uuid4
from homeassistant.const import ( from homeassistant.const import (
ATTR_SUPPORTED_FEATURES, ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF) ATTR_SUPPORTED_FEATURES, ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF)
from homeassistant.components import switch, light from homeassistant.components import switch, light
import homeassistant.util.color as color_util
from homeassistant.util.decorator import Registry from homeassistant.util.decorator import Registry
HANDLERS = Registry() HANDLERS = Registry()
@ -22,7 +24,9 @@ MAPPING_COMPONENT = {
switch.DOMAIN: ['SWITCH', ('Alexa.PowerController',), None], switch.DOMAIN: ['SWITCH', ('Alexa.PowerController',), None],
light.DOMAIN: [ light.DOMAIN: [
'LIGHT', ('Alexa.PowerController',), { '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 @asyncio.coroutine
def async_api_set_brightness(hass, request, entity): def async_api_set_brightness(hass, request, entity):
"""Process a set brightness request.""" """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, { yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
ATTR_ENTITY_ID: entity.entity_id, 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) }, blocking=True)
return api_message(request) return api_message(request)

View file

@ -257,6 +257,48 @@ def color_xy_brightness_to_RGB(vX: float, vY: float,
return (ir, ig, ib) 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 # pylint: disable=invalid-sequence-index
def color_RGB_to_hsv(iR: int, iG: int, iB: int) -> Tuple[int, int, int]: def color_RGB_to_hsv(iR: int, iG: int, iB: int) -> Tuple[int, int, int]:
"""Convert an rgb color to its hsv representation.""" """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): def color_temperature_mired_to_kelvin(mired_temperature):
"""Convert absolute mired shift to degrees kelvin.""" """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): def color_temperature_kelvin_to_mired(kelvin_temperature):
"""Convert degrees kelvin to mired shift.""" """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', { 'light.test_2', 'on', {
'friendly_name': "Test light 2", 'supported_features': 1 '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) msg = yield from smart_home.async_handle_message(hass, request)
assert 'event' in msg assert 'event' in msg
msg = msg['event'] msg = msg['event']
assert len(msg['payload']['endpoints']) == 3 assert len(msg['payload']['endpoints']) == 4
assert msg['header']['name'] == 'Discover.Response' assert msg['header']['name'] == 'Discover.Response'
assert msg['header']['namespace'] == 'Alexa.Discovery' assert msg['header']['namespace'] == 'Alexa.Discovery'
@ -150,6 +154,22 @@ def test_discovery_request(hass):
continue 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!") raise AssertionError("Unknown appliance!")
@ -257,5 +277,147 @@ def test_api_set_brightness(hass):
assert len(call_light) == 1 assert len(call_light) == 1
assert call_light[0].data['entity_id'] == 'light.test' 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' assert msg['header']['name'] == 'Response'

View file

@ -57,7 +57,7 @@ class TestColorUtil(unittest.TestCase):
color_util.color_RGB_to_hsv(255, 0, 0)) color_util.color_RGB_to_hsv(255, 0, 0))
def test_color_hsv_to_RGB(self): def test_color_hsv_to_RGB(self):
"""Test color_RGB_to_hsv.""" """Test color_hsv_to_RGB."""
self.assertEqual((0, 0, 0), self.assertEqual((0, 0, 0),
color_util.color_hsv_to_RGB(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), self.assertEqual((255, 0, 0),
color_util.color_hsv_to_RGB(0, 255, 255)) 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): def test_color_xy_to_hs(self):
"""Test color_xy_to_hs.""" """Test color_xy_to_hs."""
self.assertEqual((8609, 255), self.assertEqual((8609, 255),