Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion homeassistant/components/homekit/type_lights.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,10 @@ def _async_send_events(self, *_):
params[ATTR_COLOR_TEMP] = temp
elif self.rgbww_supported:
params[ATTR_RGBWW_COLOR] = color_temperature_to_rgbww(
temp, bright_val, self.min_mireds, self.max_mireds
color_temperature_mired_to_kelvin(temp),
bright_val,
color_temperature_mired_to_kelvin(self.max_mireds),
color_temperature_mired_to_kelvin(self.min_mireds),
)
elif self.rgbw_supported:
params[ATTR_RGBW_COLOR] = (*(0,) * 3, bright_val)
Expand Down
118 changes: 88 additions & 30 deletions homeassistant/components/light/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,10 +196,13 @@ def get_supported_color_modes(hass: HomeAssistant, entity_id: str) -> set[str] |
ATTR_RGBWW_COLOR = "rgbww_color"
ATTR_XY_COLOR = "xy_color"
ATTR_HS_COLOR = "hs_color"
ATTR_COLOR_TEMP = "color_temp"
ATTR_KELVIN = "kelvin"
ATTR_MIN_MIREDS = "min_mireds"
ATTR_MAX_MIREDS = "max_mireds"
ATTR_COLOR_TEMP = "color_temp" # Deprecated in HA Core 2022.11
ATTR_KELVIN = "kelvin" # Deprecated in HA Core 2022.11
ATTR_MIN_MIREDS = "min_mireds" # Deprecated in HA Core 2022.11
ATTR_MAX_MIREDS = "max_mireds" # Deprecated in HA Core 2022.11
ATTR_COLOR_TEMP_KELVIN = "color_temp_kelvin"
ATTR_MIN_COLOR_TEMP_KELVIN = "min_color_temp_kelvin"
ATTR_MAX_COLOR_TEMP_KELVIN = "max_color_temp_kelvin"
ATTR_COLOR_NAME = "color_name"
ATTR_WHITE = "white"

