Skip to content

Commit

Permalink
Use hue/sat as internal light color interface (home-assistant#11288)
Browse files Browse the repository at this point in the history
* Accept and report both xy and RGB color for lights

* Fix demo light supported_features

* Add new XY color util functions

* Always make color changes available as xy and RGB

* Always expose color as RGB and XY

* Consolidate color supported_features

* Test fixes

* Additional test fix

* Use hue/sat as the hass core color interface

* Tests updates

* Assume MQTT RGB devices need full RGB brightness

* Convert new platforms

* More migration

* Use float for HS API

* Fix backwards conversion for KNX lights

* Adjust limitless min saturation for new scale
  • Loading branch information
emlove authored and balloob committed Mar 18, 2018
1 parent 6b05948 commit 89c7c80
Show file tree
Hide file tree
Showing 57 changed files with 898 additions and 965 deletions.
21 changes: 5 additions & 16 deletions homeassistant/components/alexa/smart_home.py
Original file line number Diff line number Diff line change
Expand Up @@ -438,9 +438,7 @@ def interfaces(self):
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
if supported & light.SUPPORT_BRIGHTNESS:
yield _AlexaBrightnessController(self.entity)
if supported & light.SUPPORT_RGB_COLOR:
yield _AlexaColorController(self.entity)
if supported & light.SUPPORT_XY_COLOR:
if supported & light.SUPPORT_COLOR:
yield _AlexaColorController(self.entity)
if supported & light.SUPPORT_COLOR_TEMP:
yield _AlexaColorTemperatureController(self.entity)
Expand Down Expand Up @@ -842,25 +840,16 @@ def async_api_adjust_brightness(hass, config, request, entity):
@asyncio.coroutine
def async_api_set_color(hass, config, request, entity):
"""Process a set color request."""
supported = entity.attributes.get(ATTR_SUPPORTED_FEATURES)
rgb = color_util.color_hsb_to_RGB(
float(request[API_PAYLOAD]['color']['hue']),
float(request[API_PAYLOAD]['color']['saturation']),
float(request[API_PAYLOAD]['color']['brightness'])
)

if supported & light.SUPPORT_RGB_COLOR > 0:
yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
ATTR_ENTITY_ID: entity.entity_id,
light.ATTR_RGB_COLOR: rgb,
}, blocking=False)
else:
xyz = color_util.color_RGB_to_xy(*rgb)
yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
ATTR_ENTITY_ID: entity.entity_id,
light.ATTR_XY_COLOR: (xyz[0], xyz[1]),
light.ATTR_BRIGHTNESS: xyz[2],
}, blocking=False)
yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
ATTR_ENTITY_ID: entity.entity_id,
light.ATTR_RGB_COLOR: rgb,
}, blocking=False)

return api_message(request)

Expand Down
15 changes: 7 additions & 8 deletions homeassistant/components/google_assistant/trait.py
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ def supported(domain, features):
if domain != light.DOMAIN:
return False

return features & (light.SUPPORT_RGB_COLOR | light.SUPPORT_XY_COLOR)
return features & light.SUPPORT_COLOR

def sync_attributes(self):
"""Return color spectrum attributes for a sync request."""
Expand All @@ -254,13 +254,11 @@ def query_attributes(self):
"""Return color spectrum query attributes."""
response = {}

# No need to handle XY color because light component will always
# convert XY to RGB if possible (which is when brightness is available)
color_rgb = self.state.attributes.get(light.ATTR_RGB_COLOR)
if color_rgb is not None:
color_hs = self.state.attributes.get(light.ATTR_HS_COLOR)
if color_hs is not None:
response['color'] = {
'spectrumRGB': int(color_util.color_rgb_to_hex(
color_rgb[0], color_rgb[1], color_rgb[2]), 16),
*color_util.color_hs_to_RGB(*color_hs)), 16),
}

