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
|
CAPABILITIES_COLOR_TEMP = 0x10
|
||||||
|
|
||||||
DEFAULT_TRANSITION = 1
|
DEFAULT_TRANSITION = 1
|
||||||
|
DEFAULT_MIN_BRIGHTNESS = 2
|
||||||
|
|
||||||
UPDATE_COLORLOOP_ACTION = 0x1
|
UPDATE_COLORLOOP_ACTION = 0x1
|
||||||
UPDATE_COLORLOOP_DIRECTION = 0x2
|
UPDATE_COLORLOOP_DIRECTION = 0x2
|
||||||
|
@ -118,12 +119,14 @@ class BaseLight(LogMixin, light.LightEntity):
|
||||||
"""Operations common to all light entities."""
|
"""Operations common to all light entities."""
|
||||||
|
|
||||||
_FORCE_ON = False
|
_FORCE_ON = False
|
||||||
|
_DEFAULT_COLOR_FROM_OFF_TRANSITION = 0
|
||||||
|
|
||||||
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._available: bool = False
|
||||||
self._brightness: int | None = None
|
self._brightness: int | None = None
|
||||||
|
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._hs_color: tuple[float, float] | None = None
|
||||||
self._color_temp: int | None = None
|
self._color_temp: int | None = None
|
||||||
|
@ -143,7 +146,10 @@ class BaseLight(LogMixin, light.LightEntity):
|
||||||
@property
|
@property
|
||||||
def extra_state_attributes(self) -> dict[str, Any]:
|
def extra_state_attributes(self) -> dict[str, Any]:
|
||||||
"""Return state attributes."""
|
"""Return state attributes."""
|
||||||
attributes = {"off_brightness": self._off_brightness}
|
attributes = {
|
||||||
|
"off_with_transition": self._off_with_transition,
|
||||||
|
"off_brightness": self._off_brightness,
|
||||||
|
}
|
||||||
return attributes
|
return attributes
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -224,17 +230,53 @@ 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)
|
||||||
|
|
||||||
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
|
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:
|
if brightness is not None:
|
||||||
level = min(254, brightness)
|
level = min(254, brightness)
|
||||||
else:
|
else:
|
||||||
level = self._brightness or 254
|
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(
|
result = await self._level_channel.move_to_level_with_on_off(
|
||||||
level, duration
|
level, duration
|
||||||
)
|
)
|
||||||
|
@ -246,7 +288,11 @@ class BaseLight(LogMixin, light.LightEntity):
|
||||||
if level:
|
if level:
|
||||||
self._brightness = 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,
|
# 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.
|
# we should call the on command on the on_off cluster if brightness is not 0.
|
||||||
result = await self._on_off_channel.on()
|
result = await self._on_off_channel.on()
|
||||||
|
@ -255,6 +301,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 light.ATTR_COLOR_TEMP in kwargs:
|
if light.ATTR_COLOR_TEMP in kwargs:
|
||||||
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)
|
||||||
|
@ -280,6 +327,17 @@ class BaseLight(LogMixin, light.LightEntity):
|
||||||
self._hs_color = hs_color
|
self._hs_color = hs_color
|
||||||
self._color_temp = None
|
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:
|
if 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
|
||||||
|
@ -311,6 +369,7 @@ class BaseLight(LogMixin, light.LightEntity):
|
||||||
)
|
)
|
||||||
t_log["trigger_effect"] = result
|
t_log["trigger_effect"] = result
|
||||||
|
|
||||||
|
self._off_with_transition = False
|
||||||
self._off_brightness = None
|
self._off_brightness = None
|
||||||
self.debug("turned on: %s", t_log)
|
self.debug("turned on: %s", t_log)
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
|
@ -331,8 +390,9 @@ class BaseLight(LogMixin, light.LightEntity):
|
||||||
return
|
return
|
||||||
self._state = False
|
self._state = False
|
||||||
|
|
||||||
if duration and 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 = bool(duration)
|
||||||
self._off_brightness = self._brightness
|
self._off_brightness = self._brightness
|
||||||
|
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
|
@ -453,6 +513,8 @@ class Light(BaseLight, ZhaEntity):
|
||||||
self._state = last_state.state == STATE_ON
|
self._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._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:
|
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:
|
if "color_mode" in last_state.attributes:
|
||||||
|
@ -556,6 +618,17 @@ class ForceOnLight(Light):
|
||||||
_FORCE_ON = True
|
_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()
|
@GROUP_MATCH()
|
||||||
class LightGroup(BaseLight, ZhaGroupEntity):
|
class LightGroup(BaseLight, ZhaGroupEntity):
|
||||||
"""Representation of a light group."""
|
"""Representation of a light group."""
|
||||||
|
|
Loading…
Reference in New Issue