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
Erik Montnemery 2022-05-27 15:38:22 +02:00 committed by GitHub
parent 35bc6900ea
commit 5ca82b2d33
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 86 additions and 48 deletions

View File

@ -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."""

View File

@ -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

View File

@ -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(