mirror of
https://github.com/home-assistant/core.git
synced 2025-07-22 12:47:08 +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_capabilities": True,
|
||||||
"color_loop_active": False,
|
"color_loop_active": False,
|
||||||
"start_up_color_temperature": True,
|
"start_up_color_temperature": True,
|
||||||
|
"options": True,
|
||||||
}
|
}
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
@ -167,3 +168,13 @@ class ColorChannel(ZigbeeChannel):
|
|||||||
self.color_capabilities is not None
|
self.color_capabilities is not None
|
||||||
and lighting.Color.ColorCapabilities.Color_loop in self.color_capabilities
|
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)
|
xy_color = kwargs.get(light.ATTR_XY_COLOR)
|
||||||
hs_color = kwargs.get(light.ATTR_HS_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 = (
|
set_transition_flag = (
|
||||||
brightness_supported(self._attr_supported_color_modes)
|
brightness_supported(self._attr_supported_color_modes)
|
||||||
or temperature is not None
|
or temperature is not None
|
||||||
@ -254,6 +260,7 @@ class BaseLight(LogMixin, light.LightEntity):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
and brightness_supported(self._attr_supported_color_modes)
|
and brightness_supported(self._attr_supported_color_modes)
|
||||||
|
and not execute_if_off_supported
|
||||||
)
|
)
|
||||||
|
|
||||||
if (
|
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
|
# 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
|
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 (
|
if (
|
||||||
(brightness is not None or transition)
|
(brightness is not None or transition)
|
||||||
and not new_color_provided_while_off
|
and not new_color_provided_while_off
|
||||||
@ -326,18 +350,20 @@ class BaseLight(LogMixin, light.LightEntity):
|
|||||||
return
|
return
|
||||||
self._attr_state = True
|
self._attr_state = True
|
||||||
|
|
||||||
if not await self.async_handle_color_commands(
|
if not execute_if_off_supported:
|
||||||
temperature,
|
self.debug("handling color commands after turning on/level")
|
||||||
duration,
|
if not await self.async_handle_color_commands(
|
||||||
hs_color,
|
temperature,
|
||||||
xy_color,
|
duration,
|
||||||
new_color_provided_while_off,
|
hs_color,
|
||||||
t_log,
|
xy_color,
|
||||||
):
|
new_color_provided_while_off,
|
||||||
# Color calls failed, but as brightness may still transition, we start the timer to unset the flag
|
t_log,
|
||||||
self.async_transition_start_timer(transition_time)
|
):
|
||||||
self.debug("turned on: %s", t_log)
|
# Color calls failed, but as brightness may still transition, we start the timer to unset the flag
|
||||||
return
|
self.async_transition_start_timer(transition_time)
|
||||||
|
self.debug("turned on: %s", t_log)
|
||||||
|
return
|
||||||
|
|
||||||
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.
|
||||||
|
@ -34,6 +34,7 @@ from .common import (
|
|||||||
get_zha_gateway,
|
get_zha_gateway,
|
||||||
patch_zha_config,
|
patch_zha_config,
|
||||||
send_attributes_report,
|
send_attributes_report,
|
||||||
|
update_attribute_cache,
|
||||||
)
|
)
|
||||||
from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_PROFILE, SIG_EP_TYPE
|
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
|
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):
|
async def async_test_on_off_from_light(hass, cluster, entity_id):
|
||||||
"""Test on off functionality from the light."""
|
"""Test on off functionality from the light."""
|
||||||
# turn on at light
|
# turn on at light
|
||||||
|
@ -358,6 +358,7 @@ async def test_color_number(
|
|||||||
"color_temp_physical_max",
|
"color_temp_physical_max",
|
||||||
"color_capabilities",
|
"color_capabilities",
|
||||||
"start_up_color_temperature",
|
"start_up_color_temperature",
|
||||||
|
"options",
|
||||||
],
|
],
|
||||||
allow_cache=True,
|
allow_cache=True,
|
||||||
only_cache=False,
|
only_cache=False,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user