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

View File

@ -29,6 +29,7 @@ from homeassistant.components.light import (
ATTR_RGBWW_COLOR, ATTR_RGBWW_COLOR,
ATTR_SUPPORTED_COLOR_MODES, ATTR_SUPPORTED_COLOR_MODES,
ATTR_TRANSITION, ATTR_TRANSITION,
ATTR_WHITE,
DOMAIN as LIGHT_DOMAIN, DOMAIN as LIGHT_DOMAIN,
FLASH_LONG, FLASH_LONG,
FLASH_SHORT, FLASH_SHORT,
@ -317,6 +318,68 @@ async def test_light_legacy_white_converted_to_brightness(
mock_client.light_command.reset_mock() 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( async def test_light_brightness_on_off_with_unknown_color_mode(
hass: HomeAssistant, mock_client: APIClient, mock_generic_device_entry hass: HomeAssistant, mock_client: APIClient, mock_generic_device_entry
) -> None: ) -> None:
@ -1676,3 +1739,139 @@ async def test_light_effects(
] ]
) )
mock_client.light_command.reset_mock() 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()