mirror of
https://github.com/home-assistant/core.git
synced 2025-07-13 08:17:08 +00:00
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:
parent
6868865e3e
commit
129b42cd23
@ -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(
|
||||
|
@ -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(
|
||||
if not await self.async_handle_color_commands(
|
||||
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
|
||||
duration,
|
||||
hs_color,
|
||||
xy_color,
|
||||
new_color_provided_while_off,
|
||||
t_log,
|
||||
):
|
||||
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:
|
||||
# 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
|
||||
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:
|
||||
# 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]
|
||||
|
@ -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)",
|
||||
|
@ -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"
|
||||
}
|
||||
},
|
||||
|
@ -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()
|
||||
|
@ -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})
|
||||
|
Loading…
x
Reference in New Issue
Block a user