Skip to content

Commit

Permalink
Initial Support for Zwave color bulbs (#2376)
Browse files Browse the repository at this point in the history
* Initial Support for Zwave color bulbs

* Revert name override for ZwaveColorLight
  • Loading branch information
emlove authored and balloob committed Jun 27, 2016
1 parent 3afc566 commit dc75b28
Show file tree
Hide file tree
Showing 2 changed files with 206 additions and 5 deletions.
210 changes: 205 additions & 5 deletions homeassistant/components/light/zwave.py
Expand Up @@ -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):
Expand All @@ -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):
Expand All @@ -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
Expand All @@ -59,14 +89,19 @@ 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:
return

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."""
Expand Down Expand Up @@ -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)
1 change: 1 addition & 0 deletions homeassistant/components/zwave.py
Expand Up @@ -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
Expand Down

0 comments on commit dc75b28

Please sign in to comment.