Skip to content

Commit

Permalink
Fix ESPHome color temperature precision for light entities (home-assi…
Browse files Browse the repository at this point in the history
…stant#91424)

Co-authored-by: J. Nick Koston <nick@koston.org>
  • Loading branch information
danielkent-net and bdraco authored Jun 23, 2023
1 parent 91611bb commit 983ff10
Show file tree
Hide file tree
Showing 2 changed files with 119 additions and 11 deletions.
35 changes: 30 additions & 5 deletions homeassistant/components/esphome/light.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

from homeassistant.components.light import (
ATTR_BRIGHTNESS,
ATTR_COLOR_TEMP,
ATTR_COLOR_TEMP_KELVIN,
ATTR_EFFECT,
ATTR_FLASH,
ATTR_RGB_COLOR,
Expand Down Expand Up @@ -98,6 +98,20 @@ async def async_setup_entry(
}


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.
Expand Down Expand Up @@ -198,8 +212,9 @@ async def async_turn_on(self, **kwargs: Any) -> None:
# 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(
Expand All @@ -216,8 +231,9 @@ async def async_turn_on(self, **kwargs: Any) -> None:
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
Expand Down Expand Up @@ -349,6 +365,12 @@ def color_temp(self) -> int:
"""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:
Expand Down Expand Up @@ -385,3 +407,6 @@ def _on_static_info_update(self, static_info: EntityInfo) -> None:
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)
95 changes: 89 additions & 6 deletions tests/components/esphome/test_light.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
)
]
Expand All @@ -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,
Expand Down

0 comments on commit 983ff10

Please sign in to comment.