return response
Expand All @@ -274,11 +272,12 @@ async def execute(self, hass, command, params):
"""Execute a color spectrum command."""
# Convert integer to hex format and left pad with 0's till length 6
hex_value = "{0:06x}".format(params['color']['spectrumRGB'])
color = color_util.rgb_hex_to_rgb_list(hex_value)
color = color_util.color_RGB_to_hs(
*color_util.rgb_hex_to_rgb_list(hex_value))

await hass.services.async_call(light.DOMAIN, SERVICE_TURN_ON, {
ATTR_ENTITY_ID: self.state.entity_id,
light.ATTR_RGB_COLOR: color
light.ATTR_HS_COLOR: color
}, blocking=True)


Expand Down
28 changes: 11 additions & 17 deletions homeassistant/components/homekit/type_lights.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,8 @@
import logging

from homeassistant.components.light import (
ATTR_RGB_COLOR, ATTR_BRIGHTNESS,
SUPPORT_BRIGHTNESS, SUPPORT_RGB_COLOR)
ATTR_HS_COLOR, ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, SUPPORT_COLOR)
from homeassistant.const import ATTR_SUPPORTED_FEATURES, STATE_ON, STATE_OFF
from homeassistant.util.color import color_RGB_to_hsv, color_hsv_to_RGB

from . import TYPES
from .accessories import HomeAccessory, add_preload_service
Expand Down Expand Up @@ -40,7 +38,7 @@ def __init__(self, hass, entity_id, name, *args, **kwargs):
.attributes.get(ATTR_SUPPORTED_FEATURES)
if self._features & SUPPORT_BRIGHTNESS:
self.chars.append(CHAR_BRIGHTNESS)
if self._features & SUPPORT_RGB_COLOR:
if self._features & SUPPORT_COLOR:
self.chars.append(CHAR_HUE)
self.chars.append(CHAR_SATURATION)
self._hue = None
Expand Down Expand Up @@ -102,15 +100,15 @@ def set_hue(self, value):

def set_color(self):
"""Set color if call came from HomeKit."""
# Handle RGB Color
if self._features & SUPPORT_RGB_COLOR and self._flag[CHAR_HUE] and \
# Handle Color
if self._features & SUPPORT_COLOR and self._flag[CHAR_HUE] and \
self._flag[CHAR_SATURATION]:
color = color_hsv_to_RGB(self._hue, self._saturation, 100)
_LOGGER.debug('%s: Set rgb_color to %s', self._entity_id, color)
color = (self._hue, self._saturation)
_LOGGER.debug('%s: Set hs_color to %s', self._entity_id, color)
self._flag.update({
CHAR_HUE: False, CHAR_SATURATION: False, RGB_COLOR: True})
self._hass.components.light.turn_on(
self._entity_id, rgb_color=color)
self._entity_id, hs_color=color)

def update_state(self, entity_id=None, old_state=None, new_state=None):
"""Update light after state change."""
Expand All @@ -134,15 +132,11 @@ def update_state(self, entity_id=None, old_state=None, new_state=None):
should_callback=False)
self._flag[CHAR_BRIGHTNESS] = False

# Handle RGB Color
# Handle Color
if CHAR_SATURATION in self.chars and CHAR_HUE in self.chars:
rgb_color = new_state.attributes.get(ATTR_RGB_COLOR)
current_color = color_hsv_to_RGB(self._hue, self._saturation, 100)\
if self._hue and self._saturation else [None] * 3
if not self._flag[RGB_COLOR] and \
isinstance(rgb_color, (list, tuple)) and \
tuple(rgb_color) != current_color:
hue, saturation, _ = color_RGB_to_hsv(*rgb_color)
hue, saturation = new_state.attributes.get(ATTR_HS_COLOR)
if not self._flag[RGB_COLOR] and (
hue != self._hue or saturation != self._saturation):
self.char_hue.set_value(hue, should_callback=False)
self.char_saturation.set_value(saturation,
should_callback=False)
Expand Down
54 changes: 33 additions & 21 deletions homeassistant/components/light/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,8 @@
SUPPORT_COLOR_TEMP = 2
SUPPORT_EFFECT = 4
SUPPORT_FLASH = 8
SUPPORT_RGB_COLOR = 16
SUPPORT_COLOR = 16
SUPPORT_TRANSITION = 32
SUPPORT_XY_COLOR = 64
SUPPORT_WHITE_VALUE = 128