Expand Down Expand Up @@ -249,6 +252,7 @@ def get_supported_color_modes(hass: HomeAssistant, entity_id: str) -> set[str] |
vol.Exclusive(ATTR_COLOR_TEMP, COLOR_GROUP): vol.All(
vol.Coerce(int), vol.Range(min=1)
),
vol.Exclusive(ATTR_COLOR_TEMP_KELVIN, COLOR_GROUP): cv.positive_int,
vol.Exclusive(ATTR_KELVIN, COLOR_GROUP): cv.positive_int,
vol.Exclusive(ATTR_HS_COLOR, COLOR_GROUP): vol.All(
vol.Coerce(tuple),
Expand Down Expand Up @@ -309,9 +313,20 @@ def preprocess_turn_on_alternatives(
_LOGGER.warning("Got unknown color %s, falling back to white", color_name)
params[ATTR_RGB_COLOR] = (255, 255, 255)

if (mired := params.pop(ATTR_COLOR_TEMP, None)) is not None:
kelvin = color_util.color_temperature_mired_to_kelvin(mired)
params[ATTR_COLOR_TEMP] = int(mired)
params[ATTR_COLOR_TEMP_KELVIN] = int(kelvin)

if (kelvin := params.pop(ATTR_KELVIN, None)) is not None:
mired = color_util.color_temperature_kelvin_to_mired(kelvin)
params[ATTR_COLOR_TEMP] = int(mired)
params[ATTR_COLOR_TEMP_KELVIN] = int(kelvin)

if (kelvin := params.pop(ATTR_COLOR_TEMP_KELVIN, None)) is not None:
mired = color_util.color_temperature_kelvin_to_mired(kelvin)
params[ATTR_COLOR_TEMP] = int(mired)
params[ATTR_COLOR_TEMP_KELVIN] = int(kelvin)

brightness_pct = params.pop(ATTR_BRIGHTNESS_PCT, None)
if brightness_pct is not None:
Expand Down Expand Up @@ -350,6 +365,7 @@ def filter_turn_on_params(light: LightEntity, params: dict[str, Any]) -> dict[st
params.pop(ATTR_BRIGHTNESS, None)
if ColorMode.COLOR_TEMP not in supported_color_modes:
params.pop(ATTR_COLOR_TEMP, None)
params.pop(ATTR_COLOR_TEMP_KELVIN, None)
if ColorMode.HS not in supported_color_modes:
params.pop(ATTR_HS_COLOR, None)
if ColorMode.RGB not in supported_color_modes:
Expand Down Expand Up @@ -424,22 +440,28 @@ async def async_handle_light_on_service(
supported_color_modes = light.supported_color_modes

# If a color temperature is specified, emulate it if not supported by the light
if ATTR_COLOR_TEMP in params:
if ATTR_COLOR_TEMP_KELVIN in params:
if (
supported_color_modes
and ColorMode.COLOR_TEMP not in supported_color_modes
and ColorMode.RGBWW in supported_color_modes
):
color_temp = params.pop(ATTR_COLOR_TEMP)
params.pop(ATTR_COLOR_TEMP)
color_temp = params.pop(ATTR_COLOR_TEMP_KELVIN)
brightness = params.get(ATTR_BRIGHTNESS, light.brightness)
params[ATTR_RGBWW_COLOR] = color_util.color_temperature_to_rgbww(
color_temp, brightness, light.min_mireds, light.max_mireds
color_temp,
brightness,
light.min_color_temp_kelvin,
light.max_color_temp_kelvin,
)
elif ColorMode.COLOR_TEMP not in legacy_supported_color_modes:
color_temp = params.pop(ATTR_COLOR_TEMP)
params.pop(ATTR_COLOR_TEMP)
color_temp = params.pop(ATTR_COLOR_TEMP_KELVIN)
if color_supported(legacy_supported_color_modes):
temp_k = color_util.color_temperature_mired_to_kelvin(color_temp)
params[ATTR_HS_COLOR] = color_util.color_temperature_to_hs(temp_k)
params[ATTR_HS_COLOR] = color_util.color_temperature_to_hs(
color_temp
)

# If a color is specified, convert to the color space supported by the light
# Backwards compatibility: Fall back to hs color if light.supported_color_modes
Expand All @@ -457,7 +479,9 @@ async def async_handle_light_on_service(
elif (rgbww_color := params.pop(ATTR_RGBWW_COLOR, None)) is not None:
# https://github.com/python/mypy/issues/13673
rgb_color = color_util.color_rgbww_to_rgb( # type: ignore[call-arg]
*rgbww_color, light.min_mireds, light.max_mireds
*rgbww_color,
light.min_color_temp_kelvin,
light.max_color_temp_kelvin,
)
params[ATTR_HS_COLOR] = color_util.color_RGB_to_hs(*rgb_color)
elif ATTR_HS_COLOR in params and ColorMode.HS not in supported_color_modes:
Expand All @@ -470,7 +494,7 @@ async def async_handle_light_on_service(
elif ColorMode.RGBWW in supported_color_modes:
rgb_color = color_util.color_hs_to_RGB(*hs_color)
params[ATTR_RGBWW_COLOR] = color_util.color_rgb_to_rgbww(
*rgb_color, light.min_mireds, light.max_mireds
*rgb_color, light.min_color_temp_kelvin, light.max_color_temp_kelvin
)
elif ColorMode.XY in supported_color_modes:
params[ATTR_XY_COLOR] = color_util.color_hs_to_xy(*hs_color)
Expand All @@ -481,7 +505,7 @@ async def async_handle_light_on_service(
elif ColorMode.RGBWW in supported_color_modes:
# https://github.com/python/mypy/issues/13673
params[ATTR_RGBWW_COLOR] = color_util.color_rgb_to_rgbww( # type: ignore[call-arg]
*rgb_color, light.min_mireds, light.max_mireds
*rgb_color, light.min_color_temp_kelvin, light.max_color_temp_kelvin
)
elif ColorMode.HS in supported_color_modes:
params[ATTR_HS_COLOR] = color_util.color_RGB_to_hs(*rgb_color)
Expand All @@ -499,7 +523,7 @@ async def async_handle_light_on_service(
elif ColorMode.RGBWW in supported_color_modes:
rgb_color = color_util.color_xy_to_RGB(*xy_color)
params[ATTR_RGBWW_COLOR] = color_util.color_rgb_to_rgbww(
*rgb_color, light.min_mireds, light.max_mireds
*rgb_color, light.min_color_temp_kelvin, light.max_color_temp_kelvin
)
elif ATTR_RGBW_COLOR in params and ColorMode.RGBW not in supported_color_modes:
rgbw_color = params.pop(ATTR_RGBW_COLOR)
Expand All @@ -508,7 +532,7 @@ async def async_handle_light_on_service(
params[ATTR_RGB_COLOR] = rgb_color
elif ColorMode.RGBWW in supported_color_modes:
params[ATTR_RGBWW_COLOR] = color_util.color_rgb_to_rgbww(
*rgb_color, light.min_mireds, light.max_mireds
*rgb_color, light.min_color_temp_kelvin, light.max_color_temp_kelvin
)
elif ColorMode.HS in supported_color_modes:
params[ATTR_HS_COLOR] = color_util.color_RGB_to_hs(*rgb_color)
Expand All @@ -520,7 +544,7 @@ async def async_handle_light_on_service(
assert (rgbww_color := params.pop(ATTR_RGBWW_COLOR)) is not None
# https://github.com/python/mypy/issues/13673
rgb_color = color_util.color_rgbww_to_rgb( # type: ignore[call-arg]
*rgbww_color, light.min_mireds, light.max_mireds
*rgbww_color, light.min_color_temp_kelvin, light.max_color_temp_kelvin
)
if ColorMode.RGB in supported_color_modes:
params[ATTR_RGB_COLOR] = rgb_color
Expand Down Expand Up @@ -755,11 +779,16 @@ class LightEntity(ToggleEntity):
_attr_brightness: int | None = None
_attr_color_mode: ColorMode | str | None = None
_attr_color_temp: int | None = None
_attr_color_temp_kelvin: int | None = None
_attr_effect_list: list[str] | None = None
_attr_effect: str | None = None
_attr_hs_color: tuple[float, float] | None = None
_attr_max_mireds: int = 500
_attr_min_mireds: int = 153
# Default to the Philips Hue value that HA has always assumed
# https://developers.meethue.com/documentation/core-concepts
_attr_max_color_temp_kelvin: int | None = None
_attr_min_color_temp_kelvin: int | None = None
_attr_max_mireds: int = 500 # 2000 K
_attr_min_mireds: int = 153 # 6535 K
_attr_rgb_color: tuple[int, int, int] | None = None
_attr_rgbw_color: tuple[int, int, int, int] | None = None
_attr_rgbww_color: tuple[int, int, int, int, int] | None = None
Expand Down Expand Up @@ -787,7 +816,7 @@ def _light_internal_color_mode(self) -> str:

if ColorMode.HS in supported and self.hs_color is not None:
return ColorMode.HS
if ColorMode.COLOR_TEMP in supported and self.color_temp is not None:
if ColorMode.COLOR_TEMP in supported and self.color_temp_kelvin is not None:
return ColorMode.COLOR_TEMP
if ColorMode.BRIGHTNESS in supported and self.brightness is not None:
return ColorMode.BRIGHTNESS
Expand Down Expand Up @@ -833,20 +862,37 @@ def color_temp(self) -> int | None:
"""Return the CT color value in mireds."""
return self._attr_color_temp

@property
def color_temp_kelvin(self) -> int | None:
"""Return the CT color value in Kelvin."""
if self._attr_color_temp_kelvin is None and self.color_temp:
return color_util.color_temperature_mired_to_kelvin(self.color_temp)
return self._attr_color_temp_kelvin

@property
def min_mireds(self) -> int:
"""Return the coldest color_temp that this light supports."""
# Default to the Philips Hue value that HA has always assumed
# https://developers.meethue.com/documentation/core-concepts
return self._attr_min_mireds

@property
def max_mireds(self) -> int:
"""Return the warmest color_temp that this light supports."""
# Default to the Philips Hue value that HA has always assumed
# https://developers.meethue.com/documentation/core-concepts
return self._attr_max_mireds

@property
def min_color_temp_kelvin(self) -> int:
"""Return the warmest color_temp_kelvin that this light supports."""
if self._attr_min_color_temp_kelvin is None:
return color_util.color_temperature_mired_to_kelvin(self.max_mireds)
return self._attr_min_color_temp_kelvin

@property
def max_color_temp_kelvin(self) -> int:
"""Return the coldest color_temp_kelvin that this light supports."""
if self._attr_min_color_temp_kelvin is None:
return color_util.color_temperature_mired_to_kelvin(self.min_mireds)
return self._attr_min_color_temp_kelvin

@property
def effect_list(self) -> list[str] | None:
"""Return the list of supported effects."""
Expand All @@ -867,6 +913,8 @@ def capability_attributes(self) -> dict[str, Any]:
if ColorMode.COLOR_TEMP in supported_color_modes:
data[ATTR_MIN_MIREDS] = self.min_mireds
data[ATTR_MAX_MIREDS] = self.max_mireds
data[ATTR_MIN_COLOR_TEMP_KELVIN] = self.min_color_temp_kelvin
data[ATTR_MAX_COLOR_TEMP_KELVIN] = self.max_color_temp_kelvin

if supported_features & LightEntityFeature.EFFECT:
data[ATTR_EFFECT_LIST] = self.effect_list
Expand Down Expand Up @@ -904,16 +952,14 @@ def _light_internal_convert_color(
elif color_mode == ColorMode.RGBWW and self.rgbww_color:
rgbww_color = self.rgbww_color
rgb_color = color_util.color_rgbww_to_rgb(
*rgbww_color, self.min_mireds, self.max_mireds
*rgbww_color, self.min_color_temp_kelvin, self.max_color_temp_kelvin
)
data[ATTR_HS_COLOR] = color_util.color_RGB_to_hs(*rgb_color)
data[ATTR_RGB_COLOR] = tuple(int(x) for x in rgb_color[0:3])
data[ATTR_RGBWW_COLOR] = tuple(int(x) for x in rgbww_color[0:5])
data[ATTR_XY_COLOR] = color_util.color_RGB_to_xy(*rgb_color)
elif color_mode == ColorMode.COLOR_TEMP and self.color_temp:
hs_color = color_util.color_temperature_to_hs(
color_util.color_temperature_mired_to_kelvin(self.color_temp)
)
elif color_mode == ColorMode.COLOR_TEMP and self.color_temp_kelvin:
hs_color = color_util.color_temperature_to_hs(self.color_temp_kelvin)
data[ATTR_HS_COLOR] = (round(hs_color[0], 3), round(hs_color[1], 3))
data[ATTR_RGB_COLOR] = color_util.color_hs_to_RGB(*hs_color)
data[ATTR_XY_COLOR] = color_util.color_hs_to_xy(*hs_color)
Expand Down Expand Up @@ -949,15 +995,27 @@ def state_attributes(self) -> dict[str, Any] | None:
data[ATTR_BRIGHTNESS] = self.brightness

if color_mode == ColorMode.COLOR_TEMP:
data[ATTR_COLOR_TEMP] = self.color_temp
data[ATTR_COLOR_TEMP_KELVIN] = self.color_temp_kelvin
if not self.color_temp_kelvin:
data[ATTR_COLOR_TEMP] = None
else:
data[ATTR_COLOR_TEMP] = color_util.color_temperature_kelvin_to_mired(
self.color_temp_kelvin
)

if color_mode in COLOR_MODES_COLOR or color_mode == ColorMode.COLOR_TEMP:
data.update(self._light_internal_convert_color(color_mode))

if supported_features & SUPPORT_COLOR_TEMP and not self.supported_color_modes:
# Backwards compatibility
# Add warning in 2021.6, remove in 2021.10
data[ATTR_COLOR_TEMP] = self.color_temp
data[ATTR_COLOR_TEMP_KELVIN] = self.color_temp_kelvin
if not self.color_temp_kelvin:
data[ATTR_COLOR_TEMP] = None
else:
data[ATTR_COLOR_TEMP] = color_util.color_temperature_kelvin_to_mired(
self.color_temp_kelvin
)

if supported_features & LightEntityFeature.EFFECT:
data[ATTR_EFFECT] = self.effect
Expand Down
45 changes: 33 additions & 12 deletions homeassistant/util/color.py
Original file line number Diff line number Diff line change
Expand Up @@ -436,10 +436,12 @@ def color_rgbw_to_rgb(r: int, g: int, b: int, w: int) -> tuple[int, int, int]:


def color_rgb_to_rgbww(
Comment thread
bdraco marked this conversation as resolved.
r: int, g: int, b: int, min_mireds: int, max_mireds: int
r: int, g: int, b: int, min_kelvin: int, max_kelvin: int
) -> tuple[int, int, int, int, int]:
"""Convert an rgb color to an rgbww representation."""
# Find the color temperature when both white channels have equal brightness
max_mireds = color_temperature_kelvin_to_mired(min_kelvin)
min_mireds = color_temperature_kelvin_to_mired(max_kelvin)
mired_range = max_mireds - min_mireds
mired_midpoint = min_mireds + mired_range / 2
color_temp_kelvin = color_temperature_mired_to_kelvin(mired_midpoint)
Expand All @@ -460,10 +462,12 @@ def color_rgb_to_rgbww(


def color_rgbww_to_rgb(
r: int, g: int, b: int, cw: int, ww: int, min_mireds: int, max_mireds: int
r: int, g: int, b: int, cw: int, ww: int, min_kelvin: int, max_kelvin: int
) -> tuple[int, int, int]:
"""Convert an rgbww color to an rgb representation."""
# Calculate color temperature of the white channels
max_mireds = color_temperature_kelvin_to_mired(min_kelvin)
min_mireds = color_temperature_kelvin_to_mired(max_kelvin)
mired_range = max_mireds - min_mireds
try:
ct_ratio = ww / (cw + ww)
Expand Down Expand Up @@ -530,32 +534,49 @@ def color_temperature_to_rgb(


def color_temperature_to_rgbww(
temperature: int, brightness: int, min_mireds: int, max_mireds: int
temperature: int, brightness: int, min_kelvin: int, max_kelvin: int
) -> tuple[int, int, int, int, int]:
"""Convert color temperature in mireds to rgbcw."""
"""Convert color temperature in kelvin to rgbcw.

Returns a (r, g, b, cw, ww) tuple.
"""
max_mireds = color_temperature_kelvin_to_mired(min_kelvin)
min_mireds = color_temperature_kelvin_to_mired(max_kelvin)
temperature = color_temperature_kelvin_to_mired(temperature)
mired_range = max_mireds - min_mireds
cold = ((max_mireds - temperature) / mired_range) * brightness
warm = brightness - cold
return (0, 0, 0, round(cold), round(warm))


def rgbww_to_color_temperature(
rgbww: tuple[int, int, int, int, int], min_mireds: int, max_mireds: int
rgbww: tuple[int, int, int, int, int], min_kelvin: int, max_kelvin: int
) -> tuple[int, int]:
"""Convert rgbcw to color temperature in mireds."""
"""Convert rgbcw to color temperature in kelvin.

Returns a tuple (color_temperature, brightness).
"""
_, _, _, cold, warm = rgbww
return while_levels_to_color_temperature(cold, warm, min_mireds, max_mireds)
return _white_levels_to_color_temperature(cold, warm, min_kelvin, max_kelvin)


def while_levels_to_color_temperature(
cold: int, warm: int, min_mireds: int, max_mireds: int
def _white_levels_to_color_temperature(
cold: int, warm: int, min_kelvin: int, max_kelvin: int
) -> tuple[int, int]:
"""Convert whites to color temperature in mireds."""
"""Convert whites to color temperature in kelvin.

Returns a tuple (color_temperature, brightness).
"""
max_mireds = color_temperature_kelvin_to_mired(min_kelvin)
min_mireds = color_temperature_kelvin_to_mired(max_kelvin)
brightness = warm / 255 + cold / 255
if brightness == 0:
return (max_mireds, 0)
# Return the warmest color if brightness is 0
return (min_kelvin, 0)
return round(
((cold / 255 / brightness) * (min_mireds - max_mireds)) + max_mireds
color_temperature_mired_to_kelvin(
((cold / 255 / brightness) * (min_mireds - max_mireds)) + max_mireds
)
), min(255, round(brightness * 255))


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ async def test_nanoleaf_nl55_setup(hass):
unique_id="homekit-AAAA011111111111-19",
supported_features=0,
capabilities={
"max_color_temp_kelvin": 6535,
"min_color_temp_kelvin": 2127,
"max_mireds": 470,
"min_mireds": 153,
"supported_color_modes": ["color_temp", "hs"],
Expand Down
Loading