From 4d673278c7a6405a21201015490c8d1dc433b872 Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Wed, 29 Jun 2022 19:09:52 +0200 Subject: [PATCH] Fix color transition when turning on a ZHA light (#74024) * Initial implementation of fixing color transition when turning on a ZHA light * Add off_with_transition attribute, very slightly cleanup * Fix unnecessarily using last off_brightness when just turning on light Now it uses the Zigbee on_off call again if possible (instead of always move_to_level_with_on_off) * Use DEFAULT_TRANSITION constant for color transition, add DEFAULT_MIN_BRIGHTNESS constant * Add _DEFAULT_COLOR_FROM_OFF_TRANSITION = 0 but override transition for Sengled lights to 0.1s --- homeassistant/components/zha/light.py | 93 ++++++++++++++++++++++++--- 1 file changed, 83 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index 2f3379fa6b1..309fdf2699b 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -74,6 +74,7 @@ CAPABILITIES_COLOR_XY = 0x08 CAPABILITIES_COLOR_TEMP = 0x10 DEFAULT_TRANSITION = 1 +DEFAULT_MIN_BRIGHTNESS = 2 UPDATE_COLORLOOP_ACTION = 0x1 UPDATE_COLORLOOP_DIRECTION = 0x2 @@ -118,12 +119,14 @@ class BaseLight(LogMixin, light.LightEntity): """Operations common to all light entities.""" _FORCE_ON = False + _DEFAULT_COLOR_FROM_OFF_TRANSITION = 0 def __init__(self, *args, **kwargs): """Initialize the light.""" super().__init__(*args, **kwargs) self._available: bool = False self._brightness: int | None = None + self._off_with_transition: bool = False self._off_brightness: int | None = None self._hs_color: tuple[float, float] | None = None self._color_temp: int | None = None @@ -143,7 +146,10 @@ class BaseLight(LogMixin, light.LightEntity): @property def extra_state_attributes(self) -> dict[str, Any]: """Return state attributes.""" - attributes = {"off_brightness": self._off_brightness} + attributes = { + "off_with_transition": self._off_with_transition, + "off_brightness": self._off_brightness, + } return attributes @property @@ -224,17 +230,53 @@ class BaseLight(LogMixin, light.LightEntity): effect = kwargs.get(light.ATTR_EFFECT) flash = kwargs.get(light.ATTR_FLASH) - if brightness is None and self._off_brightness is not None: + # If the light is currently off but a turn_on call with a color/temperature is sent, + # the light needs to be turned on first at a low brightness level where the light is immediately transitioned + # to the correct color. Afterwards, the transition is only from the low brightness to the new brightness. + # Otherwise, the transition is from the color the light had before being turned on to the new color. + # This can look especially bad with transitions longer than a second. + color_provided_from_off = ( + not self._state + and brightness_supported(self._attr_supported_color_modes) + and (light.ATTR_COLOR_TEMP in kwargs or light.ATTR_HS_COLOR in kwargs) + ) + final_duration = duration + if color_provided_from_off: + # Set the duration for the color changing commands to 0. + duration = 0 + + if ( + brightness is None + and (self._off_with_transition or color_provided_from_off) + and self._off_brightness is not None + ): brightness = self._off_brightness + if brightness is not None: + level = min(254, brightness) + else: + level = self._brightness or 254 + t_log = {} - if (brightness is not None or transition) and brightness_supported( - self._attr_supported_color_modes + + if color_provided_from_off: + # If the light is currently off, we first need to turn it on at a low brightness level with no transition. + # After that, we set it to the desired color/temperature with no transition. + result = await self._level_channel.move_to_level_with_on_off( + DEFAULT_MIN_BRIGHTNESS, self._DEFAULT_COLOR_FROM_OFF_TRANSITION + ) + t_log["move_to_level_with_on_off"] = result + if isinstance(result, Exception) or result[1] is not Status.SUCCESS: + self.debug("turned on: %s", t_log) + return + # Currently only setting it to "on", as the correct level state will be set at the second move_to_level call + self._state = True + + if ( + (brightness is not None or transition) + and not color_provided_from_off + and brightness_supported(self._attr_supported_color_modes) ): - if brightness is not None: - level = min(254, brightness) - else: - level = self._brightness or 254 result = await self._level_channel.move_to_level_with_on_off( level, duration ) @@ -246,7 +288,11 @@ class BaseLight(LogMixin, light.LightEntity): if level: self._brightness = level - if brightness is None or (self._FORCE_ON and brightness): + if ( + brightness is None + and not color_provided_from_off + or (self._FORCE_ON and brightness) + ): # since some lights don't always turn on with move_to_level_with_on_off, # we should call the on command on the on_off cluster if brightness is not 0. result = await self._on_off_channel.on() @@ -255,6 +301,7 @@ class BaseLight(LogMixin, light.LightEntity): self.debug("turned on: %s", t_log) return self._state = True + if light.ATTR_COLOR_TEMP in kwargs: temperature = kwargs[light.ATTR_COLOR_TEMP] result = await self._color_channel.move_to_color_temp(temperature, duration) @@ -280,6 +327,17 @@ class BaseLight(LogMixin, light.LightEntity): self._hs_color = hs_color self._color_temp = None + if color_provided_from_off: + # The light is has the correct color, so we can now transition it to the correct brightness level. + result = await self._level_channel.move_to_level(level, final_duration) + t_log["move_to_level_if_color"] = result + if isinstance(result, Exception) or result[1] is not Status.SUCCESS: + self.debug("turned on: %s", t_log) + return + self._state = bool(level) + if level: + self._brightness = level + if effect == light.EFFECT_COLORLOOP: result = await self._color_channel.color_loop_set( UPDATE_COLORLOOP_ACTION @@ -311,6 +369,7 @@ class BaseLight(LogMixin, light.LightEntity): ) t_log["trigger_effect"] = result + self._off_with_transition = False self._off_brightness = None self.debug("turned on: %s", t_log) self.async_write_ha_state() @@ -331,8 +390,9 @@ class BaseLight(LogMixin, light.LightEntity): return self._state = False - if duration and supports_level: + if supports_level: # store current brightness so that the next turn_on uses it. + self._off_with_transition = bool(duration) self._off_brightness = self._brightness self.async_write_ha_state() @@ -453,6 +513,8 @@ class Light(BaseLight, ZhaEntity): self._state = last_state.state == STATE_ON if "brightness" in last_state.attributes: self._brightness = last_state.attributes["brightness"] + if "off_with_transition" in last_state.attributes: + self._off_with_transition = last_state.attributes["off_with_transition"] if "off_brightness" in last_state.attributes: self._off_brightness = last_state.attributes["off_brightness"] if "color_mode" in last_state.attributes: @@ -556,6 +618,17 @@ class ForceOnLight(Light): _FORCE_ON = True +@STRICT_MATCH( + channel_names=CHANNEL_ON_OFF, + aux_channels={CHANNEL_COLOR, CHANNEL_LEVEL}, + manufacturers={"Sengled"}, +) +class SengledLight(Light): + """Representation of a Sengled light which does not react to move_to_color_temp with 0 as a transition.""" + + _DEFAULT_COLOR_FROM_OFF_TRANSITION = 1 + + @GROUP_MATCH() class LightGroup(BaseLight, ZhaGroupEntity): """Representation of a light group."""