Add temperature to the light color mode parameter fallbacks (#86026)

* Add color temperature to the color mode fallbacks

* Manually add ATTR_COLOR_TEMP since ATTR_COLOR_TEMP_KELVIN is pre-parsed

* Include the legacy ATTR_COLOR_TEMP attribute in the tests

* Apply suggestions from code review

Co-authored-by: Erik Montnemery <erik@montnemery.com>

* Add citation for McCamy's approximation formula

If still existing, also see page 3 of https://www.st.com/resource/en/application_note/an5638-how-correlated-color-temperature-is-calculated-by-vd6283-stmicroelectronics.pdf

* Update homeassistant/util/color.py

---------

Co-authored-by: Erik Montnemery <erik@montnemery.com>
This commit is contained in:
Niccolò Maggioni 2023-12-01 08:26:07 +01:00 committed by GitHub
parent 7ec2980e52
commit 0232c8dcb0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 97 additions and 0 deletions

View File

@ -500,6 +500,14 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa:
)
elif ColorMode.XY in supported_color_modes:
params[ATTR_XY_COLOR] = color_util.color_hs_to_xy(*hs_color)
elif ColorMode.COLOR_TEMP in supported_color_modes:
xy_color = color_util.color_hs_to_xy(*hs_color)
params[ATTR_COLOR_TEMP_KELVIN] = color_util.color_xy_to_temperature(
*xy_color
)
params[ATTR_COLOR_TEMP] = color_util.color_temperature_kelvin_to_mired(
params[ATTR_COLOR_TEMP_KELVIN]
)
elif ATTR_RGB_COLOR in params and ColorMode.RGB not in supported_color_modes:
assert (rgb_color := params.pop(ATTR_RGB_COLOR)) is not None
if ColorMode.RGBW in supported_color_modes:
@ -515,6 +523,14 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa:
params[ATTR_HS_COLOR] = color_util.color_RGB_to_hs(*rgb_color)
elif ColorMode.XY in supported_color_modes:
params[ATTR_XY_COLOR] = color_util.color_RGB_to_xy(*rgb_color)
elif ColorMode.COLOR_TEMP in supported_color_modes:
xy_color = color_util.color_RGB_to_xy(*rgb_color)
params[ATTR_COLOR_TEMP_KELVIN] = color_util.color_xy_to_temperature(
*xy_color
)
params[ATTR_COLOR_TEMP] = color_util.color_temperature_kelvin_to_mired(
params[ATTR_COLOR_TEMP_KELVIN]
)
elif ATTR_XY_COLOR in params and ColorMode.XY not in supported_color_modes:
xy_color = params.pop(ATTR_XY_COLOR)
if ColorMode.HS in supported_color_modes:
@ -529,6 +545,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa:
params[ATTR_RGBWW_COLOR] = color_util.color_rgb_to_rgbww(
*rgb_color, light.min_color_temp_kelvin, light.max_color_temp_kelvin
)
elif ColorMode.COLOR_TEMP in supported_color_modes:
params[ATTR_COLOR_TEMP_KELVIN] = color_util.color_xy_to_temperature(
*xy_color
)
params[ATTR_COLOR_TEMP] = color_util.color_temperature_kelvin_to_mired(
params[ATTR_COLOR_TEMP_KELVIN]
)
elif ATTR_RGBW_COLOR in params and ColorMode.RGBW not in supported_color_modes:
rgbw_color = params.pop(ATTR_RGBW_COLOR)
rgb_color = color_util.color_rgbw_to_rgb(*rgbw_color)
@ -542,6 +565,14 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa:
params[ATTR_HS_COLOR] = color_util.color_RGB_to_hs(*rgb_color)
elif ColorMode.XY in supported_color_modes:
params[ATTR_XY_COLOR] = color_util.color_RGB_to_xy(*rgb_color)
elif ColorMode.COLOR_TEMP in supported_color_modes:
xy_color = color_util.color_RGB_to_xy(*rgb_color)
params[ATTR_COLOR_TEMP_KELVIN] = color_util.color_xy_to_temperature(
*xy_color
)
params[ATTR_COLOR_TEMP] = color_util.color_temperature_kelvin_to_mired(
params[ATTR_COLOR_TEMP_KELVIN]
)
elif (
ATTR_RGBWW_COLOR in params and ColorMode.RGBWW not in supported_color_modes
):
@ -558,6 +589,14 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa:
params[ATTR_HS_COLOR] = color_util.color_RGB_to_hs(*rgb_color)
elif ColorMode.XY in supported_color_modes:
params[ATTR_XY_COLOR] = color_util.color_RGB_to_xy(*rgb_color)
elif ColorMode.COLOR_TEMP in supported_color_modes:
xy_color = color_util.color_RGB_to_xy(*rgb_color)
params[ATTR_COLOR_TEMP_KELVIN] = color_util.color_xy_to_temperature(
*xy_color
)
params[ATTR_COLOR_TEMP] = color_util.color_temperature_kelvin_to_mired(
params[ATTR_COLOR_TEMP_KELVIN]
)
# If white is set to True, set it to the light's brightness
# Add a warning in Home Assistant Core 2023.5 if the brightness is set to an

