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.1spull/74184/head
parent
d6e9118f36
commit
4d673278c7
|
@ -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
|
||||
|
||||
t_log = {}
|
||||
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:
|
||||
level = self._brightness or 254
|
||||
|
||||
t_log = {}
|
||||
|
||||
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)
|
||||
):
|
||||
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."""
|
||||
|
|
Loading…
Reference in New Issue