# Integer that represents transition time in seconds to make change.
Expand All @@ -51,6 +50,7 @@
# Lists holding color values
ATTR_RGB_COLOR = "rgb_color"
ATTR_XY_COLOR = "xy_color"
ATTR_HS_COLOR = "hs_color"
ATTR_COLOR_TEMP = "color_temp"
ATTR_KELVIN = "kelvin"
ATTR_MIN_MIREDS = "min_mireds"
Expand Down Expand Up @@ -86,8 +86,9 @@
PROP_TO_ATTR = {
'brightness': ATTR_BRIGHTNESS,
'color_temp': ATTR_COLOR_TEMP,
'rgb_color': ATTR_RGB_COLOR,
'xy_color': ATTR_XY_COLOR,
'min_mireds': ATTR_MIN_MIREDS,
'max_mireds': ATTR_MAX_MIREDS,
'hs_color': ATTR_HS_COLOR,
'white_value': ATTR_WHITE_VALUE,
'effect_list': ATTR_EFFECT_LIST,
'effect': ATTR_EFFECT,
Expand All @@ -111,6 +112,11 @@
vol.Exclusive(ATTR_XY_COLOR, COLOR_GROUP):
vol.All(vol.ExactSequence((cv.small_float, cv.small_float)),
vol.Coerce(tuple)),
vol.Exclusive(ATTR_HS_COLOR, COLOR_GROUP):
vol.All(vol.ExactSequence(
(vol.All(vol.Coerce(float), vol.Range(min=0, max=360)),
vol.All(vol.Coerce(float), vol.Range(min=0, max=100)))),
vol.Coerce(tuple)),
vol.Exclusive(ATTR_COLOR_TEMP, COLOR_GROUP):
vol.All(vol.Coerce(int), vol.Range(min=1)),
vol.Exclusive(ATTR_KELVIN, COLOR_GROUP):
Expand Down Expand Up @@ -149,22 +155,23 @@ def is_on(hass, entity_id=None):

@bind_hass
def turn_on(hass, entity_id=None, transition=None, brightness=None,
brightness_pct=None, rgb_color=None, xy_color=None,
brightness_pct=None, rgb_color=None, xy_color=None, hs_color=None,
color_temp=None, kelvin=None, white_value=None,
profile=None, flash=None, effect=None, color_name=None):
"""Turn all or specified light on."""
hass.add_job(
async_turn_on, hass, entity_id, transition, brightness, brightness_pct,
rgb_color, xy_color, color_temp, kelvin, white_value,
rgb_color, xy_color, hs_color, color_temp, kelvin, white_value,
profile, flash, effect, color_name)


@callback
@bind_hass
def async_turn_on(hass, entity_id=None, transition=None, brightness=None,
brightness_pct=None, rgb_color=None, xy_color=None,
color_temp=None, kelvin=None, white_value=None,
profile=None, flash=None, effect=None, color_name=None):
hs_color=None, color_temp=None, kelvin=None,
white_value=None, profile=None, flash=None, effect=None,
color_name=None):
"""Turn all or specified light on."""
data = {
key: value for key, value in [
Expand All @@ -175,6 +182,7 @@ def async_turn_on(hass, entity_id=None, transition=None, brightness=None,
(ATTR_BRIGHTNESS_PCT, brightness_pct),
(ATTR_RGB_COLOR, rgb_color),
(ATTR_XY_COLOR, xy_color),
(ATTR_HS_COLOR, hs_color),
(ATTR_COLOR_TEMP, color_temp),
(ATTR_KELVIN, kelvin),
(ATTR_WHITE_VALUE, white_value),
Expand Down Expand Up @@ -254,6 +262,14 @@ def preprocess_turn_on_alternatives(params):
if brightness_pct is not None:
params[ATTR_BRIGHTNESS] = int(255 * brightness_pct/100)

xy_color = params.pop(ATTR_XY_COLOR, None)
if xy_color is not None:
params[ATTR_HS_COLOR] = color_util.color_xy_to_hs(*xy_color)

rgb_color = params.pop(ATTR_RGB_COLOR, None)
if rgb_color is not None:
params[ATTR_HS_COLOR] = color_util.color_RGB_to_hs(*rgb_color)


class SetIntentHandler(intent.IntentHandler):
"""Handle set color intents."""
Expand Down Expand Up @@ -281,7 +297,7 @@ async def async_handle(self, intent_obj):

if 'color' in slots:
intent.async_test_feature(
state, SUPPORT_RGB_COLOR, 'changing colors')
state, SUPPORT_COLOR, 'changing colors')
service_data[ATTR_RGB_COLOR] = slots['color']['value']
# Use original passed in value of the color because we don't have
# human readable names for that internally.
Expand Down Expand Up @@ -428,13 +444,8 @@ def brightness(self):
return None

@property
def xy_color(self):
"""Return the XY color value [float, float]."""
return None

@property
def rgb_color(self):
"""Return the RGB color value [int, int, int]."""
def hs_color(self):
"""Return the hue and saturation color value [float, float]."""
return None

@property
Expand Down Expand Up @@ -484,11 +495,12 @@ def state_attributes(self):
if value is not None:
data[attr] = value

if ATTR_RGB_COLOR not in data and ATTR_XY_COLOR in data and \
ATTR_BRIGHTNESS in data:
data[ATTR_RGB_COLOR] = color_util.color_xy_brightness_to_RGB(
data[ATTR_XY_COLOR][0], data[ATTR_XY_COLOR][1],
data[ATTR_BRIGHTNESS])
# Expose current color also as RGB and XY
if ATTR_HS_COLOR in data:
data[ATTR_RGB_COLOR] = color_util.color_hs_to_RGB(
*data[ATTR_HS_COLOR])
data[ATTR_XY_COLOR] = color_util.color_hs_to_xy(
*data[ATTR_HS_COLOR])

return data

Expand Down
19 changes: 11 additions & 8 deletions homeassistant/components/light/abode.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@

from homeassistant.components.abode import AbodeDevice, DOMAIN as ABODE_DOMAIN
from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_RGB_COLOR,
SUPPORT_BRIGHTNESS, SUPPORT_RGB_COLOR, Light)
ATTR_BRIGHTNESS, ATTR_HS_COLOR,
SUPPORT_BRIGHTNESS, SUPPORT_COLOR, Light)
import homeassistant.util.color as color_util


DEPENDENCIES = ['abode']
Expand Down Expand Up @@ -44,10 +45,12 @@ class AbodeLight(AbodeDevice, Light):

def turn_on(self, **kwargs):
"""Turn on the light."""
if (ATTR_RGB_COLOR in kwargs and
if (ATTR_HS_COLOR in kwargs and
self._device.is_dimmable and self._device.has_color):
self._device.set_color(kwargs[ATTR_RGB_COLOR])
elif ATTR_BRIGHTNESS in kwargs and self._device.is_dimmable:
self._device.set_color(color_util.color_hs_to_RGB(
*kwargs[ATTR_HS_COLOR]))

if ATTR_BRIGHTNESS in kwargs and self._device.is_dimmable:
self._device.set_level(kwargs[ATTR_BRIGHTNESS])
else:
self._device.switch_on()
Expand All @@ -68,16 +71,16 @@ def brightness(self):
return self._device.brightness

@property
def rgb_color(self):
def hs_color(self):
"""Return the color of the light."""
if self._device.is_dimmable and self._device.has_color:
return self._device.color
return color_util.color_RGB_to_hs(*self._device.color)

@property
def supported_features(self):
"""Flag supported features."""
if self._device.is_dimmable and self._device.has_color:
return SUPPORT_BRIGHTNESS | SUPPORT_RGB_COLOR
return SUPPORT_BRIGHTNESS | SUPPORT_COLOR
elif self._device.is_dimmable:
return SUPPORT_BRIGHTNESS

Expand Down
Loading

0 comments on commit 89c7c80

Please sign in to comment.