mirror of
https://github.com/home-assistant/core.git
synced 2025-07-20 19:57:07 +00:00
Use ExecuteIfOff on color cluster for supported bulbs with ZHA (#84874)
* Add options and execute_if_off_supported properties to Color channel * Initialize "options" attribute on Color channel (allowing cache) * Implement execute_if_off_supported for ZHA lights * Make sure that color_channel exists, before checking execute_if_off_supported * Replace "color_channel is not None" check with simplified "if color_channel" * Make "test_number" test expect "options" for init attribute * Add test_on_with_off_color test to test old and new behavior * Experimental code to also support "execute_if_off" for groups if all members support it * Remove support for groups for now Group support will likely be added in a separate PR. For now, the old/standard behavior is used for groups.
This commit is contained in:
parent
29e3d06a42
commit
6582ee3591
@ -45,6 +45,7 @@ class ColorChannel(ZigbeeChannel):
|
||||
"color_capabilities": True,
|
||||
"color_loop_active": False,
|
||||
"start_up_color_temperature": True,
|
||||
"options": True,
|
||||
}
|
||||
|
||||
@cached_property
|
||||
@ -167,3 +168,13 @@ class ColorChannel(ZigbeeChannel):
|
||||
self.color_capabilities is not None
|
||||
and lighting.Color.ColorCapabilities.Color_loop in self.color_capabilities
|
||||
)
|
||||
|
||||
@property
|
||||
def options(self) -> lighting.Color.Options:
|
||||
"""Return ZCL options of the channel."""
|
||||
return lighting.Color.Options(self.cluster.get("options", 0))
|
||||
|
||||
@property
|
||||
def execute_if_off_supported(self) -> bool:
|
||||
"""Return True if the channel can execute commands when off."""
|
||||
return lighting.Color.Options.Execute_if_off in self.options
|
||||
|
@ -188,6 +188,12 @@ class BaseLight(LogMixin, light.LightEntity):
|
||||
xy_color = kwargs.get(light.ATTR_XY_COLOR)
|
||||
hs_color = kwargs.get(light.ATTR_HS_COLOR)
|
||||
|
||||
execute_if_off_supported = (
|
||||
not isinstance(self, LightGroup)
|
||||
and self._color_channel
|
||||
and self._color_channel.execute_if_off_supported
|
||||
)
|
||||
|
||||
set_transition_flag = (
|
||||
brightness_supported(self._attr_supported_color_modes)
|
||||
or temperature is not None
|
||||
@ -254,6 +260,7 @@ class BaseLight(LogMixin, light.LightEntity):
|
||||
)
|
||||
)
|
||||
and brightness_supported(self._attr_supported_color_modes)
|
||||
and not execute_if_off_supported
|
||||
)
|
||||
|
||||
if (
|
||||
@ -288,6 +295,23 @@ class BaseLight(LogMixin, light.LightEntity):
|
||||
# Currently only setting it to "on", as the correct level state will be set at the second move_to_level call
|
||||
self._attr_state = True
|
||||
|
||||
if execute_if_off_supported:
|
||||
self.debug("handling color commands before turning on/level")
|
||||
if not await self.async_handle_color_commands(
|
||||
temperature,
|
||||
duration, # duration is ignored by lights when off
|
||||
hs_color,
|
||||
xy_color,
|
||||
new_color_provided_while_off,
|
||||
t_log,
|
||||
):
|
||||
# Color calls before on/level calls 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
|
||||
|
||||
if (
|
||||
(brightness is not None or transition)
|
||||
and not new_color_provided_while_off
|
||||
@ -326,18 +350,20 @@ class BaseLight(LogMixin, light.LightEntity):
|
||||
return
|
||||
self._attr_state = True
|
||||
|
||||
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 not execute_if_off_supported:
|
||||
self.debug("handling color commands after turning on/level")
|
||||
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.
|
||||
|
@ -34,6 +34,7 @@ from .common import (
|
||||
get_zha_gateway,
|
||||
patch_zha_config,
|
||||
send_attributes_report,
|
||||
update_attribute_cache,
|
||||
)
|
||||
from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_PROFILE, SIG_EP_TYPE
|
||||
|
||||
@ -1199,6 +1200,151 @@ async def test_transitions(
|
||||
assert eWeLink_state.attributes["max_mireds"] == 500
|
||||
|
||||
|
||||
@patch(
|
||||
"zigpy.zcl.clusters.lighting.Color.request",
|
||||
new=AsyncMock(return_value=[sentinel.data, zcl_f.Status.SUCCESS]),
|
||||
)
|
||||
@patch(
|
||||
"zigpy.zcl.clusters.general.LevelControl.request",
|
||||
new=AsyncMock(return_value=[sentinel.data, zcl_f.Status.SUCCESS]),
|
||||
)
|
||||
@patch(
|
||||
"zigpy.zcl.clusters.general.OnOff.request",
|
||||
new=AsyncMock(return_value=[sentinel.data, zcl_f.Status.SUCCESS]),
|
||||
)
|
||||
async def test_on_with_off_color(hass, device_light_1):
|
||||
"""Test turning on the light and sending color commands before on/level commands for supporting lights."""
|
||||
|
||||
device_1_entity_id = await find_entity_id(Platform.LIGHT, device_light_1, hass)
|
||||
dev1_cluster_on_off = device_light_1.device.endpoints[1].on_off
|
||||
dev1_cluster_level = device_light_1.device.endpoints[1].level
|
||||
dev1_cluster_color = device_light_1.device.endpoints[1].light_color
|
||||
|
||||
# Execute_if_off will override the "enhanced turn on from an off-state" config option that's enabled here
|
||||
dev1_cluster_color.PLUGGED_ATTR_READS = {
|
||||
"options": lighting.Color.Options.Execute_if_off
|
||||
}
|
||||
update_attribute_cache(dev1_cluster_color)
|
||||
|
||||
# turn on via UI
|
||||
dev1_cluster_on_off.request.reset_mock()
|
||||
dev1_cluster_level.request.reset_mock()
|
||||
dev1_cluster_color.request.reset_mock()
|
||||
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
"turn_on",
|
||||
{
|
||||
"entity_id": device_1_entity_id,
|
||||
"color_temp": 235,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert dev1_cluster_on_off.request.call_count == 1
|
||||
assert dev1_cluster_on_off.request.await_count == 1
|
||||
assert dev1_cluster_color.request.call_count == 1
|
||||
assert dev1_cluster_color.request.await_count == 1
|
||||
assert dev1_cluster_level.request.call_count == 0
|
||||
assert dev1_cluster_level.request.await_count == 0
|
||||
|
||||
assert dev1_cluster_on_off.request.call_args_list[0] == call(
|
||||
False,
|
||||
dev1_cluster_on_off.commands_by_name["on"].id,
|
||||
dev1_cluster_on_off.commands_by_name["on"].schema,
|
||||
expect_reply=True,
|
||||
manufacturer=None,
|
||||
tries=1,
|
||||
tsn=None,
|
||||
)
|
||||
assert dev1_cluster_color.request.call_args == call(
|
||||
False,
|
||||
dev1_cluster_color.commands_by_name["move_to_color_temp"].id,
|
||||
dev1_cluster_color.commands_by_name["move_to_color_temp"].schema,
|
||||
color_temp_mireds=235,
|
||||
transition_time=0,
|
||||
expect_reply=True,
|
||||
manufacturer=None,
|
||||
tries=1,
|
||||
tsn=None,
|
||||
)
|
||||
|
||||
light1_state = hass.states.get(device_1_entity_id)
|
||||
assert light1_state.state == STATE_ON
|
||||
assert light1_state.attributes["color_temp"] == 235
|
||||
assert light1_state.attributes["color_mode"] == ColorMode.COLOR_TEMP
|
||||
|
||||
# now let's turn off the Execute_if_off option and see if the old behavior is restored
|
||||
dev1_cluster_color.PLUGGED_ATTR_READS = {"options": 0}
|
||||
update_attribute_cache(dev1_cluster_color)
|
||||
|
||||
# turn off via UI, so the old "enhanced turn on from an off-state" behavior can do something
|
||||
await async_test_off_from_hass(hass, dev1_cluster_on_off, device_1_entity_id)
|
||||
|
||||
# turn on via UI (with a different color temp, so the "enhanced turn on" does something)
|
||||
dev1_cluster_on_off.request.reset_mock()
|
||||
dev1_cluster_level.request.reset_mock()
|
||||
dev1_cluster_color.request.reset_mock()
|
||||
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
"turn_on",
|
||||
{
|
||||
"entity_id": device_1_entity_id,
|
||||
"color_temp": 240,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert dev1_cluster_on_off.request.call_count == 0
|
||||
assert dev1_cluster_on_off.request.await_count == 0
|
||||
assert dev1_cluster_color.request.call_count == 1
|
||||
assert dev1_cluster_color.request.await_count == 1
|
||||
assert dev1_cluster_level.request.call_count == 2
|
||||
assert dev1_cluster_level.request.await_count == 2
|
||||
|
||||
# first it comes on with no transition at 2 brightness
|
||||
assert dev1_cluster_level.request.call_args_list[0] == call(
|
||||
False,
|
||||
dev1_cluster_level.commands_by_name["move_to_level_with_on_off"].id,
|
||||
dev1_cluster_level.commands_by_name["move_to_level_with_on_off"].schema,
|
||||
level=2,
|
||||
transition_time=0,
|
||||
expect_reply=True,
|
||||
manufacturer=None,
|
||||
tries=1,
|
||||
tsn=None,
|
||||
)
|
||||
assert dev1_cluster_color.request.call_args == call(
|
||||
False,
|
||||
dev1_cluster_color.commands_by_name["move_to_color_temp"].id,
|
||||
dev1_cluster_color.commands_by_name["move_to_color_temp"].schema,
|
||||
color_temp_mireds=240,
|
||||
transition_time=0,
|
||||
expect_reply=True,
|
||||
manufacturer=None,
|
||||
tries=1,
|
||||
tsn=None,
|
||||
)
|
||||
assert dev1_cluster_level.request.call_args_list[1] == call(
|
||||
False,
|
||||
dev1_cluster_level.commands_by_name["move_to_level"].id,
|
||||
dev1_cluster_level.commands_by_name["move_to_level"].schema,
|
||||
level=254,
|
||||
transition_time=0,
|
||||
expect_reply=True,
|
||||
manufacturer=None,
|
||||
tries=1,
|
||||
tsn=None,
|
||||
)
|
||||
|
||||
light1_state = hass.states.get(device_1_entity_id)
|
||||
assert light1_state.state == STATE_ON
|
||||
assert light1_state.attributes["brightness"] == 254
|
||||
assert light1_state.attributes["color_temp"] == 240
|
||||
assert light1_state.attributes["color_mode"] == ColorMode.COLOR_TEMP
|
||||
|
||||
|
||||
async def async_test_on_off_from_light(hass, cluster, entity_id):
|
||||
"""Test on off functionality from the light."""
|
||||
# turn on at light
|
||||
|
@ -358,6 +358,7 @@ async def test_color_number(
|
||||
"color_temp_physical_max",
|
||||
"color_capabilities",
|
||||
"start_up_color_temperature",
|
||||
"options",
|
||||
],
|
||||
allow_cache=True,
|
||||
only_cache=False,
|
||||
|
Loading…
x
Reference in New Issue
Block a user