1
mirror of https://github.com/home-assistant/core synced 2024-08-02 23:40:32 +02:00
ha-core/homeassistant/components/ozw/light.py
epenet 277562bc38
Add light setup type hints [l-r] (#63292)
Co-authored-by: epenet <epenet@users.noreply.github.com>
2022-01-03 15:11:59 +01:00

344 lines
12 KiB
Python

"""Support for Z-Wave lights."""
import logging
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
ATTR_COLOR_TEMP,
ATTR_HS_COLOR,
ATTR_RGBW_COLOR,
ATTR_TRANSITION,
COLOR_MODE_BRIGHTNESS,
COLOR_MODE_COLOR_TEMP,
COLOR_MODE_HS,
COLOR_MODE_RGBW,
DOMAIN as LIGHT_DOMAIN,
SUPPORT_TRANSITION,
LightEntity,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback
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: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""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._rgbw_color = None
self._ct = None
self._attr_color_mode = None
self._attr_supported_features = 0
self._attr_supported_color_modes = set()
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._attr_supported_features |= SUPPORT_TRANSITION
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._attr_supported_color_modes.add(COLOR_MODE_COLOR_TEMP)
self._attr_supported_color_modes.add(COLOR_MODE_HS)
# 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._attr_supported_color_modes.add(COLOR_MODE_RGBW)
if not self._attr_supported_color_modes and self.values.color is not None:
self._attr_supported_color_modes.add(COLOR_MODE_HS)
if not self._attr_supported_color_modes:
self._attr_supported_color_modes.add(COLOR_MODE_BRIGHTNESS)
# Default: Brightness (no color)
self._attr_color_mode = COLOR_MODE_BRIGHTNESS
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 hs_color(self):
"""Return the hs color."""
return self._hs
@property
def rgbw_color(self):
"""Return the rgbw color."""
return self._rgbw_color
@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):
if (transition := kwargs[ATTR_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
hs_color = kwargs.get(ATTR_HS_COLOR)
rgbw_color = kwargs.get(ATTR_RGBW_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 rgbw_color is not None:
red = rgbw_color[0]
green = rgbw_color[1]
blue = rgbw_color[2]
white = rgbw_color[3]
if self._color_channels & COLOR_CHANNEL_WARM_WHITE:
# trim the CW value or it will not work correctly
rgbw = f"#{red:02x}{green:02x}{blue:02x}{white:02x}"
else:
rgbw = f"#{red:02x}{green:02x}{blue:02x}00{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)
# Light supports color, set color mode to hs
self._attr_color_mode = COLOR_MODE_HS
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:
white = int(data[index : index + 2], 16)
self._rgbw_color = [rgb[0], rgb[1], rgb[2], white]
temp_warm = white
# Light supports rgbw, set color mode to rgbw
self._attr_color_mode = COLOR_MODE_RGBW
index += 2
# Cold white
if self._color_channels & COLOR_CHANNEL_COLD_WHITE:
white = int(data[index : index + 2], 16)
self._rgbw_color = [rgb[0], rgb[1], rgb[2], white]
temp_cold = white
# Light supports rgbw, set color mode to rgbw
self._attr_color_mode = COLOR_MODE_RGBW
# 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 (
self._color_channels & COLOR_CHANNEL_WARM_WHITE
and self._color_channels & COLOR_CHANNEL_COLD_WHITE
):
# Light supports 5 channels, set color_mode to color_temp or hs
if rgb[0] == 0 and rgb[1] == 0 and rgb[2] == 0:
# Color channels turned off, set color mode to color_temp
self._attr_color_mode = COLOR_MODE_COLOR_TEMP
else:
self._attr_color_mode = COLOR_MODE_HS
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