mirror of
https://github.com/home-assistant/core.git
synced 2025-07-20 03:37:07 +00:00
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>
This commit is contained in:
parent
35bc6900ea
commit
5ca82b2d33
@ -36,6 +36,7 @@ class ColorChannel(ZigbeeChannel):
|
|||||||
MAX_MIREDS: int = 500
|
MAX_MIREDS: int = 500
|
||||||
MIN_MIREDS: int = 153
|
MIN_MIREDS: int = 153
|
||||||
ZCL_INIT_ATTRS = {
|
ZCL_INIT_ATTRS = {
|
||||||
|
"color_mode": False,
|
||||||
"color_temp_physical_min": True,
|
"color_temp_physical_min": True,
|
||||||
"color_temp_physical_max": True,
|
"color_temp_physical_max": True,
|
||||||
"color_capabilities": 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 | self.CAPABILITIES_COLOR_TEMP
|
||||||
return self.CAPABILITIES_COLOR_XY
|
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
|
@property
|
||||||
def color_loop_active(self) -> int | None:
|
def color_loop_active(self) -> int | None:
|
||||||
"""Return cached value of the color_loop_active attribute."""
|
"""Return cached value of the color_loop_active attribute."""
|
||||||
|
@ -3,12 +3,11 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from collections import Counter
|
from collections import Counter
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
import enum
|
|
||||||
import functools
|
import functools
|
||||||
import itertools
|
import itertools
|
||||||
import logging
|
import logging
|
||||||
import random
|
import random
|
||||||
from typing import Any
|
from typing import Any, cast
|
||||||
|
|
||||||
from zigpy.zcl.clusters.general import Identify, LevelControl, OnOff
|
from zigpy.zcl.clusters.general import Identify, LevelControl, OnOff
|
||||||
from zigpy.zcl.clusters.lighting import Color
|
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 import light
|
||||||
from homeassistant.components.light import (
|
from homeassistant.components.light import (
|
||||||
ATTR_BRIGHTNESS,
|
ATTR_BRIGHTNESS,
|
||||||
|
ATTR_COLOR_MODE,
|
||||||
ATTR_COLOR_TEMP,
|
ATTR_COLOR_TEMP,
|
||||||
ATTR_EFFECT,
|
ATTR_EFFECT,
|
||||||
ATTR_EFFECT_LIST,
|
ATTR_EFFECT_LIST,
|
||||||
ATTR_HS_COLOR,
|
ATTR_HS_COLOR,
|
||||||
ATTR_MAX_MIREDS,
|
ATTR_MAX_MIREDS,
|
||||||
ATTR_MIN_MIREDS,
|
ATTR_MIN_MIREDS,
|
||||||
SUPPORT_BRIGHTNESS,
|
ATTR_SUPPORTED_COLOR_MODES,
|
||||||
SUPPORT_COLOR,
|
ColorMode,
|
||||||
SUPPORT_COLOR_TEMP,
|
brightness_supported,
|
||||||
LightEntityFeature,
|
filter_supported_color_modes,
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
@ -86,24 +86,14 @@ GROUP_MATCH = functools.partial(ZHA_ENTITIES.group_match, Platform.LIGHT)
|
|||||||
PARALLEL_UPDATES = 0
|
PARALLEL_UPDATES = 0
|
||||||
SIGNAL_LIGHT_GROUP_STATE_CHANGED = "zha_light_group_state_changed"
|
SIGNAL_LIGHT_GROUP_STATE_CHANGED = "zha_light_group_state_changed"
|
||||||
|
|
||||||
|
COLOR_MODES_GROUP_LIGHT = {ColorMode.COLOR_TEMP, ColorMode.HS}
|
||||||
SUPPORT_GROUP_LIGHT = (
|
SUPPORT_GROUP_LIGHT = (
|
||||||
SUPPORT_BRIGHTNESS
|
light.LightEntityFeature.EFFECT
|
||||||
| SUPPORT_COLOR_TEMP
|
| light.LightEntityFeature.FLASH
|
||||||
| LightEntityFeature.EFFECT
|
| light.LightEntityFeature.TRANSITION
|
||||||
| LightEntityFeature.FLASH
|
|
||||||
| SUPPORT_COLOR
|
|
||||||
| 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(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
config_entry: ConfigEntry,
|
config_entry: ConfigEntry,
|
||||||
@ -146,6 +136,7 @@ class BaseLight(LogMixin, light.LightEntity):
|
|||||||
self._color_channel = None
|
self._color_channel = None
|
||||||
self._identify_channel = None
|
self._identify_channel = None
|
||||||
self._default_transition = None
|
self._default_transition = None
|
||||||
|
self._color_mode = ColorMode.UNKNOWN # Set by sub classes
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def extra_state_attributes(self) -> dict[str, Any]:
|
def extra_state_attributes(self) -> dict[str, Any]:
|
||||||
@ -160,6 +151,11 @@ class BaseLight(LogMixin, light.LightEntity):
|
|||||||
return False
|
return False
|
||||||
return self._state
|
return self._state
|
||||||
|
|
||||||
|
@property
|
||||||
|
def color_mode(self):
|
||||||
|
"""Return the color mode of this light."""
|
||||||
|
return self._color_mode
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def brightness(self):
|
def brightness(self):
|
||||||
"""Return the brightness of this light."""
|
"""Return the brightness of this light."""
|
||||||
@ -230,9 +226,9 @@ class BaseLight(LogMixin, light.LightEntity):
|
|||||||
brightness = self._off_brightness
|
brightness = self._off_brightness
|
||||||
|
|
||||||
t_log = {}
|
t_log = {}
|
||||||
if (
|
if (brightness is not None or transition) and brightness_supported(
|
||||||
brightness is not None or transition
|
self._attr_supported_color_modes
|
||||||
) and self._supported_features & light.SUPPORT_BRIGHTNESS:
|
):
|
||||||
if brightness is not None:
|
if brightness is not None:
|
||||||
level = min(254, brightness)
|
level = min(254, brightness)
|
||||||
else:
|
else:
|
||||||
@ -257,10 +253,7 @@ class BaseLight(LogMixin, light.LightEntity):
|
|||||||
self.debug("turned on: %s", t_log)
|
self.debug("turned on: %s", t_log)
|
||||||
return
|
return
|
||||||
self._state = True
|
self._state = True
|
||||||
if (
|
if light.ATTR_COLOR_TEMP in kwargs:
|
||||||
light.ATTR_COLOR_TEMP in kwargs
|
|
||||||
and self.supported_features & light.SUPPORT_COLOR_TEMP
|
|
||||||
):
|
|
||||||
temperature = kwargs[light.ATTR_COLOR_TEMP]
|
temperature = kwargs[light.ATTR_COLOR_TEMP]
|
||||||
result = await self._color_channel.move_to_color_temp(temperature, duration)
|
result = await self._color_channel.move_to_color_temp(temperature, duration)
|
||||||
t_log["move_to_color_temp"] = result
|
t_log["move_to_color_temp"] = result
|
||||||
@ -270,10 +263,7 @@ class BaseLight(LogMixin, light.LightEntity):
|
|||||||
self._color_temp = temperature
|
self._color_temp = temperature
|
||||||
self._hs_color = None
|
self._hs_color = None
|
||||||
|
|
||||||
if (
|
if light.ATTR_HS_COLOR in kwargs:
|
||||||
light.ATTR_HS_COLOR in kwargs
|
|
||||||
and self.supported_features & light.SUPPORT_COLOR
|
|
||||||
):
|
|
||||||
hs_color = kwargs[light.ATTR_HS_COLOR]
|
hs_color = kwargs[light.ATTR_HS_COLOR]
|
||||||
xy_color = color_util.color_hs_to_xy(*hs_color)
|
xy_color = color_util.color_hs_to_xy(*hs_color)
|
||||||
result = await self._color_channel.move_to_color(
|
result = await self._color_channel.move_to_color(
|
||||||
@ -286,10 +276,7 @@ class BaseLight(LogMixin, light.LightEntity):
|
|||||||
self._hs_color = hs_color
|
self._hs_color = hs_color
|
||||||
self._color_temp = None
|
self._color_temp = None
|
||||||
|
|
||||||
if (
|
if effect == light.EFFECT_COLORLOOP:
|
||||||
effect == light.EFFECT_COLORLOOP
|
|
||||||
and self.supported_features & light.LightEntityFeature.EFFECT
|
|
||||||
):
|
|
||||||
result = await self._color_channel.color_loop_set(
|
result = await self._color_channel.color_loop_set(
|
||||||
UPDATE_COLORLOOP_ACTION
|
UPDATE_COLORLOOP_ACTION
|
||||||
| UPDATE_COLORLOOP_DIRECTION
|
| UPDATE_COLORLOOP_DIRECTION
|
||||||
@ -302,9 +289,7 @@ class BaseLight(LogMixin, light.LightEntity):
|
|||||||
t_log["color_loop_set"] = result
|
t_log["color_loop_set"] = result
|
||||||
self._effect = light.EFFECT_COLORLOOP
|
self._effect = light.EFFECT_COLORLOOP
|
||||||
elif (
|
elif (
|
||||||
self._effect == light.EFFECT_COLORLOOP
|
self._effect == light.EFFECT_COLORLOOP and effect != light.EFFECT_COLORLOOP
|
||||||
and effect != light.EFFECT_COLORLOOP
|
|
||||||
and self.supported_features & light.LightEntityFeature.EFFECT
|
|
||||||
):
|
):
|
||||||
result = await self._color_channel.color_loop_set(
|
result = await self._color_channel.color_loop_set(
|
||||||
UPDATE_COLORLOOP_ACTION,
|
UPDATE_COLORLOOP_ACTION,
|
||||||
@ -316,10 +301,7 @@ class BaseLight(LogMixin, light.LightEntity):
|
|||||||
t_log["color_loop_set"] = result
|
t_log["color_loop_set"] = result
|
||||||
self._effect = None
|
self._effect = None
|
||||||
|
|
||||||
if (
|
if flash is not None:
|
||||||
flash is not None
|
|
||||||
and self._supported_features & light.LightEntityFeature.FLASH
|
|
||||||
):
|
|
||||||
result = await self._identify_channel.trigger_effect(
|
result = await self._identify_channel.trigger_effect(
|
||||||
FLASH_EFFECTS[flash], EFFECT_DEFAULT_VARIANT
|
FLASH_EFFECTS[flash], EFFECT_DEFAULT_VARIANT
|
||||||
)
|
)
|
||||||
@ -332,7 +314,7 @@ class BaseLight(LogMixin, light.LightEntity):
|
|||||||
async def async_turn_off(self, **kwargs):
|
async def async_turn_off(self, **kwargs):
|
||||||
"""Turn the entity off."""
|
"""Turn the entity off."""
|
||||||
duration = kwargs.get(light.ATTR_TRANSITION)
|
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:
|
if duration and supports_level:
|
||||||
result = await self._level_channel.move_to_level_with_on_off(
|
result = await self._level_channel.move_to_level_with_on_off(
|
||||||
@ -356,6 +338,7 @@ class BaseLight(LogMixin, light.LightEntity):
|
|||||||
class Light(BaseLight, ZhaEntity):
|
class Light(BaseLight, ZhaEntity):
|
||||||
"""Representation of a ZHA or ZLL light."""
|
"""Representation of a ZHA or ZLL light."""
|
||||||
|
|
||||||
|
_attr_supported_color_modes: set(ColorMode)
|
||||||
_REFRESH_INTERVAL = (45, 75)
|
_REFRESH_INTERVAL = (45, 75)
|
||||||
|
|
||||||
def __init__(self, unique_id, zha_device: ZhaDeviceType, channels, **kwargs):
|
def __init__(self, unique_id, zha_device: ZhaDeviceType, channels, **kwargs):
|
||||||
@ -372,19 +355,20 @@ class Light(BaseLight, ZhaEntity):
|
|||||||
self._cancel_refresh_handle = None
|
self._cancel_refresh_handle = None
|
||||||
effect_list = []
|
effect_list = []
|
||||||
|
|
||||||
|
self._attr_supported_color_modes = {ColorMode.ONOFF}
|
||||||
if self._level_channel:
|
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._supported_features |= light.LightEntityFeature.TRANSITION
|
||||||
self._brightness = self._level_channel.current_level
|
self._brightness = self._level_channel.current_level
|
||||||
|
|
||||||
if self._color_channel:
|
if self._color_channel:
|
||||||
color_capabilities = self._color_channel.color_capabilities
|
color_capabilities = self._color_channel.color_capabilities
|
||||||
if color_capabilities & CAPABILITIES_COLOR_TEMP:
|
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
|
self._color_temp = self._color_channel.color_temperature
|
||||||
|
|
||||||
if color_capabilities & CAPABILITIES_COLOR_XY:
|
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_x = self._color_channel.current_x
|
||||||
curr_y = self._color_channel.current_y
|
curr_y = self._color_channel.current_y
|
||||||
if curr_x is not None and curr_y is not None:
|
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)
|
effect_list.append(light.EFFECT_COLORLOOP)
|
||||||
if self._color_channel.color_loop_active == 1:
|
if self._color_channel.color_loop_active == 1:
|
||||||
self._effect = light.EFFECT_COLORLOOP
|
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:
|
if self._identify_channel:
|
||||||
self._supported_features |= light.LightEntityFeature.FLASH
|
self._supported_features |= light.LightEntityFeature.FLASH
|
||||||
@ -455,6 +449,8 @@ class Light(BaseLight, ZhaEntity):
|
|||||||
self._brightness = last_state.attributes["brightness"]
|
self._brightness = last_state.attributes["brightness"]
|
||||||
if "off_brightness" in last_state.attributes:
|
if "off_brightness" in last_state.attributes:
|
||||||
self._off_brightness = last_state.attributes["off_brightness"]
|
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:
|
if "color_temp" in last_state.attributes:
|
||||||
self._color_temp = last_state.attributes["color_temp"]
|
self._color_temp = last_state.attributes["color_temp"]
|
||||||
if "hs_color" in last_state.attributes:
|
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 := 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")
|
color_temp = results.get("color_temperature")
|
||||||
if color_temp is not None and color_mode:
|
if color_temp is not None and color_mode:
|
||||||
self._color_temp = color_temp
|
self._color_temp = color_temp
|
||||||
self._hs_color = None
|
self._hs_color = None
|
||||||
else:
|
else:
|
||||||
|
self._color_mode = ColorMode.HS
|
||||||
color_x = results.get("current_x")
|
color_x = results.get("current_x")
|
||||||
color_y = results.get("current_y")
|
color_y = results.get("current_y")
|
||||||
if color_x is not None and color_y is not None:
|
if color_x is not None and color_y is not None:
|
||||||
@ -573,6 +571,7 @@ class LightGroup(BaseLight, ZhaGroupEntity):
|
|||||||
CONF_DEFAULT_LIGHT_TRANSITION,
|
CONF_DEFAULT_LIGHT_TRANSITION,
|
||||||
0,
|
0,
|
||||||
)
|
)
|
||||||
|
self._color_mode = None
|
||||||
|
|
||||||
async def async_added_to_hass(self):
|
async def async_added_to_hass(self):
|
||||||
"""Run when about to be added to hass."""
|
"""Run when about to be added to hass."""
|
||||||
@ -633,6 +632,29 @@ class LightGroup(BaseLight, ZhaGroupEntity):
|
|||||||
effects_count = Counter(itertools.chain(all_effects))
|
effects_count = Counter(itertools.chain(all_effects))
|
||||||
self._effect = effects_count.most_common(1)[0][0]
|
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
|
self._supported_features = 0
|
||||||
for support in helpers.find_state_attributes(states, ATTR_SUPPORTED_FEATURES):
|
for support in helpers.find_state_attributes(states, ATTR_SUPPORTED_FEATURES):
|
||||||
# Merge supported features by emulating support for every feature
|
# Merge supported features by emulating support for every feature
|
||||||
|
@ -12,6 +12,7 @@ from homeassistant.components.light import (
|
|||||||
DOMAIN as LIGHT_DOMAIN,
|
DOMAIN as LIGHT_DOMAIN,
|
||||||
FLASH_LONG,
|
FLASH_LONG,
|
||||||
FLASH_SHORT,
|
FLASH_SHORT,
|
||||||
|
ColorMode,
|
||||||
)
|
)
|
||||||
from homeassistant.components.zha.core.group import GroupMember
|
from homeassistant.components.zha.core.group import GroupMember
|
||||||
from homeassistant.components.zha.light import FLASH_EFFECTS
|
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)
|
await async_wait_for_updates(hass)
|
||||||
|
|
||||||
# test that the lights were created and are off
|
# 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
|
# 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)
|
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(
|
await async_test_dimmer_from_light(
|
||||||
hass, dev1_cluster_level, group_entity_id, 150, STATE_ON
|
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
|
# test long flashing the lights from the HA
|
||||||
await async_test_flash_from_hass(
|
await async_test_flash_from_hass(
|
||||||
|
Loading…
x
Reference in New Issue
Block a user