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
pull/74184/head
TheJulianJES 2022-06-29 19:09:52 +02:00 committed by GitHub
parent d6e9118f36
commit 4d673278c7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
1 changed files with 83 additions and 10 deletions

View File

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