Migrate zha light to color_mode (#70970)
* Migrate zha light to color_mode * Fix restoring color mode * Correct set operations * Derive color mode from group members * Add color mode to color channel * use Zigpy color mode enum Co-authored-by: David Mulcahey <david.mulcahey@me.com>pull/72601/head
parent
35bc6900ea
commit
5ca82b2d33
|
@ -36,6 +36,7 @@ class ColorChannel(ZigbeeChannel):
|
|||
MAX_MIREDS: int = 500
|
||||
MIN_MIREDS: int = 153
|
||||
ZCL_INIT_ATTRS = {
|
||||
"color_mode": False,
|
||||
"color_temp_physical_min": True,
|
||||
"color_temp_physical_max": True,
|
||||
"color_capabilities": True,
|
||||
|
@ -51,6 +52,11 @@ class ColorChannel(ZigbeeChannel):
|
|||
return self.CAPABILITIES_COLOR_XY | self.CAPABILITIES_COLOR_TEMP
|
||||
return self.CAPABILITIES_COLOR_XY
|
||||
|
||||
@property
|
||||
def color_mode(self) -> int | None:
|
||||
"""Return cached value of the color_mode attribute."""
|
||||
return self.cluster.get("color_mode")
|
||||
|
||||
@property
|
||||
def color_loop_active(self) -> int | None:
|
||||
"""Return cached value of the color_loop_active attribute."""
|
||||
|
|
|
@ -3,12 +3,11 @@ from __future__ import annotations
|
|||
|
||||
from collections import Counter
|
||||
from datetime import timedelta
|
||||
import enum
|
||||
import functools
|
||||
import itertools
|
||||
import logging
|
||||
import random
|
||||
from typing import Any
|
||||
from typing import Any, cast
|
||||
|
||||
from zigpy.zcl.clusters.general import Identify, LevelControl, OnOff
|
||||
from zigpy.zcl.clusters.lighting import Color
|
||||
|
@ -17,16 +16,17 @@ from zigpy.zcl.foundation import Status
|
|||
from homeassistant.components import light
|
||||
from homeassistant.components.light import (
|
||||
ATTR_BRIGHTNESS,
|
||||
ATTR_COLOR_MODE,
|
||||
ATTR_COLOR_TEMP,
|
||||
ATTR_EFFECT,
|
||||
ATTR_EFFECT_LIST,
|
||||
ATTR_HS_COLOR,
|
||||
ATTR_MAX_MIREDS,
|
||||
ATTR_MIN_MIREDS,
|
||||
SUPPORT_BRIGHTNESS,
|
||||
SUPPORT_COLOR,
|
||||
SUPPORT_COLOR_TEMP,
|
||||
LightEntityFeature,
|
||||
ATTR_SUPPORTED_COLOR_MODES,
|
||||
ColorMode,
|
||||
brightness_supported,
|
||||
filter_supported_color_modes,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
|
@ -86,24 +86,14 @@ GROUP_MATCH = functools.partial(ZHA_ENTITIES.group_match, Platform.LIGHT)
|
|||
PARALLEL_UPDATES = 0
|
||||
SIGNAL_LIGHT_GROUP_STATE_CHANGED = "zha_light_group_state_changed"
|
||||
|
||||
COLOR_MODES_GROUP_LIGHT = {ColorMode.COLOR_TEMP, ColorMode.HS}
|
||||
SUPPORT_GROUP_LIGHT = (
|
||||
SUPPORT_BRIGHTNESS
|
||||
| SUPPORT_COLOR_TEMP
|
||||
| LightEntityFeature.EFFECT
|
||||
| LightEntityFeature.FLASH
|
||||
| SUPPORT_COLOR
|
||||
| LightEntityFeature.TRANSITION
|
||||
light.LightEntityFeature.EFFECT
|
||||
| light.LightEntityFeature.FLASH
|
||||
| light.LightEntityFeature.TRANSITION
|
||||
)
|
||||
|
||||
|
||||
class LightColorMode(enum.IntEnum):
|
||||
"""ZCL light color mode enum."""
|
||||
|
||||
HS_COLOR = 0x00
|
||||
XY_COLOR = 0x01
|
||||
COLOR_TEMP = 0x02
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
|
@ -146,6 +136,7 @@ class BaseLight(LogMixin, light.LightEntity):
|
|||
self._color_channel = None
|
||||
self._identify_channel = None
|
||||
self._default_transition = None
|
||||
self._color_mode = ColorMode.UNKNOWN # Set by sub classes
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> dict[str, Any]:
|
||||
|
@ -160,6 +151,11 @@ class BaseLight(LogMixin, light.LightEntity):
|
|||
return False
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def color_mode(self):
|
||||
"""Return the color mode of this light."""
|
||||
return self._color_mode
|
||||
|
||||
@property
|
||||
def brightness(self):
|
||||
"""Return the brightness of this light."""
|
||||
|
@ -230,9 +226,9 @@ class BaseLight(LogMixin, light.LightEntity):
|
|||
brightness = self._off_brightness
|
||||
|
||||
t_log = {}
|
||||
if (
|
||||
brightness is not None or transition
|
||||
) and self._supported_features & light.SUPPORT_BRIGHTNESS:
|
||||
if (brightness is not None or transition) and brightness_supported(
|
||||
self._attr_supported_color_modes
|
||||
):
|
||||
if brightness is not None:
|
||||
level = min(254, brightness)
|
||||
else:
|
||||
|
@ -257,10 +253,7 @@ class BaseLight(LogMixin, light.LightEntity):
|
|||
self.debug("turned on: %s", t_log)
|
||||
return
|
||||
self._state = True
|
||||
if (
|
||||
light.ATTR_COLOR_TEMP in kwargs
|
||||
and self.supported_features & light.SUPPORT_COLOR_TEMP
|
||||
):
|
||||
if light.ATTR_COLOR_TEMP in kwargs:
|
||||
temperature = kwargs[light.ATTR_COLOR_TEMP]
|
||||
result = await self._color_channel.move_to_color_temp(temperature, duration)
|
||||
t_log["move_to_color_temp"] = result
|
||||
|
@ -270,10 +263,7 @@ class BaseLight(LogMixin, light.LightEntity):
|
|||
self._color_temp = temperature
|
||||
self._hs_color = None
|
||||
|
||||
if (
|
||||
light.ATTR_HS_COLOR in kwargs
|
||||
and self.supported_features & light.SUPPORT_COLOR
|
||||
):
|
||||
if light.ATTR_HS_COLOR in kwargs:
|
||||
hs_color = kwargs[light.ATTR_HS_COLOR]
|
||||
xy_color = color_util.color_hs_to_xy(*hs_color)
|
||||
result = await self._color_channel.move_to_color(
|
||||
|
@ -286,10 +276,7 @@ class BaseLight(LogMixin, light.LightEntity):
|
|||
self._hs_color = hs_color
|
||||
self._color_temp = None
|
||||
|
||||
if (
|
||||
effect == light.EFFECT_COLORLOOP
|
||||
and self.supported_features & light.LightEntityFeature.EFFECT
|
||||
):
|
||||
if effect == light.EFFECT_COLORLOOP:
|
||||
result = await self._color_channel.color_loop_set(
|
||||
UPDATE_COLORLOOP_ACTION
|
||||
| UPDATE_COLORLOOP_DIRECTION
|
||||
|
@ -302,9 +289,7 @@ class BaseLight(LogMixin, light.LightEntity):
|
|||
t_log["color_loop_set"] = result
|
||||
self._effect = light.EFFECT_COLORLOOP
|
||||
elif (
|
||||
self._effect == light.EFFECT_COLORLOOP
|
||||
and effect != light.EFFECT_COLORLOOP
|
||||
and self.supported_features & light.LightEntityFeature.EFFECT
|
||||
self._effect == light.EFFECT_COLORLOOP and effect != light.EFFECT_COLORLOOP
|
||||
):
|
||||
result = await self._color_channel.color_loop_set(
|
||||
UPDATE_COLORLOOP_ACTION,
|
||||
|
@ -316,10 +301,7 @@ class BaseLight(LogMixin, light.LightEntity):
|
|||
t_log["color_loop_set"] = result
|
||||
self._effect = None
|
||||
|
||||
if (
|
||||
flash is not None
|
||||
and self._supported_features & light.LightEntityFeature.FLASH
|
||||
):
|
||||
if flash is not None:
|
||||
result = await self._identify_channel.trigger_effect(
|
||||
FLASH_EFFECTS[flash], EFFECT_DEFAULT_VARIANT
|
||||
)
|
||||
|
@ -332,7 +314,7 @@ class BaseLight(LogMixin, light.LightEntity):
|
|||
async def async_turn_off(self, **kwargs):
|
||||
"""Turn the entity off."""
|
||||
duration = kwargs.get(light.ATTR_TRANSITION)
|
||||
supports_level = self.supported_features & light.SUPPORT_BRIGHTNESS
|
||||
supports_level = brightness_supported(self._attr_supported_color_modes)
|
||||
|
||||
if duration and supports_level:
|
||||
result = await self._level_channel.move_to_level_with_on_off(
|
||||
|
@ -356,6 +338,7 @@ class BaseLight(LogMixin, light.LightEntity):
|
|||
class Light(BaseLight, ZhaEntity):
|
||||
"""Representation of a ZHA or ZLL light."""
|
||||
|
||||
_attr_supported_color_modes: set(ColorMode)
|
||||
_REFRESH_INTERVAL = (45, 75)
|
||||
|
||||
def __init__(self, unique_id, zha_device: ZhaDeviceType, channels, **kwargs):
|
||||
|
@ -372,19 +355,20 @@ class Light(BaseLight, ZhaEntity):
|
|||
self._cancel_refresh_handle = None
|
||||
effect_list = []
|
||||
|
||||
self._attr_supported_color_modes = {ColorMode.ONOFF}
|
||||
if self._level_channel:
|
||||
self._supported_features |= light.SUPPORT_BRIGHTNESS
|
||||
self._attr_supported_color_modes.add(ColorMode.BRIGHTNESS)
|
||||
self._supported_features |= light.LightEntityFeature.TRANSITION
|
||||
self._brightness = self._level_channel.current_level
|
||||
|
||||
if self._color_channel:
|
||||
color_capabilities = self._color_channel.color_capabilities
|
||||
if color_capabilities & CAPABILITIES_COLOR_TEMP:
|
||||
self._supported_features |= light.SUPPORT_COLOR_TEMP
|
||||
self._attr_supported_color_modes.add(ColorMode.COLOR_TEMP)
|
||||
self._color_temp = self._color_channel.color_temperature
|
||||
|
||||
if color_capabilities & CAPABILITIES_COLOR_XY:
|
||||
self._supported_features |= light.SUPPORT_COLOR
|
||||
self._attr_supported_color_modes.add(ColorMode.HS)
|
||||
curr_x = self._color_channel.current_x
|
||||
curr_y = self._color_channel.current_y
|
||||
if curr_x is not None and curr_y is not None:
|
||||
|
@ -399,6 +383,16 @@ class Light(BaseLight, ZhaEntity):
|
|||
effect_list.append(light.EFFECT_COLORLOOP)
|
||||
if self._color_channel.color_loop_active == 1:
|
||||
self._effect = light.EFFECT_COLORLOOP
|
||||
self._attr_supported_color_modes = filter_supported_color_modes(
|
||||
self._attr_supported_color_modes
|
||||
)
|
||||
if len(self._attr_supported_color_modes) == 1:
|
||||
self._color_mode = next(iter(self._attr_supported_color_modes))
|
||||
else: # Light supports color_temp + hs, determine which mode the light is in
|
||||
if self._color_channel.color_mode == Color.ColorMode.Color_temperature:
|
||||
self._color_mode = ColorMode.COLOR_TEMP
|
||||
else:
|
||||
self._color_mode = ColorMode.HS
|
||||
|
||||
if self._identify_channel:
|
||||
self._supported_features |= light.LightEntityFeature.FLASH
|
||||
|
@ -455,6 +449,8 @@ class Light(BaseLight, ZhaEntity):
|
|||
self._brightness = last_state.attributes["brightness"]
|
||||
if "off_brightness" in last_state.attributes:
|
||||
self._off_brightness = last_state.attributes["off_brightness"]
|
||||
if "color_mode" in last_state.attributes:
|
||||
self._color_mode = ColorMode(last_state.attributes["color_mode"])
|
||||
if "color_temp" in last_state.attributes:
|
||||
self._color_temp = last_state.attributes["color_temp"]
|
||||
if "hs_color" in last_state.attributes:
|
||||
|
@ -493,12 +489,14 @@ class Light(BaseLight, ZhaEntity):
|
|||
)
|
||||
|
||||
if (color_mode := results.get("color_mode")) is not None:
|
||||
if color_mode == LightColorMode.COLOR_TEMP:
|
||||
if color_mode == Color.ColorMode.Color_temperature:
|
||||
self._color_mode = ColorMode.COLOR_TEMP
|
||||
color_temp = results.get("color_temperature")
|
||||
if color_temp is not None and color_mode:
|
||||
self._color_temp = color_temp
|
||||
self._hs_color = None
|
||||
else:
|
||||
self._color_mode = ColorMode.HS
|
||||
color_x = results.get("current_x")
|
||||
color_y = results.get("current_y")
|
||||
if color_x is not None and color_y is not None:
|
||||
|
@ -573,6 +571,7 @@ class LightGroup(BaseLight, ZhaGroupEntity):
|
|||
CONF_DEFAULT_LIGHT_TRANSITION,
|
||||
0,
|
||||
)
|
||||
self._color_mode = None
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Run when about to be added to hass."""
|
||||
|
@ -633,6 +632,29 @@ class LightGroup(BaseLight, ZhaGroupEntity):
|
|||
effects_count = Counter(itertools.chain(all_effects))
|
||||
self._effect = effects_count.most_common(1)[0][0]
|
||||
|
||||
self._attr_color_mode = None
|
||||
all_color_modes = list(
|
||||
helpers.find_state_attributes(on_states, ATTR_COLOR_MODE)
|
||||
)
|
||||
if all_color_modes:
|
||||
# Report the most common color mode, select brightness and onoff last
|
||||
color_mode_count = Counter(itertools.chain(all_color_modes))
|
||||
if ColorMode.ONOFF in color_mode_count:
|
||||
color_mode_count[ColorMode.ONOFF] = -1
|
||||
if ColorMode.BRIGHTNESS in color_mode_count:
|
||||
color_mode_count[ColorMode.BRIGHTNESS] = 0
|
||||
self._attr_color_mode = color_mode_count.most_common(1)[0][0]
|
||||
|
||||
self._attr_supported_color_modes = None
|
||||
all_supported_color_modes = list(
|
||||
helpers.find_state_attributes(states, ATTR_SUPPORTED_COLOR_MODES)
|
||||
)
|
||||
if all_supported_color_modes:
|
||||
# Merge all color modes.
|
||||
self._attr_supported_color_modes = cast(
|
||||
set[str], set().union(*all_supported_color_modes)
|
||||
)
|
||||
|
||||
self._supported_features = 0
|
||||
for support in helpers.find_state_attributes(states, ATTR_SUPPORTED_FEATURES):
|
||||
# Merge supported features by emulating support for every feature
|
||||
|
|
|
@ -12,6 +12,7 @@ from homeassistant.components.light import (
|
|||
DOMAIN as LIGHT_DOMAIN,
|
||||
FLASH_LONG,
|
||||
FLASH_SHORT,
|
||||
ColorMode,
|
||||
)
|
||||
from homeassistant.components.zha.core.group import GroupMember
|
||||
from homeassistant.components.zha.light import FLASH_EFFECTS
|
||||
|
@ -580,7 +581,11 @@ async def test_zha_group_light_entity(
|
|||
await async_wait_for_updates(hass)
|
||||
|
||||
# test that the lights were created and are off
|
||||
assert hass.states.get(group_entity_id).state == STATE_OFF
|
||||
group_state = hass.states.get(group_entity_id)
|
||||
assert group_state.state == STATE_OFF
|
||||
assert group_state.attributes["supported_color_modes"] == [ColorMode.HS]
|
||||
# Light which is off has no color mode
|
||||
assert "color_mode" not in group_state.attributes
|
||||
|
||||
# test turning the lights on and off from the HA
|
||||
await async_test_on_off_from_hass(hass, group_cluster_on_off, group_entity_id)
|
||||
|
@ -603,6 +608,11 @@ async def test_zha_group_light_entity(
|
|||
await async_test_dimmer_from_light(
|
||||
hass, dev1_cluster_level, group_entity_id, 150, STATE_ON
|
||||
)
|
||||
# Check state
|
||||
group_state = hass.states.get(group_entity_id)
|
||||
assert group_state.state == STATE_ON
|
||||
assert group_state.attributes["supported_color_modes"] == [ColorMode.HS]
|
||||
assert group_state.attributes["color_mode"] == ColorMode.HS
|
||||
|
||||
# test long flashing the lights from the HA
|
||||
await async_test_flash_from_hass(
|
||||
|
|
Loading…
Reference in New Issue