Fix ESPHome color temperature precision for light entities (#91424)

Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
Daniel Kent 2023-06-23 09:08:28 -04:00 committed by GitHub
parent 91611bbd3b
commit 983ff10541
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 119 additions and 11 deletions

View File

@ -13,7 +13,7 @@ from aioesphomeapi import (
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
ATTR_COLOR_TEMP,
ATTR_COLOR_TEMP_KELVIN,
ATTR_EFFECT,
ATTR_FLASH,
ATTR_RGB_COLOR,
@ -98,6 +98,20 @@ _COLOR_MODE_MAPPING = {
}
def _mired_to_kelvin(mired_temperature: float) -> int:
"""Convert absolute mired shift to degrees kelvin.
This function rounds the converted value instead of flooring the value as
is done in homeassistant.util.color.color_temperature_mired_to_kelvin().
If the value of mired_temperature is less than or equal to zero, return
the original value to avoid a divide by zero.
"""
if mired_temperature <= 0:
return round(mired_temperature)
return round(1000000 / mired_temperature)
def _color_mode_to_ha(mode: int) -> str:
"""Convert an esphome color mode to a HA color mode constant.
@ -198,8 +212,9 @@ class EsphomeLight(EsphomeEntity[LightInfo, LightState], LightEntity):
# need to convert cw+ww part to white+color_temp
white = data["white"] = max(cw, ww)
if white != 0:
min_ct = self.min_mireds
max_ct = self.max_mireds
static_info = self._static_info
min_ct = static_info.min_mireds
max_ct = static_info.max_mireds
ct_ratio = ww / (cw + ww)
data["color_temperature"] = min_ct + ct_ratio * (max_ct - min_ct)
color_modes = _filter_color_modes(
@ -216,8 +231,9 @@ class EsphomeLight(EsphomeEntity[LightInfo, LightState], LightEntity):
if (transition := kwargs.get(ATTR_TRANSITION)) is not None:
data["transition_length"] = transition
if (color_temp := kwargs.get(ATTR_COLOR_TEMP)) is not None:
data["color_temperature"] = color_temp
if (color_temp_k := kwargs.get(ATTR_COLOR_TEMP_KELVIN)) is not None:
# Do not use kelvin_to_mired here to prevent precision loss
data["color_temperature"] = 1000000.0 / color_temp_k
if _filter_color_modes(color_modes, LightColorCapability.COLOR_TEMPERATURE):
color_modes = _filter_color_modes(
color_modes, LightColorCapability.COLOR_TEMPERATURE
@ -349,6 +365,12 @@ class EsphomeLight(EsphomeEntity[LightInfo, LightState], LightEntity):
"""Return the CT color value in mireds."""
return round(self._state.color_temperature)
@property
@esphome_state_property
def color_temp_kelvin(self) -> int:
"""Return the CT color value in Kelvin."""
return _mired_to_kelvin(self._state.color_temperature)
@property
@esphome_state_property
def effect(self) -> str | None:
@ -385,3 +407,6 @@ class EsphomeLight(EsphomeEntity[LightInfo, LightState], LightEntity):
self._attr_effect_list = static_info.effects
self._attr_min_mireds = round(static_info.min_mireds)
self._attr_max_mireds = round(static_info.max_mireds)
if ColorMode.COLOR_TEMP in supported:
self._attr_min_color_temp_kelvin = _mired_to_kelvin(static_info.max_mireds)
self._attr_max_color_temp_kelvin = _mired_to_kelvin(static_info.min_mireds)

View File

@ -76,8 +76,8 @@ async def test_light_color_temp(
key=1,
name="my light",
unique_id="my_light",
min_mireds=153,
max_mireds=400,
min_mireds=153.846161,
max_mireds=370.370361,
supported_color_modes=[
LightColorCapability.COLOR_TEMPERATURE
| LightColorCapability.ON_OFF
@ -90,7 +90,7 @@ async def test_light_color_temp(
key=1,
state=True,
brightness=100,
color_temperature=153,
color_temperature=153.846161,
color_mode=LightColorCapability.COLOR_TEMPERATURE,
)
]
@ -106,10 +106,93 @@ async def test_light_color_temp(
assert state.state == STATE_ON
attributes = state.attributes
assert attributes[ATTR_MAX_MIREDS] == 400
assert attributes[ATTR_MIN_MIREDS] == 153
assert attributes[ATTR_MIN_COLOR_TEMP_KELVIN] == 2500
assert attributes[ATTR_MAX_COLOR_TEMP_KELVIN] == 6535
assert attributes[ATTR_MAX_MIREDS] == 370
assert attributes[ATTR_MIN_COLOR_TEMP_KELVIN] == 2700
assert attributes[ATTR_MAX_COLOR_TEMP_KELVIN] == 6500
await hass.services.async_call(
LIGHT_DOMAIN,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: "light.test_my_light"},
blocking=True,
)
mock_client.light_command.assert_has_calls(
[
call(
key=1,
state=True,
color_mode=LightColorCapability.COLOR_TEMPERATURE
| LightColorCapability.ON_OFF
| LightColorCapability.BRIGHTNESS,
)
]
)
mock_client.light_command.reset_mock()
await hass.services.async_call(
LIGHT_DOMAIN,
SERVICE_TURN_OFF,
{ATTR_ENTITY_ID: "light.test_my_light"},
blocking=True,
)
mock_client.light_command.assert_has_calls([call(key=1, state=False)])
mock_client.light_command.reset_mock()
async def test_light_color_temp_legacy(
hass: HomeAssistant, mock_client: APIClient, mock_generic_device_entry
) -> None:
"""Test a legacy light entity that does supports color temp."""
mock_client.api_version = APIVersion(1, 7)
entity_info = [
LightInfo(
object_id="mylight",
key=1,
name="my light",
unique_id="my_light",
min_mireds=153.846161,
max_mireds=370.370361,
supported_color_modes=[
LightColorCapability.COLOR_TEMPERATURE
| LightColorCapability.ON_OFF
| LightColorCapability.BRIGHTNESS
],
legacy_supports_brightness=True,
legacy_supports_color_temperature=True,
)
]
states = [
LightState(
key=1,
state=True,
brightness=100,
red=1,
green=1,
blue=1,
white=1,
cold_white=1,
color_temperature=153.846161,
color_mode=19,
)
]
user_service = []
await mock_generic_device_entry(
mock_client=mock_client,
entity_info=entity_info,
user_service=user_service,
states=states,
)
state = hass.states.get("light.test_my_light")
assert state is not None
assert state.state == STATE_ON
attributes = state.attributes
assert attributes[ATTR_MIN_MIREDS] == 153
assert attributes[ATTR_MAX_MIREDS] == 370
assert attributes[ATTR_MIN_COLOR_TEMP_KELVIN] == 2700
assert attributes[ATTR_MAX_COLOR_TEMP_KELVIN] == 6500
await hass.services.async_call(
LIGHT_DOMAIN,
SERVICE_TURN_ON,