diff --git a/CODEOWNERS b/CODEOWNERS index 29a2f66628b..398d7e60f5e 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -768,8 +768,8 @@ build.json @home-assistant/supervisor /tests/components/lupusec/ @majuss @suaveolent /homeassistant/components/lutron/ @cdheiser @wilburCForce /tests/components/lutron/ @cdheiser @wilburCForce -/homeassistant/components/lutron_caseta/ @swails @bdraco @danaues -/tests/components/lutron_caseta/ @swails @bdraco @danaues +/homeassistant/components/lutron_caseta/ @swails @bdraco @danaues @eclair4151 +/tests/components/lutron_caseta/ @swails @bdraco @danaues @eclair4151 /homeassistant/components/lyric/ @timmo001 /tests/components/lyric/ @timmo001 /homeassistant/components/mastodon/ @fabaff diff --git a/homeassistant/components/lutron_caseta/const.py b/homeassistant/components/lutron_caseta/const.py index af06bf0e0f0..7493878bece 100644 --- a/homeassistant/components/lutron_caseta/const.py +++ b/homeassistant/components/lutron_caseta/const.py @@ -14,6 +14,9 @@ LUTRON_CASETA_BUTTON_EVENT = "lutron_caseta_button_event" BRIDGE_DEVICE_ID = "1" +DEVICE_TYPE_WHITE_TUNE = "WhiteTune" +DEVICE_TYPE_SPECTRUM_TUNE = "SpectrumTune" + MANUFACTURER = "Lutron Electronics Co., Inc" ATTR_SERIAL = "serial" diff --git a/homeassistant/components/lutron_caseta/light.py b/homeassistant/components/lutron_caseta/light.py index ffab0689636..eb3e38b2e39 100644 --- a/homeassistant/components/lutron_caseta/light.py +++ b/homeassistant/components/lutron_caseta/light.py @@ -2,9 +2,18 @@ from datetime import timedelta from typing import Any +from pylutron_caseta.color_value import ( + ColorMode as LutronColorMode, + FullColorValue, + WarmCoolColorValue, +) + from homeassistant.components.light import ( ATTR_BRIGHTNESS, + ATTR_COLOR_TEMP_KELVIN, + ATTR_HS_COLOR, ATTR_TRANSITION, + ATTR_WHITE, DOMAIN, ColorMode, LightEntity, @@ -15,9 +24,24 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import LutronCasetaDeviceUpdatableEntity -from .const import DOMAIN as CASETA_DOMAIN +from .const import ( + DEVICE_TYPE_SPECTRUM_TUNE, + DEVICE_TYPE_WHITE_TUNE, + DOMAIN as CASETA_DOMAIN, +) from .models import LutronCasetaData +SUPPORTED_COLOR_MODE_DICT = { + DEVICE_TYPE_SPECTRUM_TUNE: { + ColorMode.HS, + ColorMode.COLOR_TEMP, + ColorMode.WHITE, + }, + DEVICE_TYPE_WHITE_TUNE: {ColorMode.COLOR_TEMP}, +} + +WARM_DEVICE_TYPES = {DEVICE_TYPE_WHITE_TUNE, DEVICE_TYPE_SPECTRUM_TUNE} + def to_lutron_level(level): """Convert the given Home Assistant light level (0-255) to Lutron (0-100).""" @@ -48,37 +72,158 @@ async def async_setup_entry( class LutronCasetaLight(LutronCasetaDeviceUpdatableEntity, LightEntity): - """Representation of a Lutron Light, including dimmable.""" + """Representation of a Lutron Light, including dimmable, white tune, and spectrum tune.""" - _attr_color_mode = ColorMode.BRIGHTNESS - _attr_supported_color_modes = {ColorMode.BRIGHTNESS} _attr_supported_features = LightEntityFeature.TRANSITION + def __init__(self, light: dict[str, Any], data: LutronCasetaData) -> None: + """Initialize the light and set the supported color modes. + + :param light: The lutron light device to initialize. + :param data: The integration data + """ + super().__init__(light, data) + + self._attr_min_color_temp_kelvin = self._get_min_color_temp_kelvin(light) + self._attr_max_color_temp_kelvin = self._get_max_color_temp_kelvin(light) + + light_type = light["type"] + self._attr_supported_color_modes = SUPPORTED_COLOR_MODE_DICT.get( + light_type, {ColorMode.BRIGHTNESS} + ) + + self.supports_warm_cool = light_type in WARM_DEVICE_TYPES + self.supports_warm_dim = light_type == DEVICE_TYPE_SPECTRUM_TUNE + self.supports_spectrum_tune = light_type == DEVICE_TYPE_SPECTRUM_TUNE + + def _get_min_color_temp_kelvin(self, light: dict[str, Any]) -> int: + """Return minimum supported color temperature. + + :param light: The light to get the minimum color temperature for. + """ + white_tune_range = light.get("white_tuning_range") + # Default to 1.4k if not found + if white_tune_range is None or "Min" not in white_tune_range: + return 1400 + + return white_tune_range.get("Min") + + def _get_max_color_temp_kelvin(self, light: dict[str, Any]) -> int: + """Return maximum supported color temperature. + + :param light: The light to get the maximum color temperature for. + """ + white_tune_range = light.get("white_tuning_range") + # Default to 10k if not found + if white_tune_range is None or "Max" not in white_tune_range: + return 10000 + + return white_tune_range.get("Max") + @property - def brightness(self): + def brightness(self) -> int: """Return the brightness of the light.""" return to_hass_level(self._device["current_state"]) - async def _set_brightness(self, brightness, **kwargs): + async def _async_set_brightness( + self, brightness: int | None, color_value: LutronColorMode | None, **kwargs: Any + ) -> None: args = {} if ATTR_TRANSITION in kwargs: args["fade_time"] = timedelta(seconds=kwargs[ATTR_TRANSITION]) + if brightness is not None: + brightness = to_lutron_level(brightness) await self._smartbridge.set_value( - self.device_id, to_lutron_level(brightness), **args + self.device_id, value=brightness, color_value=color_value, **args + ) + + async def _async_set_warm_dim(self, brightness: int | None, **kwargs: Any): + """Set the light to warm dim mode.""" + set_warm_dim_kwargs: dict[str, Any] = {} + if ATTR_TRANSITION in kwargs: + set_warm_dim_kwargs["fade_time"] = timedelta( + seconds=kwargs[ATTR_TRANSITION] + ) + + if brightness is not None: + brightness = to_lutron_level(brightness) + + await self._smartbridge.set_warm_dim( + self.device_id, brightness, **set_warm_dim_kwargs ) async def async_turn_on(self, **kwargs: Any) -> None: """Turn the light on.""" - brightness = kwargs.pop(ATTR_BRIGHTNESS, 255) + # first check for "white mode" (WarmDim) + if (white_color := kwargs.get(ATTR_WHITE)) is not None: + await self._async_set_warm_dim(white_color) + return - await self._set_brightness(brightness, **kwargs) + brightness = kwargs.pop(ATTR_BRIGHTNESS, None) + color: LutronColorMode | None = None + hs_color: tuple[float, float] | None = kwargs.pop(ATTR_HS_COLOR, None) + kelvin_color: int | None = kwargs.pop(ATTR_COLOR_TEMP_KELVIN, None) + + if hs_color is not None: + color = FullColorValue(hs_color[0], hs_color[1]) + elif kelvin_color is not None: + color = WarmCoolColorValue(kelvin_color) + + # if user is pressing on button nothing is set, so set brightness to 255 + if color is None and brightness is None: + brightness = 255 + + await self._async_set_brightness(brightness, color, **kwargs) async def async_turn_off(self, **kwargs: Any) -> None: """Turn the light off.""" - await self._set_brightness(0, **kwargs) + await self._async_set_brightness(0, None, **kwargs) @property - def is_on(self): + def color_mode(self) -> ColorMode: + """Return the current color mode of the light.""" + + currently_warm_dim = self._device.get("warm_dim", False) + if self.supports_warm_dim and currently_warm_dim: + return ColorMode.WHITE + + current_color = self._device.get("color") + if self.supports_warm_cool and isinstance(current_color, WarmCoolColorValue): + return ColorMode.COLOR_TEMP + + if self.supports_spectrum_tune and isinstance(current_color, FullColorValue): + return ColorMode.HS + + return ColorMode.BRIGHTNESS + + @property + def is_on(self) -> bool: """Return true if device is on.""" return self._device["current_state"] > 0 + + @property + def hs_color(self) -> tuple[float, float] | None: + """Return the current color of the light.""" + current_color: FullColorValue | WarmCoolColorValue | None = self._device.get( + "color" + ) + + # if bulb is set to full spectrum, return the hue and saturation + if isinstance(current_color, FullColorValue): + return (current_color.hue, current_color.saturation) + + return None + + @property + def color_temp_kelvin(self) -> int | None: + """Return the CT color value in kelvin.""" + current_color: FullColorValue | WarmCoolColorValue | None = self._device.get( + "color" + ) + + # if bulb is set to warm cool mode, return the kelvin value + if isinstance(current_color, WarmCoolColorValue): + return current_color.kelvin + + return None diff --git a/homeassistant/components/lutron_caseta/manifest.json b/homeassistant/components/lutron_caseta/manifest.json index 1216ff70fdd..48445f645aa 100644 --- a/homeassistant/components/lutron_caseta/manifest.json +++ b/homeassistant/components/lutron_caseta/manifest.json @@ -1,7 +1,7 @@ { "domain": "lutron_caseta", "name": "Lutron Cas\u00e9ta", - "codeowners": ["@swails", "@bdraco", "@danaues"], + "codeowners": ["@swails", "@bdraco", "@danaues", "@eclair4151"], "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/lutron_caseta", "homekit": {