Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 66 additions & 14 deletions homeassistant/components/hue/v1/light.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,12 @@
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
Expand Down Expand Up @@ -60,10 +59,24 @@

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 = {
Expand Down Expand Up @@ -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,
)


Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -354,6 +398,19 @@ def brightness(self):

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."""
Expand Down Expand Up @@ -426,11 +483,6 @@ def available(self):
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."""
Expand Down
17 changes: 17 additions & 0 deletions homeassistant/components/light/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,23 @@ class ColorMode(StrEnum):
}


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]:
Expand Down
22 changes: 22 additions & 0 deletions tests/components/hue/test_light_v1.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
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

Expand Down Expand Up @@ -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()
Expand All @@ -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):
Expand Down Expand Up @@ -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={},
)
Expand All @@ -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})),
Expand All @@ -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})),
Expand All @@ -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={},
)
Expand All @@ -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={},
)
Expand All @@ -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={},
)
Expand Down Expand Up @@ -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)
Expand Down
43 changes: 42 additions & 1 deletion tests/components/light/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
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

Expand Down Expand Up @@ -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}