View File

@ -576,6 +576,18 @@ def _white_levels_to_color_temperature(
), min(255, round(brightness * 255))
def color_xy_to_temperature(x: float, y: float) -> int:
"""Convert an xy color to a color temperature in Kelvin.
Uses McCamy's approximation (https://doi.org/10.1002/col.5080170211),
close enough for uses between 2000 K and 10000 K.
"""
n = (x - 0.3320) / (0.1858 - y)
CCT = 437 * (n**3) + 3601 * (n**2) + 6861 * n + 5517
return int(CCT)
def _clamp(color_component: float, minimum: float = 0, maximum: float = 255) -> float:
"""Clamp the given color component value between the given min and max values.

View File

@ -1444,6 +1444,7 @@ async def test_light_service_call_color_conversion(
platform.ENTITIES.append(platform.MockLight("Test_legacy", STATE_ON))
platform.ENTITIES.append(platform.MockLight("Test_rgbw", STATE_ON))
platform.ENTITIES.append(platform.MockLight("Test_rgbww", STATE_ON))
platform.ENTITIES.append(platform.MockLight("Test_temperature", STATE_ON))
entity0 = platform.ENTITIES[0]
entity0.supported_color_modes = {light.ColorMode.HS}
@ -1470,6 +1471,9 @@ async def test_light_service_call_color_conversion(
entity6 = platform.ENTITIES[6]
entity6.supported_color_modes = {light.ColorMode.RGBWW}
entity7 = platform.ENTITIES[7]
entity7.supported_color_modes = {light.ColorMode.COLOR_TEMP}
assert await async_setup_component(hass, "light", {"light": {"platform": "test"}})
await hass.async_block_till_done()
@ -1498,6 +1502,9 @@ async def test_light_service_call_color_conversion(
state = hass.states.get(entity6.entity_id)
assert state.attributes["supported_color_modes"] == [light.ColorMode.RGBWW]
state = hass.states.get(entity7.entity_id)
assert state.attributes["supported_color_modes"] == [light.ColorMode.COLOR_TEMP]
await hass.services.async_call(
"light",
"turn_on",
@ -1510,6 +1517,7 @@ async def test_light_service_call_color_conversion(
entity4.entity_id,
entity5.entity_id,
entity6.entity_id,
entity7.entity_id,
],
"brightness_pct": 100,
"hs_color": (240, 100),
@ -1530,6 +1538,8 @@ async def test_light_service_call_color_conversion(
assert data == {"brightness": 255, "rgbw_color": (0, 0, 255, 0)}
_, data = entity6.last_call("turn_on")
assert data == {"brightness": 255, "rgbww_color": (0, 0, 255, 0, 0)}
_, data = entity7.last_call("turn_on")
assert data == {"brightness": 255, "color_temp_kelvin": 1739, "color_temp": 575}
await hass.services.async_call(
"light",
@ -1543,6 +1553,7 @@ async def test_light_service_call_color_conversion(
entity4.entity_id,
entity5.entity_id,
entity6.entity_id,
entity7.entity_id,
],
"brightness_pct": 100,
"hs_color": (240, 0),
@ -1564,6 +1575,8 @@ async def test_light_service_call_color_conversion(
_, data = entity6.last_call("turn_on")
# The midpoint of the the white channels is warm, compensated by adding green + blue
assert data == {"brightness": 255, "rgbww_color": (0, 76, 141, 255, 255)}
_, data = entity7.last_call("turn_on")
assert data == {"brightness": 255, "color_temp_kelvin": 5962, "color_temp": 167}
await hass.services.async_call(
"light",
@ -1577,6 +1590,7 @@ async def test_light_service_call_color_conversion(
entity4.entity_id,
entity5.entity_id,
entity6.entity_id,
entity7.entity_id,
],
"brightness_pct": 50,
"rgb_color": (128, 0, 0),
@ -1597,6 +1611,8 @@ async def test_light_service_call_color_conversion(
assert data == {"brightness": 128, "rgbw_color": (128, 0, 0, 0)}
_, data = entity6.last_call("turn_on")
assert data == {"brightness": 128, "rgbww_color": (128, 0, 0, 0, 0)}
_, data = entity7.last_call("turn_on")
assert data == {"brightness": 128, "color_temp_kelvin": 6279, "color_temp": 159}
await hass.services.async_call(
"light",
@ -1610,6 +1626,7 @@ async def test_light_service_call_color_conversion(
entity4.entity_id,
entity5.entity_id,
entity6.entity_id,
entity7.entity_id,
],
"brightness_pct": 50,
"rgb_color": (255, 255, 255),
@ -1631,6 +1648,8 @@ async def test_light_service_call_color_conversion(
_, data = entity6.last_call("turn_on")
# The midpoint the the white channels is warm, compensated by adding green + blue
assert data == {"brightness": 128, "rgbww_color": (0, 76, 141, 255, 255)}
_, data = entity7.last_call("turn_on")
assert data == {"brightness": 128, "color_temp_kelvin": 5962, "color_temp": 167}
await hass.services.async_call(
"light",
@ -1644,6 +1663,7 @@ async def test_light_service_call_color_conversion(
entity4.entity_id,
entity5.entity_id,
entity6.entity_id,
entity7.entity_id,
],
"brightness_pct": 50,
"xy_color": (0.1, 0.8),
@ -1664,6 +1684,8 @@ async def test_light_service_call_color_conversion(
assert data == {"brightness": 128, "rgbw_color": (0, 255, 22, 0)}
_, data = entity6.last_call("turn_on")
assert data == {"brightness": 128, "rgbww_color": (0, 255, 22, 0, 0)}
_, data = entity7.last_call("turn_on")
assert data == {"brightness": 128, "color_temp_kelvin": 8645, "color_temp": 115}
await hass.services.async_call(
"light",
@ -1677,6 +1699,7 @@ async def test_light_service_call_color_conversion(
entity4.entity_id,
entity5.entity_id,
entity6.entity_id,
entity7.entity_id,
],
"brightness_pct": 50,
"xy_color": (0.323, 0.329),
@ -1698,6 +1721,8 @@ async def test_light_service_call_color_conversion(
_, data = entity6.last_call("turn_on")
# The midpoint the the white channels is warm, compensated by adding green + blue
assert data == {"brightness": 128, "rgbww_color": (0, 75, 140, 255, 255)}
_, data = entity7.last_call("turn_on")
assert data == {"brightness": 128, "color_temp_kelvin": 5962, "color_temp": 167}
await hass.services.async_call(
"light",
@ -1711,6 +1736,7 @@ async def test_light_service_call_color_conversion(
entity4.entity_id,
entity5.entity_id,
entity6.entity_id,
entity7.entity_id,
],
"brightness_pct": 50,
"rgbw_color": (128, 0, 0, 64),
@ -1732,6 +1758,8 @@ async def test_light_service_call_color_conversion(
_, data = entity6.last_call("turn_on")
# The midpoint the the white channels is warm, compensated by adding green + blue
assert data == {"brightness": 128, "rgbww_color": (128, 0, 30, 117, 117)}
_, data = entity7.last_call("turn_on")
assert data == {"brightness": 128, "color_temp_kelvin": 3011, "color_temp": 332}
await hass.services.async_call(
"light",
@ -1745,6 +1773,7 @@ async def test_light_service_call_color_conversion(
entity4.entity_id,
entity5.entity_id,
entity6.entity_id,
entity7.entity_id,
],
"brightness_pct": 50,
"rgbw_color": (255, 255, 255, 255),
@ -1766,6 +1795,8 @@ async def test_light_service_call_color_conversion(
_, data = entity6.last_call("turn_on")
# The midpoint the the white channels is warm, compensated by adding green + blue
assert data == {"brightness": 128, "rgbww_color": (0, 76, 141, 255, 255)}
_, data = entity7.last_call("turn_on")
assert data == {"brightness": 128, "color_temp_kelvin": 5962, "color_temp": 167}
await hass.services.async_call(
"light",
@ -1779,6 +1810,7 @@ async def test_light_service_call_color_conversion(
entity4.entity_id,
entity5.entity_id,
entity6.entity_id,
entity7.entity_id,
],
"brightness_pct": 50,
"rgbww_color": (128, 0, 0, 64, 32),
@ -1799,6 +1831,8 @@ async def test_light_service_call_color_conversion(
assert data == {"brightness": 128, "rgbw_color": (128, 9, 0, 33)}
_, data = entity6.last_call("turn_on")
assert data == {"brightness": 128, "rgbww_color": (128, 0, 0, 64, 32)}
_, data = entity7.last_call("turn_on")
assert data == {"brightness": 128, "color_temp_kelvin": 3845, "color_temp": 260}
await hass.services.async_call(
"light",
@ -1812,6 +1846,7 @@ async def test_light_service_call_color_conversion(
entity4.entity_id,
entity5.entity_id,
entity6.entity_id,
entity7.entity_id,
],
"brightness_pct": 50,
"rgbww_color": (255, 255, 255, 255, 255),
@ -1833,6 +1868,8 @@ async def test_light_service_call_color_conversion(
assert data == {"brightness": 128, "rgbw_color": (96, 44, 0, 255)}
_, data = entity6.last_call("turn_on")
assert data == {"brightness": 128, "rgbww_color": (255, 255, 255, 255, 255)}
_, data = entity7.last_call("turn_on")
assert data == {"brightness": 128, "color_temp_kelvin": 3451, "color_temp": 289}
async def test_light_service_call_color_conversion_named_tuple(

View File

@ -270,6 +270,15 @@ def test_color_rgbw_to_rgb() -> None:
assert color_util.color_rgbw_to_rgb(0, 0, 0, 127) == (127, 127, 127)
def test_color_xy_to_temperature() -> None:
"""Test color_xy_to_temperature."""
assert color_util.color_xy_to_temperature(0.5119, 0.4147) == 2136
assert color_util.color_xy_to_temperature(0.368, 0.3686) == 4302
assert color_util.color_xy_to_temperature(0.4448, 0.4066) == 2893
assert color_util.color_xy_to_temperature(0.1, 0.8) == 8645
assert color_util.color_xy_to_temperature(0.5, 0.4) == 2140
def test_color_rgb_to_hex() -> None:
"""Test color_rgb_to_hex."""
assert color_util.color_rgb_to_hex(255, 255, 255) == "ffffff"