diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py index c72b6373ad0..4c8c6e03c79 100644 --- a/homeassistant/components/zha/core/const.py +++ b/homeassistant/components/zha/core/const.py @@ -129,6 +129,7 @@ CONF_DATABASE = "database_path" CONF_DEFAULT_LIGHT_TRANSITION = "default_light_transition" CONF_DEVICE_CONFIG = "device_config" 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_ENABLE_IDENTIFY_ON_JOIN = "enable_identify_on_join" 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.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_ENABLE_IDENTIFY_ON_JOIN, default=True): cv.boolean, vol.Optional( diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index 23552c0f469..9fc089e2241 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -1,7 +1,9 @@ """Lights on Zigbee Home Automation networks.""" from __future__ import annotations +import asyncio from collections import Counter +from collections.abc import Callable from datetime import timedelta import functools import itertools @@ -26,14 +28,14 @@ from homeassistant.const import ( STATE_UNAVAILABLE, 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.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) 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.const import ( @@ -43,6 +45,7 @@ from .core.const import ( CONF_ALWAYS_PREFER_XY_COLOR_MODE, CONF_DEFAULT_LIGHT_TRANSITION, CONF_ENABLE_ENHANCED_LIGHT_TRANSITION, + CONF_ENABLE_LIGHT_TRANSITIONING_FLAG, DATA_ZHA, EFFECT_BLINK, EFFECT_BREATHE, @@ -61,6 +64,10 @@ if TYPE_CHECKING: _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 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) PARALLEL_UPDATES = 0 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"} COLOR_MODES_GROUP_LIGHT = {ColorMode.COLOR_TEMP, ColorMode.XY} @@ -111,6 +120,7 @@ class BaseLight(LogMixin, light.LightEntity): def __init__(self, *args, **kwargs): """Initialize the light.""" + self._zha_device: ZHADevice = None super().__init__(*args, **kwargs) self._attr_min_mireds: int | None = 153 self._attr_max_mireds: int | None = 500 @@ -121,11 +131,14 @@ class BaseLight(LogMixin, light.LightEntity): self._off_brightness: int | None = None self._zha_config_transition = self._DEFAULT_MIN_TRANSITION_TIME 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._on_off_channel = None self._level_channel = None self._color_channel = None self._identify_channel = None + self._transitioning: bool = False + self._transition_listener: Callable[[], None] | None = None @property 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 level """ + if self._transitioning: + self.debug( + "received level %s while transitioning - skipping update", + value, + ) + return value = max(0, min(254, value)) self._attr_brightness = value self.async_write_ha_state() @@ -170,6 +189,36 @@ class BaseLight(LogMixin, light.LightEntity): xy_color = kwargs.get(light.ATTR_XY_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, # 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. @@ -230,6 +279,10 @@ class BaseLight(LogMixin, light.LightEntity): ) t_log["move_to_level_with_on_off"] = result 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) return # 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 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) return self._attr_state = bool(level) @@ -261,73 +318,25 @@ class BaseLight(LogMixin, light.LightEntity): result = await self._on_off_channel.on() t_log["on_off"] = result 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) return self._attr_state = True - 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: - self.debug("turned on: %s", t_log) - return - 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: - 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 not await self.async_handle_color_commands( + temperature, + duration, + hs_color, + xy_color, + new_color_provided_while_off, + t_log, + ): + # Color calls failed, but as brightness may still transition, we start the timer to unset the flag + self.async_transition_start_timer(transition_time) + self.debug("turned on: %s", t_log) + return if new_color_provided_while_off: # 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: 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: result = await self._color_channel.color_loop_set( UPDATE_COLORLOOP_ACTION @@ -382,6 +395,15 @@ class BaseLight(LogMixin, light.LightEntity): transition = kwargs.get(light.ATTR_TRANSITION) 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 if transition is not None and supports_level: result = await self._level_channel.move_to_level_with_on_off( @@ -389,6 +411,10 @@ class BaseLight(LogMixin, light.LightEntity): ) else: 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) if isinstance(result, Exception) or result[1] is not Status.SUCCESS: return @@ -401,6 +427,127 @@ class BaseLight(LogMixin, light.LightEntity): 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}) class Light(BaseLight, ZhaEntity): @@ -506,10 +653,22 @@ class Light(BaseLight, ZhaEntity): CONF_ENABLE_ENHANCED_LIGHT_TRANSITION, 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 def async_set_state(self, attr_id, attr_name, value): """Set the state.""" + if self._transitioning: + self.debug( + "received onoff %s while transitioning - skipping update", + value, + ) + return self._attr_state = bool(value) if value: self._off_with_transition = False @@ -537,6 +696,38 @@ class Light(BaseLight, ZhaEntity): 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: """Disconnect entity object when removed.""" assert self._cancel_refresh_handle @@ -654,16 +845,25 @@ class Light(BaseLight, ZhaEntity): async def async_update(self): """Update to the latest state.""" + if self._transitioning: + self.debug("skipping async_update while transitioning") + return await self.async_get_state() async def _refresh(self, time): """Call async_get_state at an interval.""" + if self._transitioning: + self.debug("skipping _refresh while transitioning") + return await self.async_get_state() self.async_write_ha_state() async def _maybe_force_refresh(self, signal): """Force update the state if the signal contains the entity id for this entity.""" 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() self.async_write_ha_state() @@ -726,6 +926,12 @@ class LightGroup(BaseLight, ZhaGroupEntity): CONF_DEFAULT_LIGHT_TRANSITION, 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( zha_device.gateway.config_entry, ZHA_OPTIONS, @@ -757,13 +963,32 @@ class LightGroup(BaseLight, ZhaGroupEntity): async def async_turn_on(self, **kwargs): """Turn the entity on.""" await super().async_turn_on(**kwargs) + if self._transitioning: + return await self._debounced_member_refresh.async_call() async def async_turn_off(self, **kwargs): """Turn the entity off.""" await super().async_turn_off(**kwargs) + if self._transitioning: + return 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: """Query all members and determine the light group state.""" all_states = [self.hass.states.get(x) for x in self._entity_ids] diff --git a/homeassistant/components/zha/strings.json b/homeassistant/components/zha/strings.json index f5321565be0..4eb872f4fae 100644 --- a/homeassistant/components/zha/strings.json +++ b/homeassistant/components/zha/strings.json @@ -38,6 +38,7 @@ "zha_options": { "title": "Global Options", "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", "enable_identify_on_join": "Enable identify effect when devices join the network", "default_light_transition": "Default light transition time (seconds)", diff --git a/homeassistant/components/zha/translations/en.json b/homeassistant/components/zha/translations/en.json index 0a7795a47b4..757ab338ec6 100644 --- a/homeassistant/components/zha/translations/en.json +++ b/homeassistant/components/zha/translations/en.json @@ -46,12 +46,13 @@ "title": "Alarm Control Panel 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_mains": "Consider mains powered devices unavailable after (seconds)", "default_light_transition": "Default light transition time (seconds)", "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", - "always_prefer_xy_color_mode": "Always prefer XY color mode", + "light_transitioning_flag": "Enable enhanced brightness slider during light transition", "title": "Global Options" } }, diff --git a/tests/components/zha/common.py b/tests/components/zha/common.py index 6a51f441a78..cad8020267f 100644 --- a/tests/components/zha/common.py +++ b/tests/components/zha/common.py @@ -1,5 +1,6 @@ """Common test objects.""" import asyncio +from datetime import timedelta import math 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 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): @@ -232,3 +236,10 @@ async def async_wait_for_updates(hass): await asyncio.sleep(0) await asyncio.sleep(0) 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() diff --git a/tests/components/zha/test_light.py b/tests/components/zha/test_light.py index 94f0c96c38d..156f692aa14 100644 --- a/tests/components/zha/test_light.py +++ b/tests/components/zha/test_light.py @@ -22,6 +22,7 @@ import homeassistant.util.dt as dt_util from .common import ( async_enable_traffic, async_find_group_entity_id, + async_shift_time, async_test_rejoin, find_entity_id, get_zha_gateway, @@ -346,6 +347,7 @@ async def test_light( await async_test_level_on_off_from_hass( hass, cluster_on_off, cluster_level, entity_id ) + await async_shift_time(hass) # test getting a brightness change from the network 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() level_cluster.request.reset_mock() + await async_shift_time(hass) + # turn on via UI await hass.services.async_call( 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() level_cluster.request.reset_mock() + await async_shift_time(hass) + await hass.services.async_call( LIGHT_DOMAIN, "turn_on", @@ -1407,11 +1413,15 @@ async def test_zha_group_light_entity( # 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_shift_time(hass) + # test short flashing the lights from the HA await async_test_flash_from_hass( hass, group_cluster_identify, group_entity_id, FLASH_SHORT ) + await async_shift_time(hass) + # 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) @@ -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 ) + await async_shift_time(hass) + # 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_dimmer_from_light( @@ -1443,6 +1455,8 @@ async def test_zha_group_light_entity( hass, group_cluster_identify, group_entity_id, FLASH_LONG ) + await async_shift_time(hass) + assert len(zha_group.members) == 2 # 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})