From dc75b28b90c1c5d5b1decf58d7121bd8226be6aa Mon Sep 17 00:00:00 2001 From: Adam Mills Date: Mon, 27 Jun 2016 12:01:41 -0400 Subject: [PATCH] Initial Support for Zwave color bulbs (#2376) * Initial Support for Zwave color bulbs * Revert name override for ZwaveColorLight --- homeassistant/components/light/zwave.py | 210 +++++++++++++++++++++++- homeassistant/components/zwave.py | 1 + 2 files changed, 206 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/light/zwave.py b/homeassistant/components/light/zwave.py index b4aaf5e2b4f25b..7c9cb72db26e56 100644 --- a/homeassistant/components/light/zwave.py +++ b/homeassistant/components/light/zwave.py @@ -4,12 +4,31 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/light.zwave/ """ +import logging + # Because we do not compile openzwave on CI # pylint: disable=import-error from threading import Timer -from homeassistant.components.light import ATTR_BRIGHTNESS, DOMAIN, Light +from homeassistant.components.light import ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, \ + ATTR_RGB_COLOR, DOMAIN, Light from homeassistant.components import zwave from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.util.color import HASS_COLOR_MAX, HASS_COLOR_MIN, \ + color_temperature_mired_to_kelvin, color_temperature_to_rgb + +_LOGGER = logging.getLogger(__name__) + +COLOR_CHANNEL_WARM_WHITE = 0x01 +COLOR_CHANNEL_COLD_WHITE = 0x02 +COLOR_CHANNEL_RED = 0x04 +COLOR_CHANNEL_GREEN = 0x08 +COLOR_CHANNEL_BLUE = 0x10 + +# Generate midpoint color temperatures for bulbs that have limited +# support for white light colors +TEMP_MID_HASS = (HASS_COLOR_MAX - HASS_COLOR_MIN) / 2 + HASS_COLOR_MIN +TEMP_WARM_HASS = (HASS_COLOR_MAX - HASS_COLOR_MIN) / 3 * 2 + HASS_COLOR_MIN +TEMP_COLD_HASS = (HASS_COLOR_MAX - HASS_COLOR_MIN) / 3 + HASS_COLOR_MIN def setup_platform(hass, config, add_devices, discovery_info=None): @@ -28,7 +47,17 @@ def setup_platform(hass, config, add_devices, discovery_info=None): return value.set_change_verified(False) - add_devices([ZwaveDimmer(value)]) + + if node.has_command_class(zwave.COMMAND_CLASS_COLOR): + try: + add_devices([ZwaveColorLight(value)]) + except ValueError as exception: + _LOGGER.warning( + "Error initializing as color bulb: %s " + "Initializing as standard dimmer.", exception) + add_devices([ZwaveDimmer(value)]) + else: + add_devices([ZwaveDimmer(value)]) def brightness_state(value): @@ -49,8 +78,9 @@ def __init__(self, value): from pydispatch import dispatcher zwave.ZWaveDeviceEntity.__init__(self, value, DOMAIN) - - self._brightness, self._state = brightness_state(value) + self._brightness = None + self._state = None + self.update_properties() # Used for value change event handling self._refreshing = False @@ -59,6 +89,11 @@ def __init__(self, value): dispatcher.connect( self._value_changed, ZWaveNetwork.SIGNAL_VALUE_CHANGED) + def update_properties(self): + """Update internal properties based on zwave values.""" + # Brightness + self._brightness, self._state = brightness_state(self._value) + def _value_changed(self, value): """Called when a value has changed on the network.""" if self._value.value_id != value.value_id: @@ -66,7 +101,7 @@ def _value_changed(self, value): if self._refreshing: self._refreshing = False - self._brightness, self._state = brightness_state(value) + self.update_properties() else: def _refresh_value(): """Used timer callback for delayed value refresh.""" @@ -107,3 +142,168 @@ def turn_off(self, **kwargs): """Turn the device off.""" if self._value.node.set_dimmer(self._value.value_id, 0): self._state = STATE_OFF + + +def ct_to_rgb(temp): + """Convert color temperature (mireds) to RGB.""" + colorlist = list( + color_temperature_to_rgb(color_temperature_mired_to_kelvin(temp))) + return [int(val) for val in colorlist] + + +class ZwaveColorLight(ZwaveDimmer): + """Representation of a Z-Wave color changing light.""" + + def __init__(self, value): + """Initialize the light.""" + self._value_color = None + self._value_color_channels = None + self._color_channels = None + self._rgb = None + self._ct = None + + # Here we attempt to find a zwave color value with the same instance + # id as the dimmer value. Currently zwave nodes that change colors + # only include one dimmer and one color command, but this will + # hopefully provide some forward compatibility for new devices that + # have multiple color changing elements. + for value_color in value.node.get_rgbbulbs().values(): + if value.instance == value_color.instance: + self._value_color = value_color + + if self._value_color is None: + raise ValueError("No matching color command found.") + + for value_color_channels in value.node.get_values( + class_id=zwave.COMMAND_CLASS_COLOR, genre='System', + type="Int").values(): + self._value_color_channels = value_color_channels + + if self._value_color_channels is None: + raise ValueError("Color Channels not found.") + + super().__init__(value) + + def update_properties(self): + """Update internal properties based on zwave values.""" + super().update_properties() + + # Color Channels + self._color_channels = self._value_color_channels.data + + # Color Data String + data = self._value_color.data + + # RGB is always present in the openzwave color data string. + self._rgb = [ + int(data[1:3], 16), + int(data[3:5], 16), + int(data[5:7], 16)] + + # Parse remaining color channels. Openzwave appends white channels + # that are present. + index = 7 + + # Warm white + if self._color_channels & COLOR_CHANNEL_WARM_WHITE: + warm_white = int(data[index:index+2], 16) + index += 2 + else: + warm_white = 0 + + # Cold white + if self._color_channels & COLOR_CHANNEL_COLD_WHITE: + cold_white = int(data[index:index+2], 16) + index += 2 + else: + cold_white = 0 + + # Color temperature. With two white channels, only two color + # temperatures are supported for the bulb. The channel values + # indicate brightness for warm/cold color temperature. + if (self._color_channels & COLOR_CHANNEL_WARM_WHITE and + self._color_channels & COLOR_CHANNEL_COLD_WHITE): + if warm_white > 0: + self._ct = TEMP_WARM_HASS + self._rgb = ct_to_rgb(self._ct) + elif cold_white > 0: + self._ct = TEMP_COLD_HASS + self._rgb = ct_to_rgb(self._ct) + else: + # RGB color is being used. Just report midpoint. + self._ct = TEMP_MID_HASS + + # If only warm white is reported 0-255 is color temperature. + elif self._color_channels & COLOR_CHANNEL_WARM_WHITE: + self._ct = HASS_COLOR_MIN + (HASS_COLOR_MAX - HASS_COLOR_MIN) * ( + warm_white / 255) + self._rgb = ct_to_rgb(self._ct) + + # If only cold white is reported 0-255 is negative color temperature. + elif self._color_channels & COLOR_CHANNEL_COLD_WHITE: + self._ct = HASS_COLOR_MIN + (HASS_COLOR_MAX - HASS_COLOR_MIN) * ( + (255 - cold_white) / 255) + self._rgb = ct_to_rgb(self._ct) + + # If no rgb channels supported, report None. + if not (self._color_channels & COLOR_CHANNEL_RED or + self._color_channels & COLOR_CHANNEL_GREEN or + self._color_channels & COLOR_CHANNEL_BLUE): + self._rgb = None + + @property + def rgb_color(self): + """Return the rgb color.""" + return self._rgb + + @property + def color_temp(self): + """Return the color temperature.""" + return self._ct + + def turn_on(self, **kwargs): + """Turn the device on.""" + rgbw = None + + if ATTR_COLOR_TEMP in kwargs: + # With two white channels, only two color temperatures are + # supported for the bulb. + if (self._color_channels & COLOR_CHANNEL_WARM_WHITE and + self._color_channels & COLOR_CHANNEL_COLD_WHITE): + if kwargs[ATTR_COLOR_TEMP] > TEMP_MID_HASS: + self._ct = TEMP_WARM_HASS + rgbw = b'#000000FF00' + else: + self._ct = TEMP_COLD_HASS + rgbw = b'#00000000FF' + + # If only warm white is reported 0-255 is color temperature + elif self._color_channels & COLOR_CHANNEL_WARM_WHITE: + rgbw = b'#000000' + temp = ( + (kwargs[ATTR_COLOR_TEMP] - HASS_COLOR_MIN) / + (HASS_COLOR_MAX - HASS_COLOR_MIN) * 255) + rgbw += format(int(temp)).encode('utf-8') + + # If only cold white is reported 0-255 is negative color temp + elif self._color_channels & COLOR_CHANNEL_COLD_WHITE: + rgbw = b'#000000' + temp = ( + 255 - (kwargs[ATTR_COLOR_TEMP] - HASS_COLOR_MIN) / + (HASS_COLOR_MAX - HASS_COLOR_MIN) * 255) + rgbw += format(int(temp)).encode('utf-8') + + elif ATTR_RGB_COLOR in kwargs: + self._rgb = kwargs[ATTR_RGB_COLOR] + + rgbw = b'#' + for colorval in self._rgb: + rgbw += format(colorval, '02x').encode('utf-8') + rgbw += b'0000' + + if rgbw is None: + _LOGGER.warning("rgbw string was not generated for turn_on") + else: + self._value_color.node.set_rgbw(self._value_color.value_id, rgbw) + + super().turn_on(**kwargs) diff --git a/homeassistant/components/zwave.py b/homeassistant/components/zwave.py index 84ec3dfd8470c6..f8959f330330b5 100644 --- a/homeassistant/components/zwave.py +++ b/homeassistant/components/zwave.py @@ -41,6 +41,7 @@ COMMAND_CLASS_WHATEVER = None COMMAND_CLASS_SENSOR_MULTILEVEL = 49 +COMMAND_CLASS_COLOR = 51 COMMAND_CLASS_METER = 50 COMMAND_CLASS_ALARM = 113 COMMAND_CLASS_SWITCH_BINARY = 37