From 09693bf16cdd04bf04f0c61519d42428276a6f5c Mon Sep 17 00:00:00 2001 From: Brad Johnson Date: Sun, 17 Apr 2016 20:07:21 -0600 Subject: [PATCH] Upgrading to python-wink 0.7.4 and improving RGB color support in HA (#1832) --- .../components/binary_sensor/wink.py | 2 +- homeassistant/components/garage_door/wink.py | 2 +- homeassistant/components/light/lifx.py | 2 + homeassistant/components/light/wink.py | 50 ++++++++++-- homeassistant/components/lock/wink.py | 2 +- homeassistant/components/sensor/wink.py | 2 +- homeassistant/components/switch/wink.py | 2 +- homeassistant/components/wink.py | 2 +- homeassistant/util/color.py | 77 +++++++++++++++++++ requirements_all.txt | 2 +- tests/util/test_color.py | 67 ++++++++++++++++ 11 files changed, 195 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/binary_sensor/wink.py b/homeassistant/components/binary_sensor/wink.py index c24253ce715..8c8c6736ba8 100644 --- a/homeassistant/components/binary_sensor/wink.py +++ b/homeassistant/components/binary_sensor/wink.py @@ -10,7 +10,7 @@ from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.const import CONF_ACCESS_TOKEN from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['python-wink==0.6.4'] +REQUIREMENTS = ['python-wink==0.7.4'] # These are the available sensors mapped to binary_sensor class SENSOR_TYPES = { diff --git a/homeassistant/components/garage_door/wink.py b/homeassistant/components/garage_door/wink.py index cc3adeaa560..324fc43468c 100644 --- a/homeassistant/components/garage_door/wink.py +++ b/homeassistant/components/garage_door/wink.py @@ -9,7 +9,7 @@ import logging from homeassistant.components.garage_door import GarageDoorDevice from homeassistant.const import CONF_ACCESS_TOKEN -REQUIREMENTS = ['python-wink==0.6.4'] +REQUIREMENTS = ['python-wink==0.7.4'] def setup_platform(hass, config, add_devices, discovery_info=None): diff --git a/homeassistant/components/light/lifx.py b/homeassistant/components/light/lifx.py index 257bfc9e408..a4d543d38c8 100644 --- a/homeassistant/components/light/lifx.py +++ b/homeassistant/components/light/lifx.py @@ -209,6 +209,8 @@ class LIFXLight(Light): brightness = self._bri if ATTR_COLOR_TEMP in kwargs: + # pylint: disable=fixme + # TODO: Use color_temperature_mired_to_kelvin from util.color kelvin = int(((TEMP_MAX - TEMP_MIN) * (kwargs[ATTR_COLOR_TEMP] - TEMP_MIN_HASS) / (TEMP_MAX_HASS - TEMP_MIN_HASS)) + TEMP_MIN) diff --git a/homeassistant/components/light/wink.py b/homeassistant/components/light/wink.py index d24c01c34c3..b2a623a1599 100644 --- a/homeassistant/components/light/wink.py +++ b/homeassistant/components/light/wink.py @@ -6,10 +6,14 @@ https://home-assistant.io/components/light.wink/ """ import logging -from homeassistant.components.light import ATTR_BRIGHTNESS, Light +from homeassistant.components.light import ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, \ + Light, ATTR_RGB_COLOR from homeassistant.const import CONF_ACCESS_TOKEN +from homeassistant.util import color as color_util +from homeassistant.util.color import \ + color_temperature_mired_to_kelvin as mired_to_kelvin -REQUIREMENTS = ['python-wink==0.6.4'] +REQUIREMENTS = ['python-wink==0.7.4'] def setup_platform(hass, config, add_devices_callback, discovery_info=None): @@ -35,7 +39,11 @@ class WinkLight(Light): """Representation of a Wink light.""" def __init__(self, wink): - """Initialize the light.""" + """ + Initialize the light. + + :type wink: pywink.devices.standard.bulb.WinkBulb + """ self.wink = wink @property @@ -63,15 +71,41 @@ class WinkLight(Light): """True if connection == True.""" return self.wink.available + @property + def xy_color(self): + """Current bulb color in CIE 1931 (XY) color space.""" + if not self.wink.supports_xy_color(): + return None + return self.wink.color_xy() + + @property + def color_temp(self): + """Current bulb color in degrees Kelvin.""" + if not self.wink.supports_temperature(): + return None + return color_util.color_temperature_kelvin_to_mired( + self.wink.color_temperature_kelvin()) + # pylint: disable=too-few-public-methods def turn_on(self, **kwargs): """Turn the switch on.""" brightness = kwargs.get(ATTR_BRIGHTNESS) + rgb_color = kwargs.get(ATTR_RGB_COLOR) + color_temp_mired = kwargs.get(ATTR_COLOR_TEMP) - if brightness is not None: - self.wink.set_state(True, brightness=brightness / 255) - else: - self.wink.set_state(True) + state_kwargs = { + } + + if rgb_color: + state_kwargs['color_xy'] = color_util.color_RGB_to_xy(*rgb_color) + + if color_temp_mired: + state_kwargs['color_kelvin'] = mired_to_kelvin(color_temp_mired) + + if brightness: + state_kwargs['brightness'] = brightness / 255.0 + + self.wink.set_state(True, **state_kwargs) def turn_off(self): """Turn the switch off.""" @@ -79,4 +113,4 @@ class WinkLight(Light): def update(self): """Update state of the light.""" - self.wink.update_state() + self.wink.update_state(require_desired_state_fulfilled=True) diff --git a/homeassistant/components/lock/wink.py b/homeassistant/components/lock/wink.py index 1ccd787b6f6..b843e841a29 100644 --- a/homeassistant/components/lock/wink.py +++ b/homeassistant/components/lock/wink.py @@ -9,7 +9,7 @@ import logging from homeassistant.components.lock import LockDevice from homeassistant.const import CONF_ACCESS_TOKEN -REQUIREMENTS = ['python-wink==0.6.4'] +REQUIREMENTS = ['python-wink==0.7.4'] def setup_platform(hass, config, add_devices, discovery_info=None): diff --git a/homeassistant/components/sensor/wink.py b/homeassistant/components/sensor/wink.py index a68642a94eb..109381d49fc 100644 --- a/homeassistant/components/sensor/wink.py +++ b/homeassistant/components/sensor/wink.py @@ -10,7 +10,7 @@ from homeassistant.const import (CONF_ACCESS_TOKEN, STATE_CLOSED, STATE_OPEN, TEMP_CELCIUS) from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['python-wink==0.6.4'] +REQUIREMENTS = ['python-wink==0.7.4'] SENSOR_TYPES = ['temperature', 'humidity'] diff --git a/homeassistant/components/switch/wink.py b/homeassistant/components/switch/wink.py index 76ed638f0b3..b414459eb89 100644 --- a/homeassistant/components/switch/wink.py +++ b/homeassistant/components/switch/wink.py @@ -9,7 +9,7 @@ import logging from homeassistant.components.wink import WinkToggleDevice from homeassistant.const import CONF_ACCESS_TOKEN -REQUIREMENTS = ['python-wink==0.6.4'] +REQUIREMENTS = ['python-wink==0.7.4'] def setup_platform(hass, config, add_devices, discovery_info=None): diff --git a/homeassistant/components/wink.py b/homeassistant/components/wink.py index 987dbfaf825..cdf97dec8c7 100644 --- a/homeassistant/components/wink.py +++ b/homeassistant/components/wink.py @@ -15,7 +15,7 @@ from homeassistant.helpers.entity import ToggleEntity from homeassistant.loader import get_component DOMAIN = "wink" -REQUIREMENTS = ['python-wink==0.6.4'] +REQUIREMENTS = ['python-wink==0.7.4'] DISCOVER_LIGHTS = "wink.lights" DISCOVER_SWITCHES = "wink.switches" diff --git a/homeassistant/util/color.py b/homeassistant/util/color.py index 1806d9058dc..2a662b15f5e 100644 --- a/homeassistant/util/color.py +++ b/homeassistant/util/color.py @@ -1,4 +1,8 @@ """Color util methods.""" +import math + +HASS_COLOR_MAX = 500 # mireds (inverted) +HASS_COLOR_MIN = 154 # Taken from: http://www.cse.unr.edu/~quiroz/inc/colortransforms.py @@ -90,3 +94,76 @@ def rgb_hex_to_rgb_list(hex_string): for i in range(0, len(hex_string), len(hex_string) // 3)] + + +def color_temperature_to_rgb(color_temperature_kelvin): + """ + Return an RGB color from a color temperature in Kelvin. + + This is a rough approximation based on the formula provided by T. Helland + http://www.tannerhelland.com/4435/convert-temperature-rgb-algorithm-code/ + """ + # range check + if color_temperature_kelvin < 1000: + color_temperature_kelvin = 1000 + elif color_temperature_kelvin > 40000: + color_temperature_kelvin = 40000 + + tmp_internal = color_temperature_kelvin / 100.0 + + red = _get_red(tmp_internal) + + green = _get_green(tmp_internal) + + blue = _get_blue(tmp_internal) + + return (red, green, blue) + + +def _bound(color_component, minimum=0, maximum=255): + """ + Bound the given color component value between the given min and max values. + + The minimum and maximum values will be included in the valid output. + i.e. Given a color_component of 0 and a minimum of 10, the returned value + will be 10. + """ + color_component_out = max(color_component, minimum) + return min(color_component_out, maximum) + + +def _get_red(temperature): + """Get the red component of the temperature in RGB space.""" + if temperature <= 66: + return 255 + tmp_red = 329.698727446 * math.pow(temperature - 60, -0.1332047592) + return _bound(tmp_red) + + +def _get_green(temperature): + """Get the green component of the given color temp in RGB space.""" + if temperature <= 66: + green = 99.4708025861 * math.log(temperature) - 161.1195681661 + else: + green = 288.1221695283 * math.pow(temperature - 60, -0.0755148492) + return _bound(green) + + +def _get_blue(tmp_internal): + """Get the blue component of the given color temperature in RGB space.""" + if tmp_internal >= 66: + return 255 + if tmp_internal <= 19: + return 0 + blue = 138.5177312231 * math.log(tmp_internal - 10) - 305.0447927307 + return _bound(blue) + + +def color_temperature_mired_to_kelvin(mired_temperature): + """Convert absolute mired shift to degrees kelvin.""" + return 1000000 / mired_temperature + + +def color_temperature_kelvin_to_mired(kelvin_temperature): + """Convert degrees kelvin to mired shift.""" + return 1000000 / kelvin_temperature diff --git a/requirements_all.txt b/requirements_all.txt index 81d94cf7a6c..10549b3d715 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -242,7 +242,7 @@ python-twitch==1.2.0 # homeassistant.components.lock.wink # homeassistant.components.sensor.wink # homeassistant.components.switch.wink -python-wink==0.6.4 +python-wink==0.7.4 # homeassistant.components.keyboard pyuserinput==0.1.9 diff --git a/tests/util/test_color.py b/tests/util/test_color.py index c5a4d40c81a..06d413778cf 100644 --- a/tests/util/test_color.py +++ b/tests/util/test_color.py @@ -57,3 +57,70 @@ class TestColorUtil(unittest.TestCase): self.assertEqual([51, 153, 255, 0], color_util.rgb_hex_to_rgb_list('3399ff00')) + + +class ColorTemperatureMiredToKelvinTests(unittest.TestCase): + """Test color_temperature_mired_to_kelvin.""" + + def test_should_return_25000_kelvin_when_input_is_40_mired(self): + """Function should return 25000K if given 40 mired.""" + kelvin = color_util.color_temperature_mired_to_kelvin(40) + self.assertEqual(25000, kelvin) + + def test_should_return_5000_kelvin_when_input_is_200_mired(self): + """Function should return 5000K if given 200 mired.""" + kelvin = color_util.color_temperature_mired_to_kelvin(200) + self.assertEqual(5000, kelvin) + + +class ColorTemperatureKelvinToMiredTests(unittest.TestCase): + """Test color_temperature_kelvin_to_mired.""" + + def test_should_return_40_mired_when_input_is_25000_kelvin(self): + """Function should return 40 mired when given 25000 Kelvin.""" + mired = color_util.color_temperature_kelvin_to_mired(25000) + self.assertEqual(40, mired) + + def test_should_return_200_mired_when_input_is_5000_kelvin(self): + """Function should return 200 mired when given 5000 Kelvin.""" + mired = color_util.color_temperature_kelvin_to_mired(5000) + self.assertEqual(200, mired) + + +class ColorTemperatureToRGB(unittest.TestCase): + """Test color_temperature_to_rgb.""" + + def test_returns_same_value_for_any_two_temperatures_below_1000(self): + """Function should return same value for 999 Kelvin and 0 Kelvin.""" + rgb_1 = color_util.color_temperature_to_rgb(999) + rgb_2 = color_util.color_temperature_to_rgb(0) + self.assertEqual(rgb_1, rgb_2) + + def test_returns_same_value_for_any_two_temperatures_above_40000(self): + """Function should return same value for 40001K and 999999K.""" + rgb_1 = color_util.color_temperature_to_rgb(40001) + rgb_2 = color_util.color_temperature_to_rgb(999999) + self.assertEqual(rgb_1, rgb_2) + + def test_should_return_pure_white_at_6600(self): + """ + Function should return red=255, blue=255, green=255 when given 6600K. + + 6600K is considered "pure white" light. + This is just a rough estimate because the formula itself is a "best + guess" approach. + """ + rgb = color_util.color_temperature_to_rgb(6600) + self.assertEqual((255, 255, 255), rgb) + + def test_color_above_6600_should_have_more_blue_than_red_or_green(self): + """Function should return a higher blue value for blue-ish light.""" + rgb = color_util.color_temperature_to_rgb(6700) + self.assertGreater(rgb[2], rgb[1]) + self.assertGreater(rgb[2], rgb[0]) + + def test_color_below_6600_should_have_more_red_than_blue_or_green(self): + """Function should return a higher red value for red-ish light.""" + rgb = color_util.color_temperature_to_rgb(6500) + self.assertGreater(rgb[0], rgb[1]) + self.assertGreater(rgb[0], rgb[2])