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_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(
|
||||||
|
@ -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]
|
||||||
|
@ -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)",
|
||||||
|
@ -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"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -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()
|
||||||
|
@ -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})
|
||||||
|
Loading…
x
Reference in New Issue
Block a user