312 lines
10 KiB
Python
312 lines
10 KiB
Python
"""Support for Z-Wave lights."""
|
|
import logging
|
|
|
|
from homeassistant.components.light import (
|
|
ATTR_BRIGHTNESS,
|
|
ATTR_COLOR_TEMP,
|
|
ATTR_HS_COLOR,
|
|
ATTR_TRANSITION,
|
|
ATTR_WHITE_VALUE,
|
|
DOMAIN as LIGHT_DOMAIN,
|
|
SUPPORT_BRIGHTNESS,
|
|
SUPPORT_COLOR,
|
|
SUPPORT_COLOR_TEMP,
|
|
SUPPORT_TRANSITION,
|
|
SUPPORT_WHITE_VALUE,
|
|
LightEntity,
|
|
)
|
|
from homeassistant.core import callback
|
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
|
import homeassistant.util.color as color_util
|
|
|
|
from .const import DATA_UNSUBSCRIBE, DOMAIN
|
|
from .entity import ZWaveDeviceEntity
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
ATTR_VALUE = "Value"
|
|
COLOR_CHANNEL_WARM_WHITE = 0x01
|
|
COLOR_CHANNEL_COLD_WHITE = 0x02
|
|
COLOR_CHANNEL_RED = 0x04
|
|
COLOR_CHANNEL_GREEN = 0x08
|
|
COLOR_CHANNEL_BLUE = 0x10
|
|
|
|
|
|
async def async_setup_entry(hass, config_entry, async_add_entities):
|
|
"""Set up Z-Wave Light from Config Entry."""
|
|
|
|
@callback
|
|
def async_add_light(values):
|
|
"""Add Z-Wave Light."""
|
|
light = ZwaveLight(values)
|
|
|
|
async_add_entities([light])
|
|
|
|
hass.data[DOMAIN][config_entry.entry_id][DATA_UNSUBSCRIBE].append(
|
|
async_dispatcher_connect(hass, f"{DOMAIN}_new_{LIGHT_DOMAIN}", async_add_light)
|
|
)
|
|
|
|
|
|
def byte_to_zwave_brightness(value):
|
|
"""Convert brightness in 0-255 scale to 0-99 scale.
|
|
|
|
`value` -- (int) Brightness byte value from 0-255.
|
|
"""
|
|
if value > 0:
|
|
return max(1, round((value / 255) * 99))
|
|
return 0
|
|
|
|
|
|
class ZwaveLight(ZWaveDeviceEntity, LightEntity):
|
|
"""Representation of a Z-Wave light."""
|
|
|
|
def __init__(self, values):
|
|
"""Initialize the light."""
|
|
super().__init__(values)
|
|
self._color_channels = None
|
|
self._hs = None
|
|
self._white = None
|
|
self._ct = None
|
|
self._supported_features = SUPPORT_BRIGHTNESS
|
|
self._min_mireds = 153 # 6500K as a safe default
|
|
self._max_mireds = 370 # 2700K as a safe default
|
|
|
|
# make sure that supported features is correctly set
|
|
self.on_value_update()
|
|
|
|
@callback
|
|
def on_value_update(self):
|
|
"""Call when the underlying value(s) is added or updated."""
|
|
if self.values.dimming_duration is not None:
|
|
self._supported_features |= SUPPORT_TRANSITION
|
|
|
|
if self.values.color is not None:
|
|
self._supported_features |= SUPPORT_COLOR
|
|
|
|
if self.values.color_channels is not None:
|
|
# Support Color Temp if both white channels
|
|
if (self.values.color_channels.value & COLOR_CHANNEL_WARM_WHITE) and (
|
|
self.values.color_channels.value & COLOR_CHANNEL_COLD_WHITE
|
|
):
|
|
self._supported_features |= SUPPORT_COLOR_TEMP
|
|
|
|
# Support White value if only a single white channel
|
|
if ((self.values.color_channels.value & COLOR_CHANNEL_WARM_WHITE) != 0) ^ (
|
|
(self.values.color_channels.value & COLOR_CHANNEL_COLD_WHITE) != 0
|
|
):
|
|
self._supported_features |= SUPPORT_WHITE_VALUE
|
|
|
|
if self.values.color is not None:
|
|
self._calculate_color_values()
|
|
|
|
@property
|
|
def brightness(self):
|
|
"""Return the brightness of this light between 0..255.
|
|
|
|
Zwave multilevel switches use a range of [0, 99] to control brightness.
|
|
"""
|
|
if "target" in self.values:
|
|
return round((self.values.target.value / 99) * 255)
|
|
return round((self.values.primary.value / 99) * 255)
|
|
|
|
@property
|
|
def is_on(self):
|
|
"""Return true if device is on (brightness above 0)."""
|
|
if "target" in self.values:
|
|
return self.values.target.value > 0
|
|
return self.values.primary.value > 0
|
|
|
|
@property
|
|
def supported_features(self):
|
|
"""Flag supported features."""
|
|
return self._supported_features
|
|
|
|
@property
|
|
def hs_color(self):
|
|
"""Return the hs color."""
|
|
return self._hs
|
|
|
|
@property
|
|
def white_value(self):
|
|
"""Return the white value of this light between 0..255."""
|
|
return self._white
|
|
|
|
@property
|
|
def color_temp(self):
|
|
"""Return the color temperature."""
|
|
return self._ct
|
|
|
|
@property
|
|
def min_mireds(self):
|
|
"""Return the coldest color_temp that this light supports."""
|
|
return self._min_mireds
|
|
|
|
@property
|
|
def max_mireds(self):
|
|
"""Return the warmest color_temp that this light supports."""
|
|
return self._max_mireds
|
|
|
|
@callback
|
|
def async_set_duration(self, **kwargs):
|
|
"""Set the transition time for the brightness value.
|
|
|
|
Zwave Dimming Duration values now use seconds as an
|
|
integer (max: 7620 seconds or 127 mins)
|
|
Build 1205 https://github.com/OpenZWave/open-zwave/commit/f81bc04
|
|
"""
|
|
if self.values.dimming_duration is None:
|
|
return
|
|
|
|
ozw_version = tuple(
|
|
int(x)
|
|
for x in self.values.primary.ozw_instance.get_status().openzwave_version.split(
|
|
"."
|
|
)
|
|
)
|
|
|
|
if ATTR_TRANSITION not in kwargs:
|
|
# no transition specified by user, use defaults
|
|
new_value = 7621 # anything over 7620 uses the factory default
|
|
if ozw_version < (1, 6, 1205):
|
|
new_value = 255 # default for older version
|
|
|
|
else:
|
|
# transition specified by user
|
|
new_value = int(max(0, min(7620, kwargs[ATTR_TRANSITION])))
|
|
if ozw_version < (1, 6, 1205):
|
|
transition = kwargs[ATTR_TRANSITION]
|
|
if transition <= 127:
|
|
new_value = int(transition)
|
|
else:
|
|
minutes = int(transition / 60)
|
|
_LOGGER.debug(
|
|
"Transition rounded to %d minutes for %s",
|
|
minutes,
|
|
self.entity_id,
|
|
)
|
|
new_value = minutes + 128
|
|
|
|
# only send value if it differs from current
|
|
# this prevents a command for nothing
|
|
if self.values.dimming_duration.value != new_value:
|
|
self.values.dimming_duration.send_value(new_value)
|
|
|
|
async def async_turn_on(self, **kwargs):
|
|
"""Turn the device on."""
|
|
self.async_set_duration(**kwargs)
|
|
|
|
rgbw = None
|
|
white = kwargs.get(ATTR_WHITE_VALUE)
|
|
hs_color = kwargs.get(ATTR_HS_COLOR)
|
|
color_temp = kwargs.get(ATTR_COLOR_TEMP)
|
|
|
|
if hs_color is not None:
|
|
rgbw = "#"
|
|
for colorval in color_util.color_hs_to_RGB(*hs_color):
|
|
rgbw += f"{colorval:02x}"
|
|
if self._color_channels and self._color_channels & COLOR_CHANNEL_COLD_WHITE:
|
|
rgbw += "0000"
|
|
else:
|
|
# trim the CW value or it will not work correctly
|
|
rgbw += "00"
|
|
# white LED must be off in order for color to work
|
|
|
|
elif white is not None:
|
|
if self._color_channels & COLOR_CHANNEL_WARM_WHITE:
|
|
# trim the CW value or it will not work correctly
|
|
rgbw = f"#000000{white:02x}"
|
|
else:
|
|
rgbw = f"#00000000{white:02x}"
|
|
|
|
elif color_temp is not None:
|
|
# Limit color temp to min/max values
|
|
cold = max(
|
|
0,
|
|
min(
|
|
255,
|
|
round(
|
|
(self._max_mireds - color_temp)
|
|
/ (self._max_mireds - self._min_mireds)
|
|
* 255
|
|
),
|
|
),
|
|
)
|
|
warm = 255 - cold
|
|
rgbw = f"#000000{warm:02x}{cold:02x}"
|
|
|
|
if rgbw and self.values.color:
|
|
self.values.color.send_value(rgbw)
|
|
|
|
# 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:
|
|
brightness = kwargs[ATTR_BRIGHTNESS]
|
|
brightness = byte_to_zwave_brightness(brightness)
|
|
else:
|
|
brightness = 255
|
|
|
|
self.values.primary.send_value(brightness)
|
|
|
|
async def async_turn_off(self, **kwargs):
|
|
"""Turn the device off."""
|
|
self.async_set_duration(**kwargs)
|
|
|
|
self.values.primary.send_value(0)
|
|
|
|
def _calculate_color_values(self):
|
|
"""Parse color rgb and color temperature data."""
|
|
# Color Data String
|
|
data = self.values.color.data[ATTR_VALUE]
|
|
|
|
# RGB is always present in the OpenZWave color data string.
|
|
rgb = [int(data[1:3], 16), int(data[3:5], 16), int(data[5:7], 16)]
|
|
self._hs = color_util.color_RGB_to_hs(*rgb)
|
|
|
|
if self.values.color_channels is None:
|
|
return
|
|
|
|
# Color Channels
|
|
self._color_channels = self.values.color_channels.data[ATTR_VALUE]
|
|
|
|
# Parse remaining color channels. OpenZWave appends white channels
|
|
# that are present.
|
|
index = 7
|
|
temp_warm = 0
|
|
temp_cold = 0
|
|
|
|
# Update color temp limits.
|
|
if self.values.min_kelvin:
|
|
self._max_mireds = color_util.color_temperature_kelvin_to_mired(
|
|
self.values.min_kelvin.data[ATTR_VALUE]
|
|
)
|
|
if self.values.max_kelvin:
|
|
self._min_mireds = color_util.color_temperature_kelvin_to_mired(
|
|
self.values.max_kelvin.data[ATTR_VALUE]
|
|
)
|
|
|
|
# Warm white
|
|
if self._color_channels & COLOR_CHANNEL_WARM_WHITE:
|
|
self._white = int(data[index : index + 2], 16)
|
|
temp_warm = self._white
|
|
|
|
index += 2
|
|
|
|
# Cold white
|
|
if self._color_channels & COLOR_CHANNEL_COLD_WHITE:
|
|
self._white = int(data[index : index + 2], 16)
|
|
temp_cold = self._white
|
|
|
|
# Calculate color temps based on white LED status
|
|
if temp_cold or temp_warm:
|
|
self._ct = round(
|
|
self._max_mireds
|
|
- ((temp_cold / 255) * (self._max_mireds - self._min_mireds))
|
|
)
|
|
|
|
if not (
|
|
self._color_channels & COLOR_CHANNEL_RED
|
|
or self._color_channels & COLOR_CHANNEL_GREEN
|
|
or self._color_channels & COLOR_CHANNEL_BLUE
|
|
):
|
|
self._hs = None
|