mirror of
https://github.com/home-assistant/core.git
synced 2025-04-23 00:37:53 +00:00
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:
parent
5043b85c58
commit
8c266f9266
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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'
|
||||
|
@ -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),
|
||||
|
Loading…
x
Reference in New Issue
Block a user