Small cleanups to ESPHome light platform (#107003)

- Remove unreachable code
- Cache filtering when possible
- Add missing coverage
This commit is contained in:
J. Nick Koston 2024-01-03 14:53:48 -10:00 committed by GitHub
parent d535409349
commit 8d2ddb6a04
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 230 additions and 17 deletions

View File

@ -1,7 +1,8 @@
"""Support for ESPHome lights."""
from __future__ import annotations
from typing import Any, cast
from functools import lru_cache
from typing import TYPE_CHECKING, Any, cast
from aioesphomeapi import (
APIVersion,
@ -111,6 +112,7 @@ def _mired_to_kelvin(mired_temperature: float) -> int:
return round(1000000 / mired_temperature)
@lru_cache
def _color_mode_to_ha(mode: int) -> str:
"""Convert an esphome color mode to a HA color mode constant.
@ -134,20 +136,34 @@ def _color_mode_to_ha(mode: int) -> str:
return candidates[-1][0]
@lru_cache
def _filter_color_modes(
supported: list[int], features: LightColorCapability
) -> list[int]:
) -> tuple[int, ...]:
"""Filter the given supported color modes.
Excluding all values that don't have the requested features.
"""
return [mode for mode in supported if (mode & features) == features]
features_value = features.value
return tuple(
mode for mode in supported if (mode & features_value) == features_value
)
@lru_cache
def _least_complex_color_mode(color_modes: tuple[int, ...]) -> int:
"""Return the color mode with the least complexity."""
# popcount with bin() function because it appears
# to be the best way: https://stackoverflow.com/a/9831671
color_modes_list = list(color_modes)
color_modes_list.sort(key=lambda mode: bin(mode).count("1"))
return color_modes_list[0]
class EsphomeLight(EsphomeEntity[LightInfo, LightState], LightEntity):
"""A light implementation for ESPHome."""
_native_supported_color_modes: list[int]
_native_supported_color_modes: tuple[int, ...]
_supports_color_mode = False
@property
@ -231,10 +247,10 @@ class EsphomeLight(EsphomeEntity[LightInfo, LightState], LightEntity):
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
)
if color_temp_modes := _filter_color_modes(
color_modes, LightColorCapability.COLOR_TEMPERATURE
):
color_modes = color_temp_modes
else:
color_modes = _filter_color_modes(
color_modes, LightColorCapability.COLD_WARM_WHITE
@ -267,10 +283,7 @@ class EsphomeLight(EsphomeEntity[LightInfo, LightState], LightEntity):
else:
# otherwise try the color mode with the least complexity
# (fewest capabilities set)
# popcount with bin() function because it appears
# to be the best way: https://stackoverflow.com/a/9831671
color_modes.sort(key=lambda mode: bin(mode).count("1"))
data["color_mode"] = color_modes[0]
data["color_mode"] = _least_complex_color_mode(color_modes)
await self._client.light_command(**data)
@ -294,9 +307,10 @@ class EsphomeLight(EsphomeEntity[LightInfo, LightState], LightEntity):
def color_mode(self) -> str | None:
"""Return the color mode of the light."""
if not self._supports_color_mode:
if not (supported := self.supported_color_modes):
return None
return next(iter(supported))
supported_color_modes = self.supported_color_modes
if TYPE_CHECKING:
assert supported_color_modes is not None
return next(iter(supported_color_modes))
return _color_mode_to_ha(self._state.color_mode)
@ -374,8 +388,8 @@ class EsphomeLight(EsphomeEntity[LightInfo, LightState], LightEntity):
super()._on_static_info_update(static_info)
static_info = self._static_info
self._supports_color_mode = self._api_version >= APIVersion(1, 6)
self._native_supported_color_modes = static_info.supported_color_modes_compat(
self._api_version
self._native_supported_color_modes = tuple(
static_info.supported_color_modes_compat(self._api_version)
)
flags = LightEntityFeature.FLASH

View File

@ -29,6 +29,7 @@ from homeassistant.components.light import (
ATTR_RGBWW_COLOR,
ATTR_SUPPORTED_COLOR_MODES,
ATTR_TRANSITION,
ATTR_WHITE,
DOMAIN as LIGHT_DOMAIN,
FLASH_LONG,
FLASH_SHORT,
@ -317,6 +318,68 @@ async def test_light_legacy_white_converted_to_brightness(
mock_client.light_command.reset_mock()
async def test_light_legacy_white_with_rgb(
hass: HomeAssistant, mock_client: APIClient, mock_generic_device_entry
) -> None:
"""Test a generic light entity with rgb and white."""
mock_client.api_version = APIVersion(1, 7)
color_mode = (
LightColorCapability.ON_OFF
| LightColorCapability.BRIGHTNESS
| LightColorCapability.WHITE
)
color_mode_2 = (
LightColorCapability.ON_OFF
| LightColorCapability.BRIGHTNESS
| LightColorCapability.RGB
)
entity_info = [
LightInfo(
object_id="mylight",
key=1,
name="my light",
unique_id="my_light",
min_mireds=153,
max_mireds=400,
supported_color_modes=[color_mode, color_mode_2],
)
]
states = [LightState(key=1, state=True, brightness=100)]
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_mylight")
assert state is not None
assert state.state == STATE_ON
assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == [
ColorMode.RGB,
ColorMode.WHITE,
]
await hass.services.async_call(
LIGHT_DOMAIN,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: "light.test_mylight", ATTR_WHITE: 60},
blocking=True,
)
mock_client.light_command.assert_has_calls(
[
call(
key=1,
state=True,
brightness=pytest.approx(0.23529411764705882),
white=1.0,
color_mode=color_mode,
)
]
)
mock_client.light_command.reset_mock()
async def test_light_brightness_on_off_with_unknown_color_mode(
hass: HomeAssistant, mock_client: APIClient, mock_generic_device_entry
) -> None:
@ -1676,3 +1739,139 @@ async def test_light_effects(
]
)
mock_client.light_command.reset_mock()
async def test_only_cold_warm_white_support(
hass: HomeAssistant, mock_client: APIClient, mock_generic_device_entry
) -> None:
"""Test a generic light entity with only cold warm white support."""
mock_client.api_version = APIVersion(1, 7)
color_modes = (
LightColorCapability.COLD_WARM_WHITE
| LightColorCapability.ON_OFF
| LightColorCapability.BRIGHTNESS
)
entity_info = [
LightInfo(
object_id="mylight",
key=1,
name="my light",
unique_id="my_light",
min_mireds=153,
max_mireds=400,
supported_color_modes=[color_modes],
)
]
states = [
LightState(
key=1,
state=True,
color_brightness=1,
brightness=100,
red=1,
green=1,
blue=1,
warm_white=1,
cold_white=1,
color_mode=color_modes,
)
]
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_mylight")
assert state is not None
assert state.state == STATE_ON
assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == [ColorMode.COLOR_TEMP]
assert state.attributes[ATTR_COLOR_MODE] == ColorMode.COLOR_TEMP
assert state.attributes[ATTR_COLOR_TEMP_KELVIN] == 0
await hass.services.async_call(
LIGHT_DOMAIN,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: "light.test_mylight"},
blocking=True,
)
mock_client.light_command.assert_has_calls(
[call(key=1, state=True, color_mode=color_modes)]
)
mock_client.light_command.reset_mock()
await hass.services.async_call(
LIGHT_DOMAIN,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: "light.test_mylight", ATTR_BRIGHTNESS: 127},
blocking=True,
)
mock_client.light_command.assert_has_calls(
[
call(
key=1,
state=True,
color_mode=color_modes,
brightness=pytest.approx(0.4980392156862745),
)
]
)
mock_client.light_command.reset_mock()
await hass.services.async_call(
LIGHT_DOMAIN,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: "light.test_mylight", ATTR_COLOR_TEMP_KELVIN: 2500},
blocking=True,
)
mock_client.light_command.assert_has_calls(
[
call(
key=1,
state=True,
color_mode=color_modes,
color_temperature=400.0,
)
]
)
mock_client.light_command.reset_mock()
async def test_light_no_color_modes(
hass: HomeAssistant, mock_client: APIClient, mock_generic_device_entry
) -> None:
"""Test a generic light entity with no color modes."""
mock_client.api_version = APIVersion(1, 7)
color_mode = 0
entity_info = [
LightInfo(
object_id="mylight",
key=1,
name="my light",
unique_id="my_light",
min_mireds=153,
max_mireds=400,
supported_color_modes=[color_mode],
)
]
states = [LightState(key=1, state=True, brightness=100)]
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_mylight")
assert state is not None
assert state.state == STATE_ON
assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == [ColorMode.UNKNOWN]
await hass.services.async_call(
LIGHT_DOMAIN,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: "light.test_mylight"},
blocking=True,
)
mock_client.light_command.assert_has_calls([call(key=1, state=True, color_mode=0)])
mock_client.light_command.reset_mock()