Fix ZHA light brightness jumping around during transitions (#74849)

* Moved color commands to a new ``async_handle_color_commands`` method

* Fixed tests

* Fix brightness jumping around during transitions

* Add config option to disable "Enhanced brightness slider during light transition"
This commit is contained in:
TheJulianJES 2022-07-27 02:03:17 +02:00 committed by GitHub
parent 6868865e3e
commit 129b42cd23
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 320 additions and 66 deletions

View File

@ -129,6 +129,7 @@ CONF_DATABASE = "database_path"
CONF_DEFAULT_LIGHT_TRANSITION = "default_light_transition" CONF_DEFAULT_LIGHT_TRANSITION = "default_light_transition"
CONF_DEVICE_CONFIG = "device_config" CONF_DEVICE_CONFIG = "device_config"
CONF_ENABLE_ENHANCED_LIGHT_TRANSITION = "enhanced_light_transition" CONF_ENABLE_ENHANCED_LIGHT_TRANSITION = "enhanced_light_transition"
CONF_ENABLE_LIGHT_TRANSITIONING_FLAG = "light_transitioning_flag"
CONF_ALWAYS_PREFER_XY_COLOR_MODE = "always_prefer_xy_color_mode" CONF_ALWAYS_PREFER_XY_COLOR_MODE = "always_prefer_xy_color_mode"
CONF_ENABLE_IDENTIFY_ON_JOIN = "enable_identify_on_join" CONF_ENABLE_IDENTIFY_ON_JOIN = "enable_identify_on_join"
CONF_ENABLE_QUIRKS = "enable_quirks" CONF_ENABLE_QUIRKS = "enable_quirks"
@ -146,6 +147,7 @@ CONF_ZHA_OPTIONS_SCHEMA = vol.Schema(
{ {
vol.Optional(CONF_DEFAULT_LIGHT_TRANSITION): cv.positive_int, vol.Optional(CONF_DEFAULT_LIGHT_TRANSITION): cv.positive_int,
vol.Required(CONF_ENABLE_ENHANCED_LIGHT_TRANSITION, default=False): cv.boolean, vol.Required(CONF_ENABLE_ENHANCED_LIGHT_TRANSITION, default=False): cv.boolean,
vol.Required(CONF_ENABLE_LIGHT_TRANSITIONING_FLAG, default=True): cv.boolean,
vol.Required(CONF_ALWAYS_PREFER_XY_COLOR_MODE, default=True): cv.boolean, vol.Required(CONF_ALWAYS_PREFER_XY_COLOR_MODE, default=True): cv.boolean,
vol.Required(CONF_ENABLE_IDENTIFY_ON_JOIN, default=True): cv.boolean, vol.Required(CONF_ENABLE_IDENTIFY_ON_JOIN, default=True): cv.boolean,
vol.Optional( vol.Optional(

View File

@ -1,7 +1,9 @@
"""Lights on Zigbee Home Automation networks.""" """Lights on Zigbee Home Automation networks."""
from __future__ import annotations from __future__ import annotations
import asyncio
from collections import Counter from collections import Counter
from collections.abc import Callable
from datetime import timedelta from datetime import timedelta
import functools import functools
import itertools import itertools
@ -26,14 +28,14 @@ from homeassistant.const import (
STATE_UNAVAILABLE, STATE_UNAVAILABLE,
Platform, Platform,
) )
from homeassistant.core import HomeAssistant, State, callback from homeassistant.core import Event, HomeAssistant, State, callback
from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.debounce import Debouncer
from homeassistant.helpers.dispatcher import ( from homeassistant.helpers.dispatcher import (
async_dispatcher_connect, async_dispatcher_connect,
async_dispatcher_send, async_dispatcher_send,
) )
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_call_later, async_track_time_interval
from .core import discovery, helpers from .core import discovery, helpers
from .core.const import ( from .core.const import (
@ -43,6 +45,7 @@ from .core.const import (
CONF_ALWAYS_PREFER_XY_COLOR_MODE, CONF_ALWAYS_PREFER_XY_COLOR_MODE,
CONF_DEFAULT_LIGHT_TRANSITION, CONF_DEFAULT_LIGHT_TRANSITION,
CONF_ENABLE_ENHANCED_LIGHT_TRANSITION, CONF_ENABLE_ENHANCED_LIGHT_TRANSITION,
CONF_ENABLE_LIGHT_TRANSITIONING_FLAG,
DATA_ZHA, DATA_ZHA,
EFFECT_BLINK, EFFECT_BLINK,
EFFECT_BREATHE, EFFECT_BREATHE,
@ -61,6 +64,10 @@ if TYPE_CHECKING:
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DEFAULT_ON_OFF_TRANSITION = 1 # most bulbs default to a 1-second turn on/off transition
DEFAULT_EXTRA_TRANSITION_DELAY_SHORT = 0.25
DEFAULT_EXTRA_TRANSITION_DELAY_LONG = 2.0
DEFAULT_LONG_TRANSITION_TIME = 10
DEFAULT_MIN_BRIGHTNESS = 2 DEFAULT_MIN_BRIGHTNESS = 2
UPDATE_COLORLOOP_ACTION = 0x1 UPDATE_COLORLOOP_ACTION = 0x1
@ -75,6 +82,8 @@ STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, Platform.LIGHT)
GROUP_MATCH = functools.partial(ZHA_ENTITIES.group_match, Platform.LIGHT) 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"
SIGNAL_LIGHT_GROUP_TRANSITION_START = "zha_light_group_transition_start"
SIGNAL_LIGHT_GROUP_TRANSITION_FINISHED = "zha_light_group_transition_finished"
DEFAULT_MIN_TRANSITION_MANUFACTURERS = {"Sengled"} DEFAULT_MIN_TRANSITION_MANUFACTURERS = {"Sengled"}
COLOR_MODES_GROUP_LIGHT = {ColorMode.COLOR_TEMP, ColorMode.XY} COLOR_MODES_GROUP_LIGHT = {ColorMode.COLOR_TEMP, ColorMode.XY}
@ -111,6 +120,7 @@ class BaseLight(LogMixin, light.LightEntity):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
"""Initialize the light.""" """Initialize the light."""
self._zha_device: ZHADevice = None
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self._attr_min_mireds: int | None = 153 self._attr_min_mireds: int | None = 153
self._attr_max_mireds: int | None = 500 self._attr_max_mireds: int | None = 500
@ -121,11 +131,14 @@ class BaseLight(LogMixin, light.LightEntity):
self._off_brightness: int | None = None self._off_brightness: int | None = None
self._zha_config_transition = self._DEFAULT_MIN_TRANSITION_TIME self._zha_config_transition = self._DEFAULT_MIN_TRANSITION_TIME
self._zha_config_enhanced_light_transition: bool = False self._zha_config_enhanced_light_transition: bool = False
self._zha_config_enable_light_transitioning_flag: bool = True
self._zha_config_always_prefer_xy_color_mode: bool = True self._zha_config_always_prefer_xy_color_mode: bool = True
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._transitioning: bool = False
self._transition_listener: Callable[[], None] | None = None
@property @property
def extra_state_attributes(self) -> dict[str, Any]: def extra_state_attributes(self) -> dict[str, Any]:
@ -151,6 +164,12 @@ class BaseLight(LogMixin, light.LightEntity):
on at `on_level` Zigbee attribute value, regardless of the last set on at `on_level` Zigbee attribute value, regardless of the last set
level level
""" """
if self._transitioning:
self.debug(
"received level %s while transitioning - skipping update",
value,
)
return
value = max(0, min(254, value)) value = max(0, min(254, value))
self._attr_brightness = value self._attr_brightness = value
self.async_write_ha_state() self.async_write_ha_state()
@ -170,6 +189,36 @@ class BaseLight(LogMixin, light.LightEntity):
xy_color = kwargs.get(light.ATTR_XY_COLOR) xy_color = kwargs.get(light.ATTR_XY_COLOR)
hs_color = kwargs.get(light.ATTR_HS_COLOR) hs_color = kwargs.get(light.ATTR_HS_COLOR)
set_transition_flag = (
brightness_supported(self._attr_supported_color_modes)
or temperature is not None
or xy_color is not None
or hs_color is not None
) and self._zha_config_enable_light_transitioning_flag
transition_time = (
(
duration / 10 + DEFAULT_EXTRA_TRANSITION_DELAY_SHORT
if (
(brightness is not None or transition is not None)
and brightness_supported(self._attr_supported_color_modes)
or (self._off_with_transition and self._off_brightness is not None)
or temperature is not None
or xy_color is not None
or hs_color is not None
)
else DEFAULT_ON_OFF_TRANSITION + DEFAULT_EXTRA_TRANSITION_DELAY_SHORT
)
if set_transition_flag
else 0
)
# If we need to pause attribute report parsing, we'll do so here.
# After successful calls, we later start a timer to unset the flag after transition_time.
# On an error on the first move to level call, we unset the flag immediately if no previous timer is running.
# On an error on subsequent calls, we start the transition timer, as a brightness call might have come through.
if set_transition_flag:
self.async_transition_set_flag()
# 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,
# the light needs to be turned on first at a low brightness level where the light is immediately transitioned # 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. # to the correct color. Afterwards, the transition is only from the low brightness to the new brightness.
@ -230,6 +279,10 @@ class BaseLight(LogMixin, light.LightEntity):
) )
t_log["move_to_level_with_on_off"] = result t_log["move_to_level_with_on_off"] = result
if isinstance(result, Exception) or result[1] is not Status.SUCCESS: if isinstance(result, Exception) or result[1] is not Status.SUCCESS:
# First 'move to level' call failed, so if the transitioning delay isn't running from a previous call,
# the flag can be unset immediately
if set_transition_flag and not self._transition_listener:
self.async_transition_complete()
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
@ -245,6 +298,10 @@ class BaseLight(LogMixin, light.LightEntity):
) )
t_log["move_to_level_with_on_off"] = result t_log["move_to_level_with_on_off"] = result
if isinstance(result, Exception) or result[1] is not Status.SUCCESS: if isinstance(result, Exception) or result[1] is not Status.SUCCESS:
# First 'move to level' call failed, so if the transitioning delay isn't running from a previous call,
# the flag can be unset immediately
if set_transition_flag and not self._transition_listener:
self.async_transition_complete()
self.debug("turned on: %s", t_log) self.debug("turned on: %s", t_log)
return return
self._attr_state = bool(level) self._attr_state = bool(level)
@ -261,73 +318,25 @@ class BaseLight(LogMixin, light.LightEntity):
result = await self._on_off_channel.on() result = await self._on_off_channel.on()
t_log["on_off"] = result t_log["on_off"] = result
if isinstance(result, Exception) or result[1] is not Status.SUCCESS: if isinstance(result, Exception) or result[1] is not Status.SUCCESS:
# 'On' call failed, but as brightness may still transition (for FORCE_ON lights),
# we start the timer to unset the flag after the transition_time if necessary.
self.async_transition_start_timer(transition_time)
self.debug("turned on: %s", t_log) self.debug("turned on: %s", t_log)
return return
self._attr_state = True self._attr_state = True
if temperature is not None: if not await self.async_handle_color_commands(
result = await self._color_channel.move_to_color_temp( temperature,
temperature, duration,
self._DEFAULT_MIN_TRANSITION_TIME hs_color,
if new_color_provided_while_off xy_color,
else duration, new_color_provided_while_off,
) t_log,
t_log["move_to_color_temp"] = result ):
if isinstance(result, Exception) or result[1] is not Status.SUCCESS: # Color calls failed, but as brightness may still transition, we start the timer to unset the flag
self.debug("turned on: %s", t_log) self.async_transition_start_timer(transition_time)
return self.debug("turned on: %s", t_log)
self._attr_color_mode = ColorMode.COLOR_TEMP return
self._attr_color_temp = temperature
self._attr_xy_color = None
self._attr_hs_color = None
if hs_color is not None:
if (
not isinstance(self, LightGroup)
and 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(
int(xy_color[0] * 65535),
int(xy_color[1] * 65535),
self._DEFAULT_MIN_TRANSITION_TIME
if new_color_provided_while_off
else duration,
)
t_log["move_to_color"] = 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.XY
self._attr_xy_color = xy_color
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.
@ -340,6 +349,10 @@ class BaseLight(LogMixin, light.LightEntity):
if level: if level:
self._attr_brightness = level self._attr_brightness = level
# Our light is guaranteed to have just started the transitioning process if necessary,
# so we start the delay for the transition (to stop parsing attribute reports after the completed transition).
self.async_transition_start_timer(transition_time)
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
@ -382,6 +395,15 @@ class BaseLight(LogMixin, light.LightEntity):
transition = kwargs.get(light.ATTR_TRANSITION) transition = kwargs.get(light.ATTR_TRANSITION)
supports_level = brightness_supported(self._attr_supported_color_modes) supports_level = brightness_supported(self._attr_supported_color_modes)
transition_time = (
transition or self._DEFAULT_MIN_TRANSITION_TIME
if transition is not None
else DEFAULT_ON_OFF_TRANSITION
) + DEFAULT_EXTRA_TRANSITION_DELAY_SHORT
# Start pausing attribute report parsing
if self._zha_config_enable_light_transitioning_flag:
self.async_transition_set_flag()
# is not none looks odd here but it will override built in bulb transition times if we pass 0 in here # is not none looks odd here but it will override built in bulb transition times if we pass 0 in here
if transition is not None and supports_level: if transition is not None and supports_level:
result = await self._level_channel.move_to_level_with_on_off( result = await self._level_channel.move_to_level_with_on_off(
@ -389,6 +411,10 @@ class BaseLight(LogMixin, light.LightEntity):
) )
else: else:
result = await self._on_off_channel.off() result = await self._on_off_channel.off()
# Pause parsing attribute reports until transition is complete
if self._zha_config_enable_light_transitioning_flag:
self.async_transition_start_timer(transition_time)
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
@ -401,6 +427,127 @@ class BaseLight(LogMixin, light.LightEntity):
self.async_write_ha_state() self.async_write_ha_state()
async def async_handle_color_commands(
self,
temperature,
duration,
hs_color,
xy_color,
new_color_provided_while_off,
t_log,
):
"""Process ZCL color commands."""
if temperature is not None:
result = await self._color_channel.move_to_color_temp(
temperature,
self._DEFAULT_MIN_TRANSITION_TIME
if new_color_provided_while_off
else duration,
)
t_log["move_to_color_temp"] = result
if isinstance(result, Exception) or result[1] is not Status.SUCCESS:
return False
self._attr_color_mode = ColorMode.COLOR_TEMP
self._attr_color_temp = temperature
self._attr_xy_color = None
self._attr_hs_color = None
if hs_color is not None:
if (
not isinstance(self, LightGroup)
and 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:
return False
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(
int(xy_color[0] * 65535),
int(xy_color[1] * 65535),
self._DEFAULT_MIN_TRANSITION_TIME
if new_color_provided_while_off
else duration,
)
t_log["move_to_color"] = result
if isinstance(result, Exception) or result[1] is not Status.SUCCESS:
return False
self._attr_color_mode = ColorMode.XY
self._attr_xy_color = xy_color
self._attr_color_temp = None
self._attr_hs_color = None
return True
@callback
def async_transition_set_flag(self) -> None:
"""Set _transitioning to True."""
self.debug("setting transitioning flag to True")
self._transitioning = True
if isinstance(self, LightGroup):
async_dispatcher_send(
self.hass,
SIGNAL_LIGHT_GROUP_TRANSITION_START,
{"entity_ids": self._entity_ids},
)
if self._transition_listener is not None:
self._transition_listener()
@callback
def async_transition_start_timer(self, transition_time) -> None:
"""Start a timer to unset _transitioning after transition_time if necessary."""
if not transition_time:
return
# For longer transitions, we want to extend the timer a bit more
if transition_time >= DEFAULT_LONG_TRANSITION_TIME:
transition_time += DEFAULT_EXTRA_TRANSITION_DELAY_LONG
self.debug("starting transitioning timer for %s", transition_time)
self._transition_listener = async_call_later(
self._zha_device.hass,
transition_time,
self.async_transition_complete,
)
@callback
def async_transition_complete(self, _=None) -> None:
"""Set _transitioning to False and write HA state."""
self.debug("transition complete - future attribute reports will write HA state")
self._transitioning = False
if self._transition_listener:
self._transition_listener()
self._transition_listener = None
self.async_write_ha_state()
if isinstance(self, LightGroup):
async_dispatcher_send(
self.hass,
SIGNAL_LIGHT_GROUP_TRANSITION_FINISHED,
{"entity_ids": self._entity_ids},
)
if self._debounced_member_refresh is not None:
self.debug("transition complete - refreshing group member states")
asyncio.create_task(self._debounced_member_refresh.async_call())
@STRICT_MATCH(channel_names=CHANNEL_ON_OFF, aux_channels={CHANNEL_COLOR, CHANNEL_LEVEL}) @STRICT_MATCH(channel_names=CHANNEL_ON_OFF, aux_channels={CHANNEL_COLOR, CHANNEL_LEVEL})
class Light(BaseLight, ZhaEntity): class Light(BaseLight, ZhaEntity):
@ -506,10 +653,22 @@ class Light(BaseLight, ZhaEntity):
CONF_ENABLE_ENHANCED_LIGHT_TRANSITION, CONF_ENABLE_ENHANCED_LIGHT_TRANSITION,
False, False,
) )
self._zha_config_enable_light_transitioning_flag = async_get_zha_config_value(
zha_device.gateway.config_entry,
ZHA_OPTIONS,
CONF_ENABLE_LIGHT_TRANSITIONING_FLAG,
True,
)
@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."""
if self._transitioning:
self.debug(
"received onoff %s while transitioning - skipping update",
value,
)
return
self._attr_state = bool(value) self._attr_state = bool(value)
if value: if value:
self._off_with_transition = False self._off_with_transition = False
@ -537,6 +696,38 @@ class Light(BaseLight, ZhaEntity):
signal_override=True, signal_override=True,
) )
@callback
def transition_on(signal):
"""Handle a transition start event from a group."""
if self.entity_id in signal["entity_ids"]:
self.debug(
"group transition started - setting member transitioning flag"
)
self._transitioning = True
self.async_accept_signal(
None,
SIGNAL_LIGHT_GROUP_TRANSITION_START,
transition_on,
signal_override=True,
)
@callback
def transition_off(signal):
"""Handle a transition finished event from a group."""
if self.entity_id in signal["entity_ids"]:
self.debug(
"group transition completed - unsetting member transitioning flag"
)
self._transitioning = False
self.async_accept_signal(
None,
SIGNAL_LIGHT_GROUP_TRANSITION_FINISHED,
transition_off,
signal_override=True,
)
async def async_will_remove_from_hass(self) -> None: async def async_will_remove_from_hass(self) -> None:
"""Disconnect entity object when removed.""" """Disconnect entity object when removed."""
assert self._cancel_refresh_handle assert self._cancel_refresh_handle
@ -654,16 +845,25 @@ class Light(BaseLight, ZhaEntity):
async def async_update(self): async def async_update(self):
"""Update to the latest state.""" """Update to the latest state."""
if self._transitioning:
self.debug("skipping async_update while transitioning")
return
await self.async_get_state() await self.async_get_state()
async def _refresh(self, time): async def _refresh(self, time):
"""Call async_get_state at an interval.""" """Call async_get_state at an interval."""
if self._transitioning:
self.debug("skipping _refresh while transitioning")
return
await self.async_get_state() await self.async_get_state()
self.async_write_ha_state() self.async_write_ha_state()
async def _maybe_force_refresh(self, signal): async def _maybe_force_refresh(self, signal):
"""Force update the state if the signal contains the entity id for this entity.""" """Force update the state if the signal contains the entity id for this entity."""
if self.entity_id in signal["entity_ids"]: if self.entity_id in signal["entity_ids"]:
if self._transitioning:
self.debug("skipping _maybe_force_refresh while transitioning")
return
await self.async_get_state() await self.async_get_state()
self.async_write_ha_state() self.async_write_ha_state()
@ -726,6 +926,12 @@ class LightGroup(BaseLight, ZhaGroupEntity):
CONF_DEFAULT_LIGHT_TRANSITION, CONF_DEFAULT_LIGHT_TRANSITION,
0, 0,
) )
self._zha_config_enable_light_transitioning_flag = async_get_zha_config_value(
zha_device.gateway.config_entry,
ZHA_OPTIONS,
CONF_ENABLE_LIGHT_TRANSITIONING_FLAG,
True,
)
self._zha_config_always_prefer_xy_color_mode = async_get_zha_config_value( self._zha_config_always_prefer_xy_color_mode = async_get_zha_config_value(
zha_device.gateway.config_entry, zha_device.gateway.config_entry,
ZHA_OPTIONS, ZHA_OPTIONS,
@ -757,13 +963,32 @@ class LightGroup(BaseLight, ZhaGroupEntity):
async def async_turn_on(self, **kwargs): async def async_turn_on(self, **kwargs):
"""Turn the entity on.""" """Turn the entity on."""
await super().async_turn_on(**kwargs) await super().async_turn_on(**kwargs)
if self._transitioning:
return
await self._debounced_member_refresh.async_call() await self._debounced_member_refresh.async_call()
async def async_turn_off(self, **kwargs): async def async_turn_off(self, **kwargs):
"""Turn the entity off.""" """Turn the entity off."""
await super().async_turn_off(**kwargs) await super().async_turn_off(**kwargs)
if self._transitioning:
return
await self._debounced_member_refresh.async_call() await self._debounced_member_refresh.async_call()
@callback
def async_state_changed_listener(self, event: Event):
"""Handle child updates."""
if self._transitioning:
self.debug("skipping group entity state update during transition")
return
super().async_state_changed_listener(event)
async def async_update_ha_state(self, force_refresh: bool = False) -> None:
"""Update Home Assistant with current state of entity."""
if self._transitioning:
self.debug("skipping group entity state update during transition")
return
await super().async_update_ha_state(force_refresh)
async def async_update(self) -> None: async def async_update(self) -> None:
"""Query all members and determine the light group state.""" """Query all members and determine the light group state."""
all_states = [self.hass.states.get(x) for x in self._entity_ids] all_states = [self.hass.states.get(x) for x in self._entity_ids]

View File

@ -38,6 +38,7 @@
"zha_options": { "zha_options": {
"title": "Global Options", "title": "Global Options",
"enhanced_light_transition": "Enable enhanced light color/temperature transition from an off-state", "enhanced_light_transition": "Enable enhanced light color/temperature transition from an off-state",
"light_transitioning_flag": "Enable enhanced brightness slider during light transition",
"always_prefer_xy_color_mode": "Always prefer XY color mode", "always_prefer_xy_color_mode": "Always prefer XY color mode",
"enable_identify_on_join": "Enable identify effect when devices join the network", "enable_identify_on_join": "Enable identify effect when devices join the network",
"default_light_transition": "Default light transition time (seconds)", "default_light_transition": "Default light transition time (seconds)",

View File

@ -46,12 +46,13 @@
"title": "Alarm Control Panel Options" "title": "Alarm Control Panel Options"
}, },
"zha_options": { "zha_options": {
"always_prefer_xy_color_mode": "Always prefer XY color mode",
"consider_unavailable_battery": "Consider battery powered devices unavailable after (seconds)", "consider_unavailable_battery": "Consider battery powered devices unavailable after (seconds)",
"consider_unavailable_mains": "Consider mains powered devices unavailable after (seconds)", "consider_unavailable_mains": "Consider mains powered devices unavailable after (seconds)",
"default_light_transition": "Default light transition time (seconds)", "default_light_transition": "Default light transition time (seconds)",
"enable_identify_on_join": "Enable identify effect when devices join the network", "enable_identify_on_join": "Enable identify effect when devices join the network",
"enhanced_light_transition": "Enable enhanced light color/temperature transition from an off-state", "enhanced_light_transition": "Enable enhanced light color/temperature transition from an off-state",
"always_prefer_xy_color_mode": "Always prefer XY color mode", "light_transitioning_flag": "Enable enhanced brightness slider during light transition",
"title": "Global Options" "title": "Global Options"
} }
}, },

View File

@ -1,5 +1,6 @@
"""Common test objects.""" """Common test objects."""
import asyncio import asyncio
from datetime import timedelta
import math import math
from unittest.mock import AsyncMock, Mock from unittest.mock import AsyncMock, Mock
@ -8,6 +9,9 @@ import zigpy.zcl.foundation as zcl_f
import homeassistant.components.zha.core.const as zha_const import homeassistant.components.zha.core.const as zha_const
from homeassistant.helpers import entity_registry from homeassistant.helpers import entity_registry
import homeassistant.util.dt as dt_util
from tests.common import async_fire_time_changed
def patch_cluster(cluster): def patch_cluster(cluster):
@ -232,3 +236,10 @@ async def async_wait_for_updates(hass):
await asyncio.sleep(0) await asyncio.sleep(0)
await asyncio.sleep(0) await asyncio.sleep(0)
await hass.async_block_till_done() await hass.async_block_till_done()
async def async_shift_time(hass):
"""Shift time to cause call later tasks to run."""
next_update = dt_util.utcnow() + timedelta(seconds=11)
async_fire_time_changed(hass, next_update)
await hass.async_block_till_done()

View File

@ -22,6 +22,7 @@ import homeassistant.util.dt as dt_util
from .common import ( from .common import (
async_enable_traffic, async_enable_traffic,
async_find_group_entity_id, async_find_group_entity_id,
async_shift_time,
async_test_rejoin, async_test_rejoin,
find_entity_id, find_entity_id,
get_zha_gateway, get_zha_gateway,
@ -346,6 +347,7 @@ async def test_light(
await async_test_level_on_off_from_hass( await async_test_level_on_off_from_hass(
hass, cluster_on_off, cluster_level, entity_id hass, cluster_on_off, cluster_level, entity_id
) )
await async_shift_time(hass)
# test getting a brightness change from the network # test getting a brightness change from the network
await async_test_on_from_light(hass, cluster_on_off, entity_id) await async_test_on_from_light(hass, cluster_on_off, entity_id)
@ -1190,6 +1192,8 @@ async def async_test_level_on_off_from_hass(
on_off_cluster.request.reset_mock() on_off_cluster.request.reset_mock()
level_cluster.request.reset_mock() level_cluster.request.reset_mock()
await async_shift_time(hass)
# turn on via UI # turn on via UI
await hass.services.async_call( await hass.services.async_call(
LIGHT_DOMAIN, "turn_on", {"entity_id": entity_id}, blocking=True LIGHT_DOMAIN, "turn_on", {"entity_id": entity_id}, blocking=True
@ -1210,6 +1214,8 @@ async def async_test_level_on_off_from_hass(
on_off_cluster.request.reset_mock() on_off_cluster.request.reset_mock()
level_cluster.request.reset_mock() level_cluster.request.reset_mock()
await async_shift_time(hass)
await hass.services.async_call( await hass.services.async_call(
LIGHT_DOMAIN, LIGHT_DOMAIN,
"turn_on", "turn_on",
@ -1407,11 +1413,15 @@ async def test_zha_group_light_entity(
# test turning the lights on and off from the HA # test turning the lights on and off from the HA
await async_test_on_off_from_hass(hass, group_cluster_on_off, group_entity_id) await async_test_on_off_from_hass(hass, group_cluster_on_off, group_entity_id)
await async_shift_time(hass)
# test short flashing the lights from the HA # test short flashing the lights from the HA
await async_test_flash_from_hass( await async_test_flash_from_hass(
hass, group_cluster_identify, group_entity_id, FLASH_SHORT hass, group_cluster_identify, group_entity_id, FLASH_SHORT
) )
await async_shift_time(hass)
# test turning the lights on and off from the light # test turning the lights on and off from the light
await async_test_on_off_from_light(hass, dev1_cluster_on_off, group_entity_id) await async_test_on_off_from_light(hass, dev1_cluster_on_off, group_entity_id)
@ -1424,6 +1434,8 @@ async def test_zha_group_light_entity(
expected_default_transition=1, # a Sengled light is in that group and needs a minimum 0.1s transition expected_default_transition=1, # a Sengled light is in that group and needs a minimum 0.1s transition
) )
await async_shift_time(hass)
# test getting a brightness change from the network # test getting a brightness change from the network
await async_test_on_from_light(hass, dev1_cluster_on_off, group_entity_id) await async_test_on_from_light(hass, dev1_cluster_on_off, group_entity_id)
await async_test_dimmer_from_light( await async_test_dimmer_from_light(
@ -1443,6 +1455,8 @@ async def test_zha_group_light_entity(
hass, group_cluster_identify, group_entity_id, FLASH_LONG hass, group_cluster_identify, group_entity_id, FLASH_LONG
) )
await async_shift_time(hass)
assert len(zha_group.members) == 2 assert len(zha_group.members) == 2
# test some of the group logic to make sure we key off states correctly # test some of the group logic to make sure we key off states correctly
await send_attributes_report(hass, dev1_cluster_on_off, {0: 1}) await send_attributes_report(hass, dev1_cluster_on_off, {0: 1})