ZHA light entity cleanup (#75573)

* use base class attributes

* initial hue and saturation support

* spec is 65536 not 65535

* fixes

* enhanced current hue

* fix comparison

* clean up

* fix channel test

* oops

* report enhanced current hue
This commit is contained in:
David F. Mulcahey 2022-07-21 17:54:50 -04:00 committed by GitHub
parent 6cb1794720
commit 04c6b9c519
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 285 additions and 166 deletions

View File

@ -31,6 +31,9 @@ class ColorChannel(ZigbeeChannel):
REPORT_CONFIG = ( REPORT_CONFIG = (
AttrReportConfig(attr="current_x", config=REPORT_CONFIG_DEFAULT), AttrReportConfig(attr="current_x", config=REPORT_CONFIG_DEFAULT),
AttrReportConfig(attr="current_y", config=REPORT_CONFIG_DEFAULT), AttrReportConfig(attr="current_y", config=REPORT_CONFIG_DEFAULT),
AttrReportConfig(attr="current_hue", config=REPORT_CONFIG_DEFAULT),
AttrReportConfig(attr="enhanced_current_hue", config=REPORT_CONFIG_DEFAULT),
AttrReportConfig(attr="current_saturation", config=REPORT_CONFIG_DEFAULT),
AttrReportConfig(attr="color_temperature", config=REPORT_CONFIG_DEFAULT), AttrReportConfig(attr="color_temperature", config=REPORT_CONFIG_DEFAULT),
) )
MAX_MIREDS: int = 500 MAX_MIREDS: int = 500
@ -52,6 +55,14 @@ 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 zcl_color_capabilities(self) -> lighting.Color.ColorCapabilities:
"""Return ZCL color capabilities of the light."""
color_capabilities = self.cluster.get("color_capabilities")
if color_capabilities is None:
return lighting.Color.ColorCapabilities(self.CAPABILITIES_COLOR_XY)
return lighting.Color.ColorCapabilities(color_capabilities)
@property @property
def color_mode(self) -> int | None: def color_mode(self) -> int | None:
"""Return cached value of the color_mode attribute.""" """Return cached value of the color_mode attribute."""
@ -77,6 +88,21 @@ class ColorChannel(ZigbeeChannel):
"""Return cached value of the current_y attribute.""" """Return cached value of the current_y attribute."""
return self.cluster.get("current_y") return self.cluster.get("current_y")
@property
def current_hue(self) -> int | None:
"""Return cached value of the current_hue attribute."""
return self.cluster.get("current_hue")
@property
def enhanced_current_hue(self) -> int | None:
"""Return cached value of the enhanced_current_hue attribute."""
return self.cluster.get("enhanced_current_hue")
@property
def current_saturation(self) -> int | None:
"""Return cached value of the current_saturation attribute."""
return self.cluster.get("current_saturation")
@property @property
def min_mireds(self) -> int: def min_mireds(self) -> int:
"""Return the coldest color_temp that this channel supports.""" """Return the coldest color_temp that this channel supports."""
@ -86,3 +112,48 @@ class ColorChannel(ZigbeeChannel):
def max_mireds(self) -> int: def max_mireds(self) -> int:
"""Return the warmest color_temp that this channel supports.""" """Return the warmest color_temp that this channel supports."""
return self.cluster.get("color_temp_physical_max", self.MAX_MIREDS) return self.cluster.get("color_temp_physical_max", self.MAX_MIREDS)
@property
def hs_supported(self) -> bool:
"""Return True if the channel supports hue and saturation."""
return (
self.zcl_color_capabilities is not None
and lighting.Color.ColorCapabilities.Hue_and_saturation
in self.zcl_color_capabilities
)
@property
def enhanced_hue_supported(self) -> bool:
"""Return True if the channel supports enhanced hue and saturation."""
return (
self.zcl_color_capabilities is not None
and lighting.Color.ColorCapabilities.Enhanced_hue
in self.zcl_color_capabilities
)
@property
def xy_supported(self) -> bool:
"""Return True if the channel supports xy."""
return (
self.zcl_color_capabilities is not None
and lighting.Color.ColorCapabilities.XY_attributes
in self.zcl_color_capabilities
)
@property
def color_temp_supported(self) -> bool:
"""Return True if the channel supports color temperature."""
return (
self.zcl_color_capabilities is not None
and lighting.Color.ColorCapabilities.Color_temperature
in self.zcl_color_capabilities
)
@property
def color_loop_supported(self) -> bool:
"""Return True if the channel supports color loop."""
return (
self.zcl_color_capabilities is not None
and lighting.Color.ColorCapabilities.Color_loop
in self.zcl_color_capabilities
)

View File

@ -15,15 +15,6 @@ 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_COLOR_MODE,
ATTR_COLOR_TEMP,
ATTR_EFFECT,
ATTR_EFFECT_LIST,
ATTR_HS_COLOR,
ATTR_MAX_MIREDS,
ATTR_MIN_MIREDS,
ATTR_SUPPORTED_COLOR_MODES,
ColorMode, ColorMode,
brightness_supported, brightness_supported,
filter_supported_color_modes, filter_supported_color_modes,
@ -43,7 +34,6 @@ from homeassistant.helpers.dispatcher import (
) )
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.event import async_track_time_interval
import homeassistant.util.color as color_util
from .core import discovery, helpers from .core import discovery, helpers
from .core.const import ( from .core.const import (
@ -69,10 +59,6 @@ if TYPE_CHECKING:
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CAPABILITIES_COLOR_LOOP = 0x4
CAPABILITIES_COLOR_XY = 0x08
CAPABILITIES_COLOR_TEMP = 0x10
DEFAULT_MIN_BRIGHTNESS = 2 DEFAULT_MIN_BRIGHTNESS = 2
UPDATE_COLORLOOP_ACTION = 0x1 UPDATE_COLORLOOP_ACTION = 0x1
@ -88,7 +74,7 @@ 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} COLOR_MODES_GROUP_LIGHT = {ColorMode.COLOR_TEMP, ColorMode.XY}
SUPPORT_GROUP_LIGHT = ( SUPPORT_GROUP_LIGHT = (
light.LightEntityFeature.EFFECT light.LightEntityFeature.EFFECT
| light.LightEntityFeature.FLASH | light.LightEntityFeature.FLASH
@ -123,24 +109,18 @@ class BaseLight(LogMixin, light.LightEntity):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
"""Initialize the light.""" """Initialize the light."""
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self._available: bool = False self._attr_min_mireds: int | None = 153
self._brightness: int | None = None self._attr_max_mireds: int | None = 500
self._attr_color_mode = ColorMode.UNKNOWN # Set by sub classes
self._attr_supported_features: int = 0
self._attr_state: bool | None
self._off_with_transition: bool = False self._off_with_transition: bool = False
self._off_brightness: int | None = None self._off_brightness: int | None = None
self._hs_color: tuple[float, float] | None = None self._zha_config_transition = self._DEFAULT_MIN_TRANSITION_TIME
self._color_temp: int | None = None
self._min_mireds: int | None = 153
self._max_mireds: int | None = 500
self._effect_list: list[str] | None = None
self._effect: str | None = None
self._supported_features: int = 0
self._state: bool = False
self._on_off_channel = None self._on_off_channel = None
self._level_channel = None self._level_channel = None
self._color_channel = None self._color_channel = None
self._identify_channel = None self._identify_channel = None
self._zha_config_transition = self._DEFAULT_MIN_TRANSITION_TIME
self._attr_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]:
@ -154,24 +134,9 @@ class BaseLight(LogMixin, light.LightEntity):
@property @property
def is_on(self) -> bool: def is_on(self) -> bool:
"""Return true if entity is on.""" """Return true if entity is on."""
if self._state is None: if self._attr_state is None:
return False return False
return self._state return self._attr_state
@property
def brightness(self):
"""Return the brightness of this light."""
return self._brightness
@property
def min_mireds(self):
"""Return the coldest color_temp that this light supports."""
return self._min_mireds
@property
def max_mireds(self):
"""Return the warmest color_temp that this light supports."""
return self._max_mireds
@callback @callback
def set_level(self, value): def set_level(self, value):
@ -182,34 +147,9 @@ class BaseLight(LogMixin, light.LightEntity):
level level
""" """
value = max(0, min(254, value)) value = max(0, min(254, value))
self._brightness = value self._attr_brightness = value
self.async_write_ha_state() self.async_write_ha_state()
@property
def hs_color(self):
"""Return the hs color value [int, int]."""
return self._hs_color
@property
def color_temp(self):
"""Return the CT color value in mireds."""
return self._color_temp
@property
def effect_list(self):
"""Return the list of supported effects."""
return self._effect_list
@property
def effect(self):
"""Return the current effect."""
return self._effect
@property
def supported_features(self):
"""Flag supported features."""
return self._supported_features
async def async_turn_on(self, **kwargs): async def async_turn_on(self, **kwargs):
"""Turn the entity on.""" """Turn the entity on."""
transition = kwargs.get(light.ATTR_TRANSITION) transition = kwargs.get(light.ATTR_TRANSITION)
@ -222,6 +162,7 @@ class BaseLight(LogMixin, light.LightEntity):
effect = kwargs.get(light.ATTR_EFFECT) effect = kwargs.get(light.ATTR_EFFECT)
flash = kwargs.get(light.ATTR_FLASH) flash = kwargs.get(light.ATTR_FLASH)
temperature = kwargs.get(light.ATTR_COLOR_TEMP) temperature = kwargs.get(light.ATTR_COLOR_TEMP)
xy_color = kwargs.get(light.ATTR_XY_COLOR)
hs_color = kwargs.get(light.ATTR_HS_COLOR) hs_color = kwargs.get(light.ATTR_HS_COLOR)
# If the light is currently off but a turn_on call with a color/temperature is sent, # If the light is currently off but a turn_on call with a color/temperature is sent,
@ -235,19 +176,26 @@ class BaseLight(LogMixin, light.LightEntity):
new_color_provided_while_off = ( new_color_provided_while_off = (
not isinstance(self, LightGroup) not isinstance(self, LightGroup)
and not self._FORCE_ON and not self._FORCE_ON
and not self._state and not self._attr_state
and ( and (
( (
temperature is not None temperature is not None
and ( and (
self._color_temp != temperature self._attr_color_temp != temperature
or self._attr_color_mode != ColorMode.COLOR_TEMP or self._attr_color_mode != ColorMode.COLOR_TEMP
) )
) )
or (
xy_color is not None
and (
self._attr_xy_color != xy_color
or self._attr_color_mode != ColorMode.XY
)
)
or ( or (
hs_color is not None hs_color is not None
and ( and (
self.hs_color != hs_color self._attr_hs_color != hs_color
or self._attr_color_mode != ColorMode.HS or self._attr_color_mode != ColorMode.HS
) )
) )
@ -265,7 +213,7 @@ class BaseLight(LogMixin, light.LightEntity):
if brightness is not None: if brightness is not None:
level = min(254, brightness) level = min(254, brightness)
else: else:
level = self._brightness or 254 level = self._attr_brightness or 254
t_log = {} t_log = {}
@ -280,7 +228,7 @@ class BaseLight(LogMixin, light.LightEntity):
self.debug("turned on: %s", t_log) self.debug("turned on: %s", t_log)
return return
# Currently only setting it to "on", as the correct level state will be set at the second move_to_level call # Currently only setting it to "on", as the correct level state will be set at the second move_to_level call
self._state = True self._attr_state = True
if ( if (
(brightness is not None or transition) (brightness is not None or transition)
@ -294,9 +242,9 @@ class BaseLight(LogMixin, light.LightEntity):
if isinstance(result, Exception) or result[1] is not Status.SUCCESS: if isinstance(result, Exception) or result[1] is not Status.SUCCESS:
self.debug("turned on: %s", t_log) self.debug("turned on: %s", t_log)
return return
self._state = bool(level) self._attr_state = bool(level)
if level: if level:
self._brightness = level self._attr_brightness = level
if ( if (
brightness is None brightness is None
@ -310,7 +258,7 @@ class BaseLight(LogMixin, light.LightEntity):
if isinstance(result, Exception) or result[1] is not Status.SUCCESS: if isinstance(result, Exception) or result[1] is not Status.SUCCESS:
self.debug("turned on: %s", t_log) self.debug("turned on: %s", t_log)
return return
self._state = True self._attr_state = True
if temperature is not None: if temperature is not None:
result = await self._color_channel.move_to_color_temp( result = await self._color_channel.move_to_color_temp(
@ -324,11 +272,39 @@ class BaseLight(LogMixin, light.LightEntity):
self.debug("turned on: %s", t_log) self.debug("turned on: %s", t_log)
return return
self._attr_color_mode = ColorMode.COLOR_TEMP self._attr_color_mode = ColorMode.COLOR_TEMP
self._color_temp = temperature self._attr_color_temp = temperature
self._hs_color = None self._attr_xy_color = None
self._attr_hs_color = None
if hs_color is not None: if hs_color is not None:
xy_color = color_util.color_hs_to_xy(*hs_color) if self._color_channel.enhanced_hue_supported:
result = await self._color_channel.enhanced_move_to_hue_and_saturation(
int(hs_color[0] * 65535 / 360),
int(hs_color[1] * 2.54),
self._DEFAULT_MIN_TRANSITION_TIME
if new_color_provided_while_off
else duration,
)
t_log["enhanced_move_to_hue_and_saturation"] = result
else:
result = await self._color_channel.move_to_hue_and_saturation(
int(hs_color[0] * 254 / 360),
int(hs_color[1] * 2.54),
self._DEFAULT_MIN_TRANSITION_TIME
if new_color_provided_while_off
else duration,
)
t_log["move_to_hue_and_saturation"] = result
if isinstance(result, Exception) or result[1] is not Status.SUCCESS:
self.debug("turned on: %s", t_log)
return
self._attr_color_mode = ColorMode.HS
self._attr_hs_color = hs_color
self._attr_xy_color = None
self._attr_color_temp = None
xy_color = None # don't set xy_color if it is also present
if xy_color is not None:
result = await self._color_channel.move_to_color( result = await self._color_channel.move_to_color(
int(xy_color[0] * 65535), int(xy_color[0] * 65535),
int(xy_color[1] * 65535), int(xy_color[1] * 65535),
@ -340,9 +316,10 @@ class BaseLight(LogMixin, light.LightEntity):
if isinstance(result, Exception) or result[1] is not Status.SUCCESS: if isinstance(result, Exception) or result[1] is not Status.SUCCESS:
self.debug("turned on: %s", t_log) self.debug("turned on: %s", t_log)
return return
self._attr_color_mode = ColorMode.HS self._attr_color_mode = ColorMode.XY
self._hs_color = hs_color self._attr_xy_color = xy_color
self._color_temp = None self._attr_color_temp = None
self._attr_hs_color = None
if new_color_provided_while_off: if new_color_provided_while_off:
# The light is has the correct color, so we can now transition it to the correct brightness level. # The light is has the correct color, so we can now transition it to the correct brightness level.
@ -351,9 +328,9 @@ class BaseLight(LogMixin, light.LightEntity):
if isinstance(result, Exception) or result[1] is not Status.SUCCESS: if isinstance(result, Exception) or result[1] is not Status.SUCCESS:
self.debug("turned on: %s", t_log) self.debug("turned on: %s", t_log)
return return
self._state = bool(level) self._attr_state = bool(level)
if level: if level:
self._brightness = level self._attr_brightness = level
if effect == light.EFFECT_COLORLOOP: if effect == light.EFFECT_COLORLOOP:
result = await self._color_channel.color_loop_set( result = await self._color_channel.color_loop_set(
@ -366,9 +343,10 @@ class BaseLight(LogMixin, light.LightEntity):
0, # no hue 0, # no hue
) )
t_log["color_loop_set"] = result t_log["color_loop_set"] = result
self._effect = light.EFFECT_COLORLOOP self._attr_effect = light.EFFECT_COLORLOOP
elif ( elif (
self._effect == light.EFFECT_COLORLOOP and effect != light.EFFECT_COLORLOOP self._attr_effect == light.EFFECT_COLORLOOP
and effect != light.EFFECT_COLORLOOP
): ):
result = await self._color_channel.color_loop_set( result = await self._color_channel.color_loop_set(
UPDATE_COLORLOOP_ACTION, UPDATE_COLORLOOP_ACTION,
@ -378,7 +356,7 @@ class BaseLight(LogMixin, light.LightEntity):
0x0, # update action only, action off, no dir, time, hue 0x0, # update action only, action off, no dir, time, hue
) )
t_log["color_loop_set"] = result t_log["color_loop_set"] = result
self._effect = None self._attr_effect = None
if flash is not None: if flash is not None:
result = await self._identify_channel.trigger_effect( result = await self._identify_channel.trigger_effect(
@ -406,12 +384,12 @@ class BaseLight(LogMixin, light.LightEntity):
self.debug("turned off: %s", result) self.debug("turned off: %s", result)
if isinstance(result, Exception) or result[1] is not Status.SUCCESS: if isinstance(result, Exception) or result[1] is not Status.SUCCESS:
return return
self._state = False self._attr_state = False
if supports_level: if supports_level:
# store current brightness so that the next turn_on uses it. # store current brightness so that the next turn_on uses it.
self._off_with_transition = transition is not None self._off_with_transition = transition is not None
self._off_brightness = self._brightness self._off_brightness = self._attr_brightness
self.async_write_ha_state() self.async_write_ha_state()
@ -427,44 +405,59 @@ class Light(BaseLight, ZhaEntity):
"""Initialize the ZHA light.""" """Initialize the ZHA light."""
super().__init__(unique_id, zha_device, channels, **kwargs) super().__init__(unique_id, zha_device, channels, **kwargs)
self._on_off_channel = self.cluster_channels[CHANNEL_ON_OFF] self._on_off_channel = self.cluster_channels[CHANNEL_ON_OFF]
self._state = bool(self._on_off_channel.on_off) self._attr_state = bool(self._on_off_channel.on_off)
self._level_channel = self.cluster_channels.get(CHANNEL_LEVEL) self._level_channel = self.cluster_channels.get(CHANNEL_LEVEL)
self._color_channel = self.cluster_channels.get(CHANNEL_COLOR) self._color_channel = self.cluster_channels.get(CHANNEL_COLOR)
self._identify_channel = self.zha_device.channels.identify_ch self._identify_channel = self.zha_device.channels.identify_ch
if self._color_channel: if self._color_channel:
self._min_mireds: int | None = self._color_channel.min_mireds self._attr_min_mireds: int = self._color_channel.min_mireds
self._max_mireds: int | None = self._color_channel.max_mireds self._attr_max_mireds: int = self._color_channel.max_mireds
self._cancel_refresh_handle = None self._cancel_refresh_handle = None
effect_list = [] effect_list = []
self._attr_supported_color_modes = {ColorMode.ONOFF} self._attr_supported_color_modes = {ColorMode.ONOFF}
if self._level_channel: if self._level_channel:
self._attr_supported_color_modes.add(ColorMode.BRIGHTNESS) self._attr_supported_color_modes.add(ColorMode.BRIGHTNESS)
self._supported_features |= light.LightEntityFeature.TRANSITION self._attr_supported_features |= light.LightEntityFeature.TRANSITION
self._brightness = self._level_channel.current_level self._attr_brightness = self._level_channel.current_level
if self._color_channel: if self._color_channel:
color_capabilities = self._color_channel.color_capabilities if self._color_channel.color_temp_supported:
if color_capabilities & CAPABILITIES_COLOR_TEMP:
self._attr_supported_color_modes.add(ColorMode.COLOR_TEMP) self._attr_supported_color_modes.add(ColorMode.COLOR_TEMP)
self._color_temp = self._color_channel.color_temperature self._attr_color_temp = self._color_channel.color_temperature
if color_capabilities & CAPABILITIES_COLOR_XY: if (
self._attr_supported_color_modes.add(ColorMode.HS) self._color_channel.xy_supported
and not self._color_channel.hs_supported
):
self._attr_supported_color_modes.add(ColorMode.XY)
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:
self._hs_color = color_util.color_xy_to_hs( self._attr_xy_color = (curr_x / 65535, curr_y / 65535)
float(curr_x / 65535), float(curr_y / 65535) else:
self._attr_xy_color = (0, 0)
if self._color_channel.hs_supported:
self._attr_supported_color_modes.add(ColorMode.HS)
if self._color_channel.enhanced_hue_supported:
curr_hue = self._color_channel.enhanced_current_hue * 65535 / 360
else:
curr_hue = self._color_channel.current_hue * 254 / 360
curr_saturation = self._color_channel.current_saturation
if curr_hue is not None and curr_saturation is not None:
self._attr_hs_color = (
int(curr_hue),
int(curr_saturation * 2.54),
) )
else: else:
self._hs_color = (0, 0) self._attr_hs_color = (0, 0)
if color_capabilities & CAPABILITIES_COLOR_LOOP: if self._color_channel.color_loop_supported:
self._supported_features |= light.LightEntityFeature.EFFECT self._attr_supported_features |= light.LightEntityFeature.EFFECT
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._attr_effect = light.EFFECT_COLORLOOP
self._attr_supported_color_modes = filter_supported_color_modes( self._attr_supported_color_modes = filter_supported_color_modes(
self._attr_supported_color_modes self._attr_supported_color_modes
) )
@ -475,13 +468,13 @@ class Light(BaseLight, ZhaEntity):
if self._color_channel.color_mode == Color.ColorMode.Color_temperature: if self._color_channel.color_mode == Color.ColorMode.Color_temperature:
self._attr_color_mode = ColorMode.COLOR_TEMP self._attr_color_mode = ColorMode.COLOR_TEMP
else: else:
self._attr_color_mode = ColorMode.HS self._attr_color_mode = ColorMode.XY
if self._identify_channel: if self._identify_channel:
self._supported_features |= light.LightEntityFeature.FLASH self._attr_supported_features |= light.LightEntityFeature.FLASH
if effect_list: if effect_list:
self._effect_list = effect_list self._attr_effect_list = effect_list
self._zha_config_transition = async_get_zha_config_value( self._zha_config_transition = async_get_zha_config_value(
zha_device.gateway.config_entry, zha_device.gateway.config_entry,
@ -493,7 +486,7 @@ class Light(BaseLight, ZhaEntity):
@callback @callback
def async_set_state(self, attr_id, attr_name, value): def async_set_state(self, attr_id, attr_name, value):
"""Set the state.""" """Set the state."""
self._state = bool(value) self._attr_state = bool(value)
if value: if value:
self._off_with_transition = False self._off_with_transition = False
self._off_brightness = None self._off_brightness = None
@ -529,9 +522,9 @@ class Light(BaseLight, ZhaEntity):
@callback @callback
def async_restore_last_state(self, last_state): def async_restore_last_state(self, last_state):
"""Restore previous state.""" """Restore previous state."""
self._state = last_state.state == STATE_ON self._attr_state = last_state.state == STATE_ON
if "brightness" in last_state.attributes: if "brightness" in last_state.attributes:
self._brightness = last_state.attributes["brightness"] self._attr_brightness = last_state.attributes["brightness"]
if "off_with_transition" in last_state.attributes: if "off_with_transition" in last_state.attributes:
self._off_with_transition = last_state.attributes["off_with_transition"] self._off_with_transition = last_state.attributes["off_with_transition"]
if "off_brightness" in last_state.attributes: if "off_brightness" in last_state.attributes:
@ -539,15 +532,17 @@ class Light(BaseLight, ZhaEntity):
if "color_mode" in last_state.attributes: if "color_mode" in last_state.attributes:
self._attr_color_mode = ColorMode(last_state.attributes["color_mode"]) self._attr_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._attr_color_temp = last_state.attributes["color_temp"]
if "xy_color" in last_state.attributes:
self._attr_xy_color = last_state.attributes["xy_color"]
if "hs_color" in last_state.attributes: if "hs_color" in last_state.attributes:
self._hs_color = last_state.attributes["hs_color"] self._attr_hs_color = last_state.attributes["hs_color"]
if "effect" in last_state.attributes: if "effect" in last_state.attributes:
self._effect = last_state.attributes["effect"] self._attr_effect = last_state.attributes["effect"]
async def async_get_state(self): async def async_get_state(self):
"""Attempt to retrieve the state from the light.""" """Attempt to retrieve the state from the light."""
if not self.available: if not self._attr_available:
return return
self.debug("polling current state") self.debug("polling current state")
if self._on_off_channel: if self._on_off_channel:
@ -555,21 +550,32 @@ class Light(BaseLight, ZhaEntity):
"on_off", from_cache=False "on_off", from_cache=False
) )
if state is not None: if state is not None:
self._state = state self._attr_state = state
if self._level_channel: if self._level_channel:
level = await self._level_channel.get_attribute_value( level = await self._level_channel.get_attribute_value(
"current_level", from_cache=False "current_level", from_cache=False
) )
if level is not None: if level is not None:
self._brightness = level self._attr_brightness = level
if self._color_channel: if self._color_channel:
attributes = [ attributes = [
"color_mode", "color_mode",
"color_temperature",
"current_x", "current_x",
"current_y", "current_y",
"color_loop_active",
] ]
if self._color_channel.enhanced_hue_supported:
attributes.append("enhanced_current_hue")
attributes.append("current_saturation")
if (
self._color_channel.hs_supported
and not self._color_channel.enhanced_hue_supported
):
attributes.append("current_hue")
attributes.append("current_saturation")
if self._color_channel.color_temp_supported:
attributes.append("color_temperature")
if self._color_channel.color_loop_supported:
attributes.append("color_loop_active")
results = await self._color_channel.get_attributes( results = await self._color_channel.get_attributes(
attributes, from_cache=False, only_cache=False attributes, from_cache=False, only_cache=False
@ -580,24 +586,40 @@ class Light(BaseLight, ZhaEntity):
self._attr_color_mode = ColorMode.COLOR_TEMP self._attr_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._attr_color_temp = color_temp
self._hs_color = None self._attr_xy_color = None
else: self._attr_hs_color = None
elif color_mode == Color.ColorMode.Hue_and_saturation:
self._attr_color_mode = ColorMode.HS self._attr_color_mode = ColorMode.HS
if self._color_channel.enhanced_hue_supported:
current_hue = results.get("enhanced_current_hue")
else:
current_hue = results.get("current_hue")
current_saturation = results.get("current_saturation")
if current_hue is not None and current_saturation is not None:
self._attr_hs_color = (
int(current_hue * 360 / 65535)
if self._color_channel.enhanced_hue_supported
else int(current_hue * 360 / 254),
int(current_saturation / 254),
)
self._attr_xy_color = None
self._attr_color_temp = None
else:
self._attr_color_mode = Color.ColorMode.X_and_Y
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:
self._hs_color = color_util.color_xy_to_hs( self._attr_xy_color = (color_x / 65535, color_y / 65535)
float(color_x / 65535), float(color_y / 65535) self._attr_color_temp = None
) self._attr_hs_color = None
self._color_temp = None
color_loop_active = results.get("color_loop_active") color_loop_active = results.get("color_loop_active")
if color_loop_active is not None: if color_loop_active is not None:
if color_loop_active == 1: if color_loop_active == 1:
self._effect = light.EFFECT_COLORLOOP self._attr_effect = light.EFFECT_COLORLOOP
else: else:
self._effect = None self._attr_effect = None
async def async_update(self): async def async_update(self):
"""Update to the latest state.""" """Update to the latest state."""
@ -671,6 +693,12 @@ class LightGroup(BaseLight, ZhaGroupEntity):
) )
self._attr_color_mode = None self._attr_color_mode = None
# remove this when all ZHA platforms and base entities are updated
@property
def available(self) -> bool:
"""Return entity availability."""
return self._attr_available
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."""
await super().async_added_to_hass() await super().async_added_to_hass()
@ -700,39 +728,49 @@ class LightGroup(BaseLight, ZhaGroupEntity):
states: list[State] = list(filter(None, all_states)) states: list[State] = list(filter(None, all_states))
on_states = [state for state in states if state.state == STATE_ON] on_states = [state for state in states if state.state == STATE_ON]
self._state = len(on_states) > 0 self._attr_state = len(on_states) > 0
self._available = any(state.state != STATE_UNAVAILABLE for state in states) self._attr_available = any(state.state != STATE_UNAVAILABLE for state in states)
self._brightness = helpers.reduce_attribute(on_states, ATTR_BRIGHTNESS) self._attr_brightness = helpers.reduce_attribute(
on_states, light.ATTR_BRIGHTNESS
self._hs_color = helpers.reduce_attribute(
on_states, ATTR_HS_COLOR, reduce=helpers.mean_tuple
) )
self._color_temp = helpers.reduce_attribute(on_states, ATTR_COLOR_TEMP) self._attr_xy_color = helpers.reduce_attribute(
self._min_mireds = helpers.reduce_attribute( on_states, light.ATTR_XY_COLOR, reduce=helpers.mean_tuple
states, ATTR_MIN_MIREDS, default=153, reduce=min
)
self._max_mireds = helpers.reduce_attribute(
states, ATTR_MAX_MIREDS, default=500, reduce=max
) )
self._effect_list = None self._attr_hs_color = helpers.reduce_attribute(
all_effect_lists = list(helpers.find_state_attributes(states, ATTR_EFFECT_LIST)) on_states, light.ATTR_HS_COLOR, reduce=helpers.mean_tuple
)
self._attr_color_temp = helpers.reduce_attribute(
on_states, light.ATTR_COLOR_TEMP
)
self._attr_min_mireds = helpers.reduce_attribute(
states, light.ATTR_MIN_MIREDS, default=153, reduce=min
)
self._attr_max_mireds = helpers.reduce_attribute(
states, light.ATTR_MAX_MIREDS, default=500, reduce=max
)
self._attr_effect_list = None
all_effect_lists = list(
helpers.find_state_attributes(states, light.ATTR_EFFECT_LIST)
)
if all_effect_lists: if all_effect_lists:
# Merge all effects from all effect_lists with a union merge. # Merge all effects from all effect_lists with a union merge.
self._effect_list = list(set().union(*all_effect_lists)) self._attr_effect_list = list(set().union(*all_effect_lists))
self._effect = None self._attr_effect = None
all_effects = list(helpers.find_state_attributes(on_states, ATTR_EFFECT)) all_effects = list(helpers.find_state_attributes(on_states, light.ATTR_EFFECT))
if all_effects: if all_effects:
# Report the most common effect. # Report the most common effect.
effects_count = Counter(itertools.chain(all_effects)) effects_count = Counter(itertools.chain(all_effects))
self._effect = effects_count.most_common(1)[0][0] self._attr_effect = effects_count.most_common(1)[0][0]
self._attr_color_mode = None self._attr_color_mode = None
all_color_modes = list( all_color_modes = list(
helpers.find_state_attributes(on_states, ATTR_COLOR_MODE) helpers.find_state_attributes(on_states, light.ATTR_COLOR_MODE)
) )
if all_color_modes: if all_color_modes:
# Report the most common color mode, select brightness and onoff last # Report the most common color mode, select brightness and onoff last
@ -745,7 +783,7 @@ class LightGroup(BaseLight, ZhaGroupEntity):
self._attr_supported_color_modes = None self._attr_supported_color_modes = None
all_supported_color_modes = list( all_supported_color_modes = list(
helpers.find_state_attributes(states, ATTR_SUPPORTED_COLOR_MODES) helpers.find_state_attributes(states, light.ATTR_SUPPORTED_COLOR_MODES)
) )
if all_supported_color_modes: if all_supported_color_modes:
# Merge all color modes. # Merge all color modes.
@ -753,14 +791,14 @@ class LightGroup(BaseLight, ZhaGroupEntity):
set[str], set().union(*all_supported_color_modes) set[str], set().union(*all_supported_color_modes)
) )
self._supported_features = 0 self._attr_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
# we find. # we find.
self._supported_features |= support self._attr_supported_features |= support
# Bitwise-and the supported features with the GroupedLight's features # Bitwise-and the supported features with the GroupedLight's features
# so that we don't break in the future when a new feature is added. # so that we don't break in the future when a new feature is added.
self._supported_features &= SUPPORT_GROUP_LIGHT self._attr_supported_features &= SUPPORT_GROUP_LIGHT
async def _force_member_updates(self): async def _force_member_updates(self):
"""Force the update of member entities to ensure the states are correct for bulbs that don't report their state.""" """Force the update of member entities to ensure the states are correct for bulbs that don't report their state."""

View File

@ -151,7 +151,18 @@ async def poll_control_device(zha_device_restored, zigpy_device_mock):
}, },
), ),
(0x0202, 1, {"fan_mode"}), (0x0202, 1, {"fan_mode"}),
(0x0300, 1, {"current_x", "current_y", "color_temperature"}), (
0x0300,
1,
{
"current_x",
"current_y",
"color_temperature",
"current_hue",
"enhanced_current_hue",
"current_saturation",
},
),
(0x0400, 1, {"measured_value"}), (0x0400, 1, {"measured_value"}),
(0x0401, 1, {"level_status"}), (0x0401, 1, {"level_status"}),
(0x0402, 1, {"measured_value"}), (0x0402, 1, {"measured_value"}),

View File

@ -15,11 +15,7 @@ from homeassistant.components.light import (
ColorMode, ColorMode,
) )
from homeassistant.components.zha.core.group import GroupMember from homeassistant.components.zha.core.group import GroupMember
from homeassistant.components.zha.light import ( from homeassistant.components.zha.light import FLASH_EFFECTS
CAPABILITIES_COLOR_TEMP,
CAPABILITIES_COLOR_XY,
FLASH_EFFECTS,
)
from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE, Platform from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE, Platform
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
@ -148,7 +144,8 @@ async def device_light_1(hass, zigpy_device_mock, zha_device_joined):
) )
color_cluster = zigpy_device.endpoints[1].light_color color_cluster = zigpy_device.endpoints[1].light_color
color_cluster.PLUGGED_ATTR_READS = { color_cluster.PLUGGED_ATTR_READS = {
"color_capabilities": CAPABILITIES_COLOR_TEMP | CAPABILITIES_COLOR_XY "color_capabilities": lighting.Color.ColorCapabilities.Color_temperature
| lighting.Color.ColorCapabilities.XY_attributes
} }
zha_device = await zha_device_joined(zigpy_device) zha_device = await zha_device_joined(zigpy_device)
zha_device.available = True zha_device.available = True
@ -180,7 +177,8 @@ async def device_light_2(hass, zigpy_device_mock, zha_device_joined):
) )
color_cluster = zigpy_device.endpoints[1].light_color color_cluster = zigpy_device.endpoints[1].light_color
color_cluster.PLUGGED_ATTR_READS = { color_cluster.PLUGGED_ATTR_READS = {
"color_capabilities": CAPABILITIES_COLOR_TEMP | CAPABILITIES_COLOR_XY "color_capabilities": lighting.Color.ColorCapabilities.Color_temperature
| lighting.Color.ColorCapabilities.XY_attributes
} }
zha_device = await zha_device_joined(zigpy_device) zha_device = await zha_device_joined(zigpy_device)
zha_device.available = True zha_device.available = True
@ -239,7 +237,8 @@ async def eWeLink_light(hass, zigpy_device_mock, zha_device_joined):
) )
color_cluster = zigpy_device.endpoints[1].light_color color_cluster = zigpy_device.endpoints[1].light_color
color_cluster.PLUGGED_ATTR_READS = { color_cluster.PLUGGED_ATTR_READS = {
"color_capabilities": CAPABILITIES_COLOR_TEMP | CAPABILITIES_COLOR_XY "color_capabilities": lighting.Color.ColorCapabilities.Color_temperature
| lighting.Color.ColorCapabilities.XY_attributes
} }
zha_device = await zha_device_joined(zigpy_device) zha_device = await zha_device_joined(zigpy_device)
zha_device.available = True zha_device.available = True
@ -302,7 +301,7 @@ async def test_light_refresh(hass, zigpy_device_mock, zha_device_joined_restored
) )
@pytest.mark.parametrize( @pytest.mark.parametrize(
"device, reporting", "device, reporting",
[(LIGHT_ON_OFF, (1, 0, 0)), (LIGHT_LEVEL, (1, 1, 0)), (LIGHT_COLOR, (1, 1, 3))], [(LIGHT_ON_OFF, (1, 0, 0)), (LIGHT_LEVEL, (1, 1, 0)), (LIGHT_COLOR, (1, 1, 6))],
) )
async def test_light( async def test_light(
hass, zigpy_device_mock, zha_device_joined_restored, device, reporting hass, zigpy_device_mock, zha_device_joined_restored, device, reporting
@ -1400,7 +1399,7 @@ async def test_zha_group_light_entity(
assert group_state.state == STATE_OFF assert group_state.state == STATE_OFF
assert group_state.attributes["supported_color_modes"] == [ assert group_state.attributes["supported_color_modes"] == [
ColorMode.COLOR_TEMP, ColorMode.COLOR_TEMP,
ColorMode.HS, ColorMode.XY,
] ]
# Light which is off has no color mode # Light which is off has no color mode
assert "color_mode" not in group_state.attributes assert "color_mode" not in group_state.attributes
@ -1431,9 +1430,9 @@ async def test_zha_group_light_entity(
assert group_state.state == STATE_ON assert group_state.state == STATE_ON
assert group_state.attributes["supported_color_modes"] == [ assert group_state.attributes["supported_color_modes"] == [
ColorMode.COLOR_TEMP, ColorMode.COLOR_TEMP,
ColorMode.HS, ColorMode.XY,
] ]
assert group_state.attributes["color_mode"] == ColorMode.HS assert group_state.attributes["color_mode"] == ColorMode.XY
# 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(