mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 19:27:45 +00:00
Small cleanups to ESPHome light platform (#107003)
- Remove unreachable code - Cache filtering when possible - Add missing coverage
This commit is contained in:
parent
d535409349
commit
8d2ddb6a04
@ -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
|
||||||
|
|
||||||
|
@ -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()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user