354 lines
12 KiB
Python
354 lines
12 KiB
Python
"""
|
|
Support for Z-Wave lights.
|
|
|
|
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, ATTR_COLOR_TEMP, \
|
|
ATTR_RGB_COLOR, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, \
|
|
SUPPORT_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, \
|
|
color_rgb_to_rgbw, color_rgbw_to_rgb
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
AEOTEC = 0x86
|
|
AEOTEC_ZW098_LED_BULB = 0x62
|
|
AEOTEC_ZW098_LED_BULB_LIGHT = (AEOTEC, AEOTEC_ZW098_LED_BULB)
|
|
|
|
COLOR_CHANNEL_WARM_WHITE = 0x01
|
|
COLOR_CHANNEL_COLD_WHITE = 0x02
|
|
COLOR_CHANNEL_RED = 0x04
|
|
COLOR_CHANNEL_GREEN = 0x08
|
|
COLOR_CHANNEL_BLUE = 0x10
|
|
|
|
WORKAROUND_ZW098 = 'zw098'
|
|
|
|
DEVICE_MAPPINGS = {
|
|
AEOTEC_ZW098_LED_BULB_LIGHT: WORKAROUND_ZW098
|
|
}
|
|
|
|
# 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
|
|
|
|
SUPPORT_ZWAVE = SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_RGB_COLOR
|
|
|
|
|
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
|
"""Find and add Z-Wave lights."""
|
|
if discovery_info is None or zwave.NETWORK is None:
|
|
return
|
|
node = zwave.NETWORK.nodes[discovery_info[zwave.const.ATTR_NODE_ID]]
|
|
value = node.values[discovery_info[zwave.const.ATTR_VALUE_ID]]
|
|
customize = hass.data['zwave_customize']
|
|
name = '{}.{}'.format(DOMAIN, zwave.object_id(value))
|
|
node_config = customize.get(name, {})
|
|
refresh = node_config.get(zwave.CONF_REFRESH_VALUE)
|
|
delay = node_config.get(zwave.CONF_REFRESH_DELAY)
|
|
_LOGGER.debug('customize=%s name=%s node_config=%s CONF_REFRESH_VALUE=%s'
|
|
' CONF_REFRESH_DELAY=%s', customize, name, node_config,
|
|
refresh, delay)
|
|
if value.command_class != zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL:
|
|
return
|
|
if value.type != zwave.const.TYPE_BYTE:
|
|
return
|
|
if value.genre != zwave.const.GENRE_USER:
|
|
return
|
|
|
|
value.set_change_verified(False)
|
|
|
|
if node.has_command_class(zwave.const.COMMAND_CLASS_SWITCH_COLOR):
|
|
add_devices([ZwaveColorLight(value, refresh, delay)])
|
|
else:
|
|
add_devices([ZwaveDimmer(value, refresh, delay)])
|
|
|
|
|
|
def brightness_state(value):
|
|
"""Return the brightness and state."""
|
|
if value.data > 0:
|
|
return (value.data / 99) * 255, STATE_ON
|
|
else:
|
|
return 0, STATE_OFF
|
|
|
|
|
|
class ZwaveDimmer(zwave.ZWaveDeviceEntity, Light):
|
|
"""Representation of a Z-Wave dimmer."""
|
|
|
|
def __init__(self, value, refresh, delay):
|
|
"""Initialize the light."""
|
|
from openzwave.network import ZWaveNetwork
|
|
from pydispatch import dispatcher
|
|
|
|
zwave.ZWaveDeviceEntity.__init__(self, value, DOMAIN)
|
|
self._brightness = None
|
|
self._state = None
|
|
self._delay = delay
|
|
self._refresh_value = refresh
|
|
self._zw098 = None
|
|
|
|
# Enable appropriate workaround flags for our device
|
|
# Make sure that we have values for the key before converting to int
|
|
if (value.node.manufacturer_id.strip() and
|
|
value.node.product_id.strip()):
|
|
specific_sensor_key = (int(value.node.manufacturer_id, 16),
|
|
int(value.node.product_id, 16))
|
|
if specific_sensor_key in DEVICE_MAPPINGS:
|
|
if DEVICE_MAPPINGS[specific_sensor_key] == WORKAROUND_ZW098:
|
|
_LOGGER.debug("AEOTEC ZW098 workaround enabled")
|
|
self._zw098 = 1
|
|
|
|
self.update_properties()
|
|
|
|
# Used for value change event handling
|
|
self._refreshing = False
|
|
self._timer = None
|
|
_LOGGER.debug('self._refreshing=%s self.delay=%s',
|
|
self._refresh_value, self._delay)
|
|
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 or \
|
|
self._value.node == value.node:
|
|
_LOGGER.debug('Value changed for label %s', self._value.label)
|
|
if self._refresh_value:
|
|
if self._refreshing:
|
|
self._refreshing = False
|
|
self.update_properties()
|
|
else:
|
|
def _refresh_value():
|
|
"""Used timer callback for delayed value refresh."""
|
|
self._refreshing = True
|
|
self._value.refresh()
|
|
|
|
if self._timer is not None and self._timer.isAlive():
|
|
self._timer.cancel()
|
|
|
|
self._timer = Timer(self._delay, _refresh_value)
|
|
self._timer.start()
|
|
self.schedule_update_ha_state()
|
|
else:
|
|
self.update_properties()
|
|
self.schedule_update_ha_state()
|
|
|
|
@property
|
|
def brightness(self):
|
|
"""Return the brightness of this light between 0..255."""
|
|
return self._brightness
|
|
|
|
@property
|
|
def is_on(self):
|
|
"""Return true if device is on."""
|
|
return self._state == STATE_ON
|
|
|
|
@property
|
|
def supported_features(self):
|
|
"""Flag supported features."""
|
|
return SUPPORT_ZWAVE
|
|
|
|
def turn_on(self, **kwargs):
|
|
"""Turn the device on."""
|
|
# Zwave multilevel switches use a range of [0, 99] to control
|
|
# brightness. Level 255 means to set it to previous value.
|
|
if ATTR_BRIGHTNESS in kwargs:
|
|
self._brightness = kwargs[ATTR_BRIGHTNESS]
|
|
brightness = int((self._brightness / 255) * 99)
|
|
else:
|
|
brightness = 255
|
|
|
|
if self._value.node.set_dimmer(self._value.value_id, brightness):
|
|
self._state = STATE_ON
|
|
|
|
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, refresh, delay):
|
|
"""Initialize the light."""
|
|
from openzwave.network import ZWaveNetwork
|
|
from pydispatch import dispatcher
|
|
|
|
self._value_color = None
|
|
self._value_color_channels = None
|
|
self._color_channels = None
|
|
self._rgb = None
|
|
self._ct = None
|
|
|
|
super().__init__(value, refresh, delay)
|
|
|
|
# Create a listener so the color values can be linked to this entity
|
|
dispatcher.connect(
|
|
self._value_added, ZWaveNetwork.SIGNAL_VALUE_ADDED)
|
|
self._get_color_values()
|
|
|
|
def _get_color_values(self):
|
|
"""Search for color values available on this node."""
|
|
from openzwave.network import ZWaveNetwork
|
|
from pydispatch import dispatcher
|
|
|
|
_LOGGER.debug("Searching for zwave color values")
|
|
# Currently zwave nodes only exist with one color element per node.
|
|
if self._value_color is None:
|
|
for value_color in self._value.node.get_rgbbulbs().values():
|
|
self._value_color = value_color
|
|
|
|
if self._value_color_channels is None:
|
|
for value_color_channels in self._value.node.get_values(
|
|
class_id=zwave.const.COMMAND_CLASS_SWITCH_COLOR,
|
|
genre=zwave.const.GENRE_SYSTEM,
|
|
type=zwave.const.TYPE_INT).values():
|
|
self._value_color_channels = value_color_channels
|
|
|
|
if self._value_color and self._value_color_channels:
|
|
_LOGGER.debug("Zwave node color values found.")
|
|
dispatcher.disconnect(
|
|
self._value_added, ZWaveNetwork.SIGNAL_VALUE_ADDED)
|
|
self.update_properties()
|
|
|
|
def _value_added(self, value):
|
|
"""Called when a value has been added to the network."""
|
|
if self._value.node != value.node:
|
|
return
|
|
# Check for the missing color values
|
|
self._get_color_values()
|
|
|
|
def update_properties(self):
|
|
"""Update internal properties based on zwave values."""
|
|
super().update_properties()
|
|
|
|
if self._value_color is None:
|
|
return
|
|
if self._value_color_channels is None:
|
|
return
|
|
|
|
# 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 the AEOTEC ZW098 bulb, only two color
|
|
# temperatures are supported. The warm and cold channel values
|
|
# indicate brightness for warm/cold color temperature.
|
|
if self._zw098:
|
|
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
|
|
|
|
elif self._color_channels & COLOR_CHANNEL_WARM_WHITE:
|
|
self._rgb = list(color_rgbw_to_rgb(*self._rgb, w=warm_white))
|
|
|
|
elif self._color_channels & COLOR_CHANNEL_COLD_WHITE:
|
|
self._rgb = list(color_rgbw_to_rgb(*self._rgb, w=cold_white))
|
|
|
|
# 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:
|
|
# Color temperature. With the AEOTEC ZW098 bulb, only two color
|
|
# temperatures are supported. The warm and cold channel values
|
|
# indicate brightness for warm/cold color temperature.
|
|
if self._zw098:
|
|
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'
|
|
|
|
elif ATTR_RGB_COLOR in kwargs:
|
|
self._rgb = kwargs[ATTR_RGB_COLOR]
|
|
if (not self._zw098 and (
|
|
self._color_channels & COLOR_CHANNEL_WARM_WHITE or
|
|
self._color_channels & COLOR_CHANNEL_COLD_WHITE)):
|
|
rgbw = b'#'
|
|
for colorval in color_rgb_to_rgbw(*self._rgb):
|
|
rgbw += format(colorval, '02x').encode('utf-8')
|
|
rgbw += b'00'
|
|
else:
|
|
rgbw = b'#'
|
|
for colorval in self._rgb:
|
|
rgbw += format(colorval, '02x').encode('utf-8')
|
|
rgbw += b'0000'
|
|
|
|
if rgbw and self._value_color:
|
|
self._value_color.node.set_rgbw(self._value_color.value_id, rgbw)
|
|
|
|
super().turn_on(**kwargs)
|