From 573e966d74221641b13e3530fcf60240da6596be Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 28 Apr 2022 09:49:51 +0200 Subject: [PATCH] Migrate hue v1 light to color_mode (#69275) * Migrate hue v1 light to color_mode * Fix test * Correct filter_supported_color_modes + add test * Use ColorMode enum --- homeassistant/components/hue/v1/light.py | 80 ++++++++++++++++++---- homeassistant/components/light/__init__.py | 17 +++++ tests/components/hue/test_light_v1.py | 22 ++++++ tests/components/light/test_init.py | 43 +++++++++++- 4 files changed, 147 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/hue/v1/light.py b/homeassistant/components/hue/v1/light.py index 9cddb665006..eed8a35f705 100644 --- a/homeassistant/components/hue/v1/light.py +++ b/homeassistant/components/hue/v1/light.py @@ -20,13 +20,12 @@ from homeassistant.components.light import ( EFFECT_RANDOM, FLASH_LONG, FLASH_SHORT, - SUPPORT_BRIGHTNESS, - SUPPORT_COLOR, - SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_FLASH, SUPPORT_TRANSITION, + ColorMode, LightEntity, + filter_supported_color_modes, ) from homeassistant.core import callback from homeassistant.exceptions import PlatformNotReady @@ -60,10 +59,24 @@ SCAN_INTERVAL = timedelta(seconds=5) LOGGER = logging.getLogger(__name__) +COLOR_MODES_HUE_ON_OFF = {ColorMode.ONOFF} +COLOR_MODES_HUE_DIMMABLE = {ColorMode.BRIGHTNESS} +COLOR_MODES_HUE_COLOR_TEMP = {ColorMode.COLOR_TEMP} +COLOR_MODES_HUE_COLOR = {ColorMode.HS} +COLOR_MODES_HUE_EXTENDED = {ColorMode.COLOR_TEMP, ColorMode.HS} + +COLOR_MODES_HUE = { + "Extended color light": COLOR_MODES_HUE_EXTENDED, + "Color light": COLOR_MODES_HUE_COLOR, + "Dimmable light": COLOR_MODES_HUE_DIMMABLE, + "On/Off plug-in unit": COLOR_MODES_HUE_ON_OFF, + "Color temperature light": COLOR_MODES_HUE_COLOR_TEMP, +} + SUPPORT_HUE_ON_OFF = SUPPORT_FLASH | SUPPORT_TRANSITION -SUPPORT_HUE_DIMMABLE = SUPPORT_HUE_ON_OFF | SUPPORT_BRIGHTNESS -SUPPORT_HUE_COLOR_TEMP = SUPPORT_HUE_DIMMABLE | SUPPORT_COLOR_TEMP -SUPPORT_HUE_COLOR = SUPPORT_HUE_DIMMABLE | SUPPORT_EFFECT | SUPPORT_COLOR +SUPPORT_HUE_DIMMABLE = SUPPORT_HUE_ON_OFF +SUPPORT_HUE_COLOR_TEMP = SUPPORT_HUE_DIMMABLE +SUPPORT_HUE_COLOR = SUPPORT_HUE_DIMMABLE | SUPPORT_EFFECT SUPPORT_HUE_EXTENDED = SUPPORT_HUE_COLOR_TEMP | SUPPORT_HUE_COLOR SUPPORT_HUE = { @@ -96,17 +109,32 @@ def create_light(item_class, coordinator, bridge, is_group, rooms, api, item_id) api_item = api[item_id] if is_group: + supported_color_modes = set() supported_features = 0 for light_id in api_item.lights: if light_id not in bridge.api.lights: continue light = bridge.api.lights[light_id] supported_features |= SUPPORT_HUE.get(light.type, SUPPORT_HUE_EXTENDED) + supported_color_modes.update( + COLOR_MODES_HUE.get(light.type, COLOR_MODES_HUE_EXTENDED) + ) supported_features = supported_features or SUPPORT_HUE_EXTENDED + supported_color_modes = supported_color_modes or COLOR_MODES_HUE_EXTENDED + supported_color_modes = filter_supported_color_modes(supported_color_modes) else: + supported_color_modes = COLOR_MODES_HUE.get( + api_item.type, COLOR_MODES_HUE_EXTENDED + ) supported_features = SUPPORT_HUE.get(api_item.type, SUPPORT_HUE_EXTENDED) return item_class( - coordinator, bridge, is_group, api_item, supported_features, rooms + coordinator, + bridge, + is_group, + api_item, + supported_color_modes, + supported_features, + rooms, ) @@ -281,18 +309,34 @@ def hass_to_hue_brightness(value): class HueLight(CoordinatorEntity, LightEntity): """Representation of a Hue light.""" - def __init__(self, coordinator, bridge, is_group, light, supported_features, rooms): + def __init__( + self, + coordinator, + bridge, + is_group, + light, + supported_color_modes, + supported_features, + rooms, + ): """Initialize the light.""" super().__init__(coordinator) + self._attr_supported_color_modes = supported_color_modes + self._attr_supported_features = supported_features self.light = light self.bridge = bridge self.is_group = is_group - self._supported_features = supported_features self._rooms = rooms self.allow_unreachable = self.bridge.config_entry.options.get( CONF_ALLOW_UNREACHABLE, DEFAULT_ALLOW_UNREACHABLE ) + self._fixed_color_mode = None + if len(supported_color_modes) == 1: + self._fixed_color_mode = next(iter(supported_color_modes)) + else: + assert supported_color_modes == {ColorMode.COLOR_TEMP, ColorMode.HS} + if is_group: self.is_osram = False self.is_philips = False @@ -354,6 +398,19 @@ class HueLight(CoordinatorEntity, LightEntity): return hue_brightness_to_hass(bri) + @property + def color_mode(self) -> str: + """Return the color mode of the light.""" + if self._fixed_color_mode: + return self._fixed_color_mode + + # The light supports both hs/xy and white with adjustabe color_temperature + mode = self._color_mode + if mode in ("xy", "hs"): + return ColorMode.HS + + return ColorMode.COLOR_TEMP + @property def _color_mode(self): """Return the hue color mode.""" @@ -426,11 +483,6 @@ class HueLight(CoordinatorEntity, LightEntity): self.is_group or self.allow_unreachable or self.light.state["reachable"] ) - @property - def supported_features(self): - """Flag supported features.""" - return self._supported_features - @property def effect(self): """Return the current effect.""" diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index 4f3240d7b31..099f917bc46 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -116,6 +116,23 @@ COLOR_MODES_COLOR = { } +def filter_supported_color_modes(color_modes: Iterable[ColorMode]) -> set[ColorMode]: + """Filter the given color modes.""" + color_modes = set(color_modes) + if ( + not color_modes + or ColorMode.UNKNOWN in color_modes + or (ColorMode.WHITE in color_modes and not color_supported(color_modes)) + ): + raise HomeAssistantError + + if ColorMode.ONOFF in color_modes and len(color_modes) > 1: + color_modes.remove(ColorMode.ONOFF) + if ColorMode.BRIGHTNESS in color_modes and len(color_modes) > 1: + color_modes.remove(ColorMode.BRIGHTNESS) + return color_modes + + def valid_supported_color_modes( color_modes: Iterable[ColorMode | str], ) -> set[ColorMode | str]: diff --git a/tests/components/hue/test_light_v1.py b/tests/components/hue/test_light_v1.py index 8c82a544ede..5423e6bd799 100644 --- a/tests/components/hue/test_light_v1.py +++ b/tests/components/hue/test_light_v1.py @@ -7,6 +7,7 @@ import aiohue from homeassistant.components import hue from homeassistant.components.hue.const import CONF_ALLOW_HUE_GROUPS from homeassistant.components.hue.v1 import light as hue_light +from homeassistant.components.light import COLOR_MODE_COLOR_TEMP, COLOR_MODE_HS from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.util import color @@ -236,6 +237,11 @@ async def test_lights_color_mode(hass, mock_bridge_v1): assert lamp_1.attributes["brightness"] == 145 assert lamp_1.attributes["hs_color"] == (36.067, 69.804) assert "color_temp" not in lamp_1.attributes + assert lamp_1.attributes["color_mode"] == COLOR_MODE_HS + assert lamp_1.attributes["supported_color_modes"] == [ + COLOR_MODE_COLOR_TEMP, + COLOR_MODE_HS, + ] new_light1_on = LIGHT_1_ON.copy() new_light1_on["state"] = new_light1_on["state"].copy() @@ -256,6 +262,11 @@ async def test_lights_color_mode(hass, mock_bridge_v1): assert lamp_1.attributes["brightness"] == 145 assert lamp_1.attributes["color_temp"] == 467 assert "hs_color" in lamp_1.attributes + assert lamp_1.attributes["color_mode"] == COLOR_MODE_COLOR_TEMP + assert lamp_1.attributes["supported_color_modes"] == [ + COLOR_MODE_COLOR_TEMP, + COLOR_MODE_HS, + ] async def test_groups(hass, mock_bridge_v1): @@ -651,6 +662,7 @@ def test_available(): bridge=Mock(config_entry=Mock(options={"allow_unreachable": False})), coordinator=Mock(last_update_success=True), is_group=False, + supported_color_modes=hue_light.COLOR_MODES_HUE_EXTENDED, supported_features=hue_light.SUPPORT_HUE_EXTENDED, rooms={}, ) @@ -666,6 +678,7 @@ def test_available(): ), coordinator=Mock(last_update_success=True), is_group=False, + supported_color_modes=hue_light.COLOR_MODES_HUE_EXTENDED, supported_features=hue_light.SUPPORT_HUE_EXTENDED, rooms={}, bridge=Mock(config_entry=Mock(options={"allow_unreachable": True})), @@ -682,6 +695,7 @@ def test_available(): ), coordinator=Mock(last_update_success=True), is_group=True, + supported_color_modes=hue_light.COLOR_MODES_HUE_EXTENDED, supported_features=hue_light.SUPPORT_HUE_EXTENDED, rooms={}, bridge=Mock(config_entry=Mock(options={"allow_unreachable": False})), @@ -702,6 +716,7 @@ def test_hs_color(): coordinator=Mock(last_update_success=True), bridge=Mock(), is_group=False, + supported_color_modes=hue_light.COLOR_MODES_HUE_EXTENDED, supported_features=hue_light.SUPPORT_HUE_EXTENDED, rooms={}, ) @@ -718,6 +733,7 @@ def test_hs_color(): coordinator=Mock(last_update_success=True), bridge=Mock(), is_group=False, + supported_color_modes=hue_light.COLOR_MODES_HUE_EXTENDED, supported_features=hue_light.SUPPORT_HUE_EXTENDED, rooms={}, ) @@ -734,6 +750,7 @@ def test_hs_color(): coordinator=Mock(last_update_success=True), bridge=Mock(), is_group=False, + supported_color_modes=hue_light.COLOR_MODES_HUE_EXTENDED, supported_features=hue_light.SUPPORT_HUE_EXTENDED, rooms={}, ) @@ -910,15 +927,20 @@ async def test_group_features(hass, mock_bridge_v1): assert len(mock_bridge_v1.mock_requests) == 2 color_temp_feature = hue_light.SUPPORT_HUE["Color temperature light"] + color_temp_mode = sorted(hue_light.COLOR_MODES_HUE["Color temperature light"]) extended_color_feature = hue_light.SUPPORT_HUE["Extended color light"] + extended_color_mode = sorted(hue_light.COLOR_MODES_HUE["Extended color light"]) group_1 = hass.states.get("light.group_1") + assert group_1.attributes["supported_color_modes"] == color_temp_mode assert group_1.attributes["supported_features"] == color_temp_feature group_2 = hass.states.get("light.living_room") + assert group_2.attributes["supported_color_modes"] == extended_color_mode assert group_2.attributes["supported_features"] == extended_color_feature group_3 = hass.states.get("light.dining_room") + assert group_3.attributes["supported_color_modes"] == extended_color_mode assert group_3.attributes["supported_features"] == extended_color_feature entity_registry = er.async_get(hass) diff --git a/tests/components/light/test_init.py b/tests/components/light/test_init.py index 3a8efa5fd97..ae9b00baeaa 100644 --- a/tests/components/light/test_init.py +++ b/tests/components/light/test_init.py @@ -16,7 +16,7 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, ) -from homeassistant.exceptions import Unauthorized +from homeassistant.exceptions import HomeAssistantError, Unauthorized from homeassistant.setup import async_setup_component import homeassistant.util.color as color_util @@ -2417,3 +2417,44 @@ def test_valid_supported_color_modes(): supported = {light.ColorMode.BRIGHTNESS, light.ColorMode.COLOR_TEMP} with pytest.raises(vol.Error): light.valid_supported_color_modes(supported) + + +def test_filter_supported_color_modes(): + """Test filter_supported_color_modes.""" + supported = {light.ColorMode.HS} + assert light.filter_supported_color_modes(supported) == supported + + # Supported color modes must not be empty + supported = set() + with pytest.raises(HomeAssistantError): + light.filter_supported_color_modes(supported) + + # ColorMode.WHITE must be combined with a color mode supporting color + supported = {light.ColorMode.WHITE} + with pytest.raises(HomeAssistantError): + light.filter_supported_color_modes(supported) + + supported = {light.ColorMode.WHITE, light.ColorMode.COLOR_TEMP} + with pytest.raises(HomeAssistantError): + light.filter_supported_color_modes(supported) + + supported = {light.ColorMode.WHITE, light.ColorMode.HS} + assert light.filter_supported_color_modes(supported) == supported + + # ColorMode.ONOFF will be removed if combined with other modes + supported = {light.ColorMode.ONOFF} + assert light.filter_supported_color_modes(supported) == supported + + supported = {light.ColorMode.ONOFF, light.ColorMode.COLOR_TEMP} + assert light.filter_supported_color_modes(supported) == {light.ColorMode.COLOR_TEMP} + + # ColorMode.BRIGHTNESS will be removed if combined with other modes + supported = {light.ColorMode.BRIGHTNESS} + assert light.filter_supported_color_modes(supported) == supported + + supported = {light.ColorMode.BRIGHTNESS, light.ColorMode.COLOR_TEMP} + assert light.filter_supported_color_modes(supported) == {light.ColorMode.COLOR_TEMP} + + # ColorMode.BRIGHTNESS has priority over ColorMode.ONOFF + supported = {light.ColorMode.ONOFF, light.ColorMode.BRIGHTNESS} + assert light.filter_supported_color_modes(supported) == {light.ColorMode.BRIGHTNESS}