Fix ZHA light turn on issues (#75220)

* rename variable

* default transition is for color commands not level

* no extra command for groups

* don't transition color change when light off -> on

* clean up

* update condition

* fix condition again...

* simplify

* simplify

* missed one

* rename

* simplify

* rename

* tests

* color_provided_while_off with no changes

* fix missing flag clear

* more tests for transition scenarios

* add to comment

* fix comment

* don't transition when force on is set

* stale comment

* dont transition when colors don't change

* remove extra line

* remove debug print :)

* fix colors

* restore color to 65535 until investigated
pull/75528/head
David F. Mulcahey 2022-07-18 10:20:49 -04:00 committed by Franck Nijhof
parent a23b427025
commit fdaaed6523
No known key found for this signature in database
GPG Key ID: D62583BA8AB11CA3
2 changed files with 868 additions and 41 deletions

View File

@ -73,7 +73,6 @@ CAPABILITIES_COLOR_LOOP = 0x4
CAPABILITIES_COLOR_XY = 0x08
CAPABILITIES_COLOR_TEMP = 0x10
DEFAULT_TRANSITION = 1
DEFAULT_MIN_BRIGHTNESS = 2
UPDATE_COLORLOOP_ACTION = 0x1
@ -119,7 +118,7 @@ class BaseLight(LogMixin, light.LightEntity):
"""Operations common to all light entities."""
_FORCE_ON = False
_DEFAULT_COLOR_FROM_OFF_TRANSITION = 0
_DEFAULT_MIN_TRANSITION_TIME = 0
def __init__(self, *args, **kwargs):
"""Initialize the light."""
@ -140,7 +139,7 @@ class BaseLight(LogMixin, light.LightEntity):
self._level_channel = None
self._color_channel = None
self._identify_channel = None
self._default_transition = None
self._zha_config_transition = self._DEFAULT_MIN_TRANSITION_TIME
self._attr_color_mode = ColorMode.UNKNOWN # Set by sub classes
@property
@ -216,33 +215,49 @@ class BaseLight(LogMixin, light.LightEntity):
transition = kwargs.get(light.ATTR_TRANSITION)
duration = (
transition * 10
if transition
else self._default_transition * 10
if self._default_transition
else DEFAULT_TRANSITION
)
if transition is not None
else self._zha_config_transition * 10
) or self._DEFAULT_MIN_TRANSITION_TIME # if 0 is passed in some devices still need the minimum default
brightness = kwargs.get(light.ATTR_BRIGHTNESS)
effect = kwargs.get(light.ATTR_EFFECT)
flash = kwargs.get(light.ATTR_FLASH)
temperature = kwargs.get(light.ATTR_COLOR_TEMP)
hs_color = kwargs.get(light.ATTR_HS_COLOR)
# 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.
# Otherwise, the transition is from the color the light had before being turned on to the new color.
# This can look especially bad with transitions longer than a second.
color_provided_from_off = (
not self._state
# This can look especially bad with transitions longer than a second. We do not want to do this for
# devices that need to be forced to use the on command because we would end up with 4 commands sent:
# move to level, on, color, move to level... We also will not set this if the bulb is already in the
# desired color mode with the desired color or color temperature.
new_color_provided_while_off = (
not isinstance(self, LightGroup)
and not self._FORCE_ON
and not self._state
and (
(
temperature is not None
and (
self._color_temp != temperature
or self._attr_color_mode != ColorMode.COLOR_TEMP
)
)
or (
hs_color is not None
and (
self.hs_color != hs_color
or self._attr_color_mode != ColorMode.HS
)
)
)
and brightness_supported(self._attr_supported_color_modes)
and (light.ATTR_COLOR_TEMP in kwargs or light.ATTR_HS_COLOR in kwargs)
)
final_duration = duration
if color_provided_from_off:
# Set the duration for the color changing commands to 0.
duration = 0
if (
brightness is None
and (self._off_with_transition or color_provided_from_off)
and (self._off_with_transition or new_color_provided_while_off)
and self._off_brightness is not None
):
brightness = self._off_brightness
@ -254,11 +269,11 @@ class BaseLight(LogMixin, light.LightEntity):
t_log = {}
if color_provided_from_off:
if new_color_provided_while_off:
# If the light is currently off, we first need to turn it on at a low brightness level with no transition.
# After that, we set it to the desired color/temperature with no transition.
result = await self._level_channel.move_to_level_with_on_off(
DEFAULT_MIN_BRIGHTNESS, self._DEFAULT_COLOR_FROM_OFF_TRANSITION
DEFAULT_MIN_BRIGHTNESS, self._DEFAULT_MIN_TRANSITION_TIME
)
t_log["move_to_level_with_on_off"] = result
if isinstance(result, Exception) or result[1] is not Status.SUCCESS:
@ -269,7 +284,7 @@ class BaseLight(LogMixin, light.LightEntity):
if (
(brightness is not None or transition)
and not color_provided_from_off
and not new_color_provided_while_off
and brightness_supported(self._attr_supported_color_modes)
):
result = await self._level_channel.move_to_level_with_on_off(
@ -285,7 +300,7 @@ class BaseLight(LogMixin, light.LightEntity):
if (
brightness is None
and not color_provided_from_off
and not new_color_provided_while_off
or (self._FORCE_ON and brightness)
):
# since some lights don't always turn on with move_to_level_with_on_off,
@ -297,9 +312,13 @@ class BaseLight(LogMixin, light.LightEntity):
return
self._state = True
if light.ATTR_COLOR_TEMP in kwargs:
temperature = kwargs[light.ATTR_COLOR_TEMP]
result = await self._color_channel.move_to_color_temp(temperature, duration)
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)
@ -308,11 +327,14 @@ class BaseLight(LogMixin, light.LightEntity):
self._color_temp = temperature
self._hs_color = None
if light.ATTR_HS_COLOR in kwargs:
hs_color = kwargs[light.ATTR_HS_COLOR]
if hs_color is not None:
xy_color = color_util.color_hs_to_xy(*hs_color)
result = await self._color_channel.move_to_color(
int(xy_color[0] * 65535), int(xy_color[1] * 65535), duration
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:
@ -322,9 +344,9 @@ class BaseLight(LogMixin, light.LightEntity):
self._hs_color = hs_color
self._color_temp = None
if color_provided_from_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.
result = await self._level_channel.move_to_level(level, final_duration)
result = await self._level_channel.move_to_level(level, duration)
t_log["move_to_level_if_color"] = result
if isinstance(result, Exception) or result[1] is not Status.SUCCESS:
self.debug("turned on: %s", t_log)
@ -371,12 +393,13 @@ class BaseLight(LogMixin, light.LightEntity):
async def async_turn_off(self, **kwargs):
"""Turn the entity off."""
duration = kwargs.get(light.ATTR_TRANSITION)
transition = kwargs.get(light.ATTR_TRANSITION)
supports_level = brightness_supported(self._attr_supported_color_modes)
if duration and supports_level:
# 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(
0, duration * 10
0, transition * 10
)
else:
result = await self._on_off_channel.off()
@ -387,7 +410,7 @@ class BaseLight(LogMixin, light.LightEntity):
if supports_level:
# store current brightness so that the next turn_on uses it.
self._off_with_transition = bool(duration)
self._off_with_transition = transition is not None
self._off_brightness = self._brightness
self.async_write_ha_state()
@ -460,7 +483,7 @@ class Light(BaseLight, ZhaEntity):
if effect_list:
self._effect_list = effect_list
self._default_transition = async_get_zha_config_value(
self._zha_config_transition = async_get_zha_config_value(
zha_device.gateway.config_entry,
ZHA_OPTIONS,
CONF_DEFAULT_LIGHT_TRANSITION,
@ -472,6 +495,7 @@ class Light(BaseLight, ZhaEntity):
"""Set the state."""
self._state = bool(value)
if value:
self._off_with_transition = False
self._off_brightness = None
self.async_write_ha_state()
@ -605,7 +629,7 @@ class HueLight(Light):
@STRICT_MATCH(
channel_names=CHANNEL_ON_OFF,
aux_channels={CHANNEL_COLOR, CHANNEL_LEVEL},
manufacturers={"Jasco", "Quotra-Vision"},
manufacturers={"Jasco", "Quotra-Vision", "eWeLight", "eWeLink"},
)
class ForceOnLight(Light):
"""Representation of a light which does not respect move_to_level_with_on_off."""
@ -621,7 +645,7 @@ class ForceOnLight(Light):
class SengledLight(Light):
"""Representation of a Sengled light which does not react to move_to_color_temp with 0 as a transition."""
_DEFAULT_COLOR_FROM_OFF_TRANSITION = 1
_DEFAULT_MIN_TRANSITION_TIME = 1
@GROUP_MATCH()
@ -639,7 +663,7 @@ class LightGroup(BaseLight, ZhaGroupEntity):
self._color_channel = group.endpoint[Color.cluster_id]
self._identify_channel = group.endpoint[Identify.cluster_id]
self._debounced_member_refresh = None
self._default_transition = async_get_zha_config_value(
self._zha_config_transition = async_get_zha_config_value(
zha_device.gateway.config_entry,
ZHA_OPTIONS,
CONF_DEFAULT_LIGHT_TRANSITION,

View File

@ -15,7 +15,11 @@ from homeassistant.components.light import (
ColorMode,
)
from homeassistant.components.zha.core.group import GroupMember
from homeassistant.components.zha.light import FLASH_EFFECTS
from homeassistant.components.zha.light import (
CAPABILITIES_COLOR_TEMP,
CAPABILITIES_COLOR_XY,
FLASH_EFFECTS,
)
from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE, Platform
import homeassistant.util.dt as dt_util
@ -142,6 +146,10 @@ async def device_light_1(hass, zigpy_device_mock, zha_device_joined):
ieee=IEEE_GROUPABLE_DEVICE,
nwk=0xB79D,
)
color_cluster = zigpy_device.endpoints[1].light_color
color_cluster.PLUGGED_ATTR_READS = {
"color_capabilities": CAPABILITIES_COLOR_TEMP | CAPABILITIES_COLOR_XY
}
zha_device = await zha_device_joined(zigpy_device)
zha_device.available = True
return zha_device
@ -167,8 +175,13 @@ async def device_light_2(hass, zigpy_device_mock, zha_device_joined):
}
},
ieee=IEEE_GROUPABLE_DEVICE2,
manufacturer="Sengled",
nwk=0xC79E,
)
color_cluster = zigpy_device.endpoints[1].light_color
color_cluster.PLUGGED_ATTR_READS = {
"color_capabilities": CAPABILITIES_COLOR_TEMP | CAPABILITIES_COLOR_XY
}
zha_device = await zha_device_joined(zigpy_device)
zha_device.available = True
return zha_device
@ -201,6 +214,38 @@ async def device_light_3(hass, zigpy_device_mock, zha_device_joined):
return zha_device
@pytest.fixture
async def eWeLink_light(hass, zigpy_device_mock, zha_device_joined):
"""Mock eWeLink light."""
zigpy_device = zigpy_device_mock(
{
1: {
SIG_EP_INPUT: [
general.OnOff.cluster_id,
general.LevelControl.cluster_id,
lighting.Color.cluster_id,
general.Groups.cluster_id,
general.Identify.cluster_id,
],
SIG_EP_OUTPUT: [],
SIG_EP_TYPE: zha.DeviceType.COLOR_DIMMABLE_LIGHT,
SIG_EP_PROFILE: zha.PROFILE_ID,
}
},
ieee="03:2d:6f:00:0a:90:69:e3",
manufacturer="eWeLink",
nwk=0xB79D,
)
color_cluster = zigpy_device.endpoints[1].light_color
color_cluster.PLUGGED_ATTR_READS = {
"color_capabilities": CAPABILITIES_COLOR_TEMP | CAPABILITIES_COLOR_XY
}
zha_device = await zha_device_joined(zigpy_device)
zha_device.available = True
return zha_device
async def test_light_refresh(hass, zigpy_device_mock, zha_device_joined_restored):
"""Test zha light platform refresh."""
@ -323,6 +368,758 @@ async def test_light(
await async_test_flash_from_hass(hass, cluster_identify, entity_id, FLASH_LONG)
@patch(
"zigpy.zcl.clusters.lighting.Color.request",
new=AsyncMock(return_value=[sentinel.data, zcl_f.Status.SUCCESS]),
)
@patch(
"zigpy.zcl.clusters.general.Identify.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_transitions(
hass, device_light_1, device_light_2, eWeLink_light, coordinator
):
"""Test ZHA light transition code."""
zha_gateway = get_zha_gateway(hass)
assert zha_gateway is not None
zha_gateway.coordinator_zha_device = coordinator
coordinator._zha_gateway = zha_gateway
device_light_1._zha_gateway = zha_gateway
device_light_2._zha_gateway = zha_gateway
member_ieee_addresses = [device_light_1.ieee, device_light_2.ieee]
members = [GroupMember(device_light_1.ieee, 1), GroupMember(device_light_2.ieee, 1)]
assert coordinator.is_coordinator
# test creating a group with 2 members
zha_group = await zha_gateway.async_create_zigpy_group("Test Group", members)
await hass.async_block_till_done()
assert zha_group is not None
assert len(zha_group.members) == 2
for member in zha_group.members:
assert member.device.ieee in member_ieee_addresses
assert member.group == zha_group
assert member.endpoint is not None
device_1_entity_id = await find_entity_id(Platform.LIGHT, device_light_1, hass)
device_2_entity_id = await find_entity_id(Platform.LIGHT, device_light_2, hass)
eWeLink_light_entity_id = await find_entity_id(Platform.LIGHT, eWeLink_light, hass)
assert device_1_entity_id != device_2_entity_id
group_entity_id = async_find_group_entity_id(hass, Platform.LIGHT, zha_group)
assert hass.states.get(group_entity_id) is not None
assert device_1_entity_id in zha_group.member_entity_ids
assert device_2_entity_id in zha_group.member_entity_ids
dev1_cluster_on_off = device_light_1.device.endpoints[1].on_off
dev2_cluster_on_off = device_light_2.device.endpoints[1].on_off
eWeLink_cluster_on_off = eWeLink_light.device.endpoints[1].on_off
dev1_cluster_level = device_light_1.device.endpoints[1].level
dev2_cluster_level = device_light_2.device.endpoints[1].level
eWeLink_cluster_level = eWeLink_light.device.endpoints[1].level
dev1_cluster_color = device_light_1.device.endpoints[1].light_color
dev2_cluster_color = device_light_2.device.endpoints[1].light_color
eWeLink_cluster_color = eWeLink_light.device.endpoints[1].light_color
# allow traffic to flow through the gateway and device
await async_enable_traffic(hass, [device_light_1, device_light_2])
await async_wait_for_updates(hass)
# test that the lights were created and are off
group_state = hass.states.get(group_entity_id)
assert group_state.state == STATE_OFF
light1_state = hass.states.get(device_1_entity_id)
assert light1_state.state == STATE_OFF
light2_state = hass.states.get(device_2_entity_id)
assert light2_state.state == STATE_OFF
# first test 0 length transition with no color provided
dev1_cluster_on_off.request.reset_mock()
dev1_cluster_level.request.reset_mock()
await hass.services.async_call(
LIGHT_DOMAIN,
"turn_on",
{"entity_id": device_1_entity_id, "transition": 0, "brightness": 50},
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 == 0
assert dev1_cluster_color.request.await_count == 0
assert dev1_cluster_level.request.call_count == 1
assert dev1_cluster_level.request.await_count == 1
assert dev1_cluster_level.request.call_args == call(
False,
4,
dev1_cluster_level.commands_by_name["move_to_level_with_on_off"].schema,
50, # brightness (level in ZCL)
0, # transition time
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"] == 50
dev1_cluster_level.request.reset_mock()
# test non 0 length transition with color provided while light is on
await hass.services.async_call(
LIGHT_DOMAIN,
"turn_on",
{
"entity_id": device_1_entity_id,
"transition": 3,
"brightness": 18,
"color_temp": 432,
},
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 == 1
assert dev1_cluster_level.request.await_count == 1
assert dev1_cluster_level.request.call_args == call(
False,
4,
dev1_cluster_level.commands_by_name["move_to_level_with_on_off"].schema,
18, # brightness (level in ZCL)
30, # transition time (ZCL time in 10ths of a second)
expect_reply=True,
manufacturer=None,
tries=1,
tsn=None,
)
assert dev1_cluster_color.request.call_args == call(
False,
10,
dev1_cluster_color.commands_by_name["move_to_color_temp"].schema,
432, # color temp mireds
30.0, # transition time (ZCL time in 10ths of a second)
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"] == 18
assert light1_state.attributes["color_temp"] == 432
assert light1_state.attributes["color_mode"] == ColorMode.COLOR_TEMP
dev1_cluster_level.request.reset_mock()
dev1_cluster_color.request.reset_mock()
# test 0 length transition to turn light off
await hass.services.async_call(
LIGHT_DOMAIN,
"turn_off",
{
"entity_id": device_1_entity_id,
"transition": 0,
},
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 == 0
assert dev1_cluster_color.request.await_count == 0
assert dev1_cluster_level.request.call_count == 1
assert dev1_cluster_level.request.await_count == 1
assert dev1_cluster_level.request.call_args == call(
False,
4,
dev1_cluster_level.commands_by_name["move_to_level_with_on_off"].schema,
0, # brightness (level in ZCL)
0, # transition time (ZCL time in 10ths of a second)
expect_reply=True,
manufacturer=None,
tries=1,
tsn=None,
)
light1_state = hass.states.get(device_1_entity_id)
assert light1_state.state == STATE_OFF
dev1_cluster_level.request.reset_mock()
# test non 0 length transition and color temp while turning light on (color_provided_while_off)
await hass.services.async_call(
LIGHT_DOMAIN,
"turn_on",
{
"entity_id": device_1_entity_id,
"transition": 1,
"brightness": 25,
"color_temp": 235,
},
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,
4,
dev1_cluster_level.commands_by_name["move_to_level_with_on_off"].schema,
2, # brightness (level in ZCL)
0, # transition time (ZCL time in 10ths of a second)
expect_reply=True,
manufacturer=None,
tries=1,
tsn=None,
)
assert dev1_cluster_color.request.call_args == call(
False,
10,
dev1_cluster_color.commands_by_name["move_to_color_temp"].schema,
235, # color temp mireds
0, # transition time (ZCL time in 10ths of a second) - no transition when color_provided_while_off
expect_reply=True,
manufacturer=None,
tries=1,
tsn=None,
)
assert dev1_cluster_level.request.call_args_list[1] == call(
False,
0,
dev1_cluster_level.commands_by_name["move_to_level"].schema,
25, # brightness (level in ZCL)
10.0, # transition time (ZCL time in 10ths of a second)
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"] == 25
assert light1_state.attributes["color_temp"] == 235
assert light1_state.attributes["color_mode"] == ColorMode.COLOR_TEMP
dev1_cluster_level.request.reset_mock()
dev1_cluster_color.request.reset_mock()
# turn light 1 back off
await hass.services.async_call(
LIGHT_DOMAIN,
"turn_off",
{
"entity_id": device_1_entity_id,
},
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 == 0
assert dev1_cluster_color.request.await_count == 0
assert dev1_cluster_level.request.call_count == 0
assert dev1_cluster_level.request.await_count == 0
group_state = hass.states.get(group_entity_id)
assert group_state.state == STATE_OFF
dev1_cluster_on_off.request.reset_mock()
dev1_cluster_color.request.reset_mock()
dev1_cluster_level.request.reset_mock()
# test no transition provided and color temp while turning light on (color_provided_while_off)
await hass.services.async_call(
LIGHT_DOMAIN,
"turn_on",
{
"entity_id": device_1_entity_id,
"brightness": 25,
"color_temp": 236,
},
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,
4,
dev1_cluster_level.commands_by_name["move_to_level_with_on_off"].schema,
2, # brightness (level in ZCL)
0, # transition time (ZCL time in 10ths of a second)
expect_reply=True,
manufacturer=None,
tries=1,
tsn=None,
)
assert dev1_cluster_color.request.call_args == call(
False,
10,
dev1_cluster_color.commands_by_name["move_to_color_temp"].schema,
236, # color temp mireds
0, # transition time (ZCL time in 10ths of a second) - no transition when color_provided_while_off
expect_reply=True,
manufacturer=None,
tries=1,
tsn=None,
)
assert dev1_cluster_level.request.call_args_list[1] == call(
False,
0,
dev1_cluster_level.commands_by_name["move_to_level"].schema,
25, # brightness (level in ZCL)
0, # transition time (ZCL time in 10ths of a second)
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"] == 25
assert light1_state.attributes["color_temp"] == 236
assert light1_state.attributes["color_mode"] == ColorMode.COLOR_TEMP
dev1_cluster_level.request.reset_mock()
dev1_cluster_color.request.reset_mock()
# turn light 1 back off to setup group test
await hass.services.async_call(
LIGHT_DOMAIN,
"turn_off",
{
"entity_id": device_1_entity_id,
},
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 == 0
assert dev1_cluster_color.request.await_count == 0
assert dev1_cluster_level.request.call_count == 0
assert dev1_cluster_level.request.await_count == 0
group_state = hass.states.get(group_entity_id)
assert group_state.state == STATE_OFF
dev1_cluster_on_off.request.reset_mock()
dev1_cluster_color.request.reset_mock()
dev1_cluster_level.request.reset_mock()
# test no transition when the same color temp is provided from off
await hass.services.async_call(
LIGHT_DOMAIN,
"turn_on",
{
"entity_id": device_1_entity_id,
"color_temp": 236,
},
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 == call(
False,
1,
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,
10,
dev1_cluster_color.commands_by_name["move_to_color_temp"].schema,
236, # color temp mireds
0, # transition time (ZCL time in 10ths of a second) - no transition when color_provided_while_off
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"] == 25
assert light1_state.attributes["color_temp"] == 236
assert light1_state.attributes["color_mode"] == ColorMode.COLOR_TEMP
dev1_cluster_on_off.request.reset_mock()
dev1_cluster_color.request.reset_mock()
# turn light 1 back off to setup group test
await hass.services.async_call(
LIGHT_DOMAIN,
"turn_off",
{
"entity_id": device_1_entity_id,
},
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 == 0
assert dev1_cluster_color.request.await_count == 0
assert dev1_cluster_level.request.call_count == 0
assert dev1_cluster_level.request.await_count == 0
group_state = hass.states.get(group_entity_id)
assert group_state.state == STATE_OFF
dev1_cluster_on_off.request.reset_mock()
dev1_cluster_color.request.reset_mock()
dev1_cluster_level.request.reset_mock()
# test sengled light uses default minimum transition time
dev2_cluster_on_off.request.reset_mock()
dev2_cluster_color.request.reset_mock()
dev2_cluster_level.request.reset_mock()
await hass.services.async_call(
LIGHT_DOMAIN,
"turn_on",
{"entity_id": device_2_entity_id, "transition": 0, "brightness": 100},
blocking=True,
)
assert dev2_cluster_on_off.request.call_count == 0
assert dev2_cluster_on_off.request.await_count == 0
assert dev2_cluster_color.request.call_count == 0
assert dev2_cluster_color.request.await_count == 0
assert dev2_cluster_level.request.call_count == 1
assert dev2_cluster_level.request.await_count == 1
assert dev2_cluster_level.request.call_args == call(
False,
4,
dev2_cluster_level.commands_by_name["move_to_level_with_on_off"].schema,
100, # brightness (level in ZCL)
1, # transition time - sengled light uses default minimum
expect_reply=True,
manufacturer=None,
tries=1,
tsn=None,
)
light2_state = hass.states.get(device_2_entity_id)
assert light2_state.state == STATE_ON
assert light2_state.attributes["brightness"] == 100
dev2_cluster_level.request.reset_mock()
# turn the sengled light back off
await hass.services.async_call(
LIGHT_DOMAIN,
"turn_off",
{
"entity_id": device_2_entity_id,
},
blocking=True,
)
assert dev2_cluster_on_off.request.call_count == 1
assert dev2_cluster_on_off.request.await_count == 1
assert dev2_cluster_color.request.call_count == 0
assert dev2_cluster_color.request.await_count == 0
assert dev2_cluster_level.request.call_count == 0
assert dev2_cluster_level.request.await_count == 0
light2_state = hass.states.get(device_2_entity_id)
assert light2_state.state == STATE_OFF
dev2_cluster_on_off.request.reset_mock()
# test non 0 length transition and color temp while turning light on and sengled (color_provided_while_off)
await hass.services.async_call(
LIGHT_DOMAIN,
"turn_on",
{
"entity_id": device_2_entity_id,
"transition": 1,
"brightness": 25,
"color_temp": 235,
},
blocking=True,
)
assert dev2_cluster_on_off.request.call_count == 0
assert dev2_cluster_on_off.request.await_count == 0
assert dev2_cluster_color.request.call_count == 1
assert dev2_cluster_color.request.await_count == 1
assert dev2_cluster_level.request.call_count == 2
assert dev2_cluster_level.request.await_count == 2
# first it comes on with no transition at 2 brightness
assert dev2_cluster_level.request.call_args_list[0] == call(
False,
4,
dev2_cluster_level.commands_by_name["move_to_level_with_on_off"].schema,
2, # brightness (level in ZCL)
1, # transition time (ZCL time in 10ths of a second)
expect_reply=True,
manufacturer=None,
tries=1,
tsn=None,
)
assert dev2_cluster_color.request.call_args == call(
False,
10,
dev2_cluster_color.commands_by_name["move_to_color_temp"].schema,
235, # color temp mireds
1, # transition time (ZCL time in 10ths of a second) - sengled transition == 1 when color_provided_while_off
expect_reply=True,
manufacturer=None,
tries=1,
tsn=None,
)
assert dev2_cluster_level.request.call_args_list[1] == call(
False,
0,
dev2_cluster_level.commands_by_name["move_to_level"].schema,
25, # brightness (level in ZCL)
10.0, # transition time (ZCL time in 10ths of a second)
expect_reply=True,
manufacturer=None,
tries=1,
tsn=None,
)
light2_state = hass.states.get(device_2_entity_id)
assert light2_state.state == STATE_ON
assert light2_state.attributes["brightness"] == 25
assert light2_state.attributes["color_temp"] == 235
assert light2_state.attributes["color_mode"] == ColorMode.COLOR_TEMP
dev2_cluster_level.request.reset_mock()
dev2_cluster_color.request.reset_mock()
# turn the sengled light back off
await hass.services.async_call(
LIGHT_DOMAIN,
"turn_off",
{
"entity_id": device_2_entity_id,
},
blocking=True,
)
assert dev2_cluster_on_off.request.call_count == 1
assert dev2_cluster_on_off.request.await_count == 1
assert dev2_cluster_color.request.call_count == 0
assert dev2_cluster_color.request.await_count == 0
assert dev2_cluster_level.request.call_count == 0
assert dev2_cluster_level.request.await_count == 0
light2_state = hass.states.get(device_2_entity_id)
assert light2_state.state == STATE_OFF
dev2_cluster_on_off.request.reset_mock()
# test non 0 length transition and color temp while turning group light on (color_provided_while_off)
await hass.services.async_call(
LIGHT_DOMAIN,
"turn_on",
{
"entity_id": group_entity_id,
"transition": 1,
"brightness": 25,
"color_temp": 235,
},
blocking=True,
)
group_on_off_channel = zha_group.endpoint[general.OnOff.cluster_id]
group_level_channel = zha_group.endpoint[general.LevelControl.cluster_id]
group_color_channel = zha_group.endpoint[lighting.Color.cluster_id]
assert group_on_off_channel.request.call_count == 0
assert group_on_off_channel.request.await_count == 0
assert group_color_channel.request.call_count == 1
assert group_color_channel.request.await_count == 1
assert group_level_channel.request.call_count == 1
assert group_level_channel.request.await_count == 1
# groups are omitted from the 3 call dance for color_provided_while_off
assert group_color_channel.request.call_args == call(
False,
10,
dev2_cluster_color.commands_by_name["move_to_color_temp"].schema,
235, # color temp mireds
10.0, # transition time (ZCL time in 10ths of a second) - sengled transition == 1 when color_provided_while_off
expect_reply=True,
manufacturer=None,
tries=1,
tsn=None,
)
assert group_level_channel.request.call_args == call(
False,
4,
dev2_cluster_level.commands_by_name["move_to_level_with_on_off"].schema,
25, # brightness (level in ZCL)
10.0, # transition time (ZCL time in 10ths of a second)
expect_reply=True,
manufacturer=None,
tries=1,
tsn=None,
)
group_state = hass.states.get(group_entity_id)
assert group_state.state == STATE_ON
assert group_state.attributes["brightness"] == 25
assert group_state.attributes["color_temp"] == 235
assert group_state.attributes["color_mode"] == ColorMode.COLOR_TEMP
group_on_off_channel.request.reset_mock()
group_color_channel.request.reset_mock()
group_level_channel.request.reset_mock()
# turn the sengled light back on
await hass.services.async_call(
LIGHT_DOMAIN,
"turn_on",
{
"entity_id": device_2_entity_id,
},
blocking=True,
)
assert dev2_cluster_on_off.request.call_count == 1
assert dev2_cluster_on_off.request.await_count == 1
assert dev2_cluster_color.request.call_count == 0
assert dev2_cluster_color.request.await_count == 0
assert dev2_cluster_level.request.call_count == 0
assert dev2_cluster_level.request.await_count == 0
light2_state = hass.states.get(device_2_entity_id)
assert light2_state.state == STATE_ON
dev2_cluster_on_off.request.reset_mock()
# turn the light off with a transition
await hass.services.async_call(
LIGHT_DOMAIN,
"turn_off",
{"entity_id": device_2_entity_id, "transition": 2},
blocking=True,
)
assert dev2_cluster_on_off.request.call_count == 0
assert dev2_cluster_on_off.request.await_count == 0
assert dev2_cluster_color.request.call_count == 0
assert dev2_cluster_color.request.await_count == 0
assert dev2_cluster_level.request.call_count == 1
assert dev2_cluster_level.request.await_count == 1
assert dev2_cluster_level.request.call_args == call(
False,
4,
dev2_cluster_level.commands_by_name["move_to_level_with_on_off"].schema,
0, # brightness (level in ZCL)
20, # transition time
expect_reply=True,
manufacturer=None,
tries=1,
tsn=None,
)
light2_state = hass.states.get(device_2_entity_id)
assert light2_state.state == STATE_OFF
dev2_cluster_level.request.reset_mock()
# turn the light back on with no args should use a transition and last known brightness
await hass.services.async_call(
LIGHT_DOMAIN,
"turn_on",
{"entity_id": device_2_entity_id},
blocking=True,
)
assert dev2_cluster_on_off.request.call_count == 0
assert dev2_cluster_on_off.request.await_count == 0
assert dev2_cluster_color.request.call_count == 0
assert dev2_cluster_color.request.await_count == 0
assert dev2_cluster_level.request.call_count == 1
assert dev2_cluster_level.request.await_count == 1
assert dev2_cluster_level.request.call_args == call(
False,
4,
dev2_cluster_level.commands_by_name["move_to_level_with_on_off"].schema,
25, # brightness (level in ZCL) - this is the last brightness we set a few tests above
1, # transition time - sengled light uses default minimum
expect_reply=True,
manufacturer=None,
tries=1,
tsn=None,
)
light2_state = hass.states.get(device_2_entity_id)
assert light2_state.state == STATE_ON
dev2_cluster_level.request.reset_mock()
# test eWeLink color temp while turning light on from off (color_provided_while_off)
await hass.services.async_call(
LIGHT_DOMAIN,
"turn_on",
{
"entity_id": eWeLink_light_entity_id,
"color_temp": 235,
},
blocking=True,
)
assert eWeLink_cluster_on_off.request.call_count == 1
assert eWeLink_cluster_on_off.request.await_count == 1
assert eWeLink_cluster_color.request.call_count == 1
assert eWeLink_cluster_color.request.await_count == 1
assert eWeLink_cluster_level.request.call_count == 0
assert eWeLink_cluster_level.request.await_count == 0
# first it comes on
assert eWeLink_cluster_on_off.request.call_args_list[0] == call(
False,
1,
eWeLink_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,
10,
dev1_cluster_color.commands_by_name["move_to_color_temp"].schema,
235, # color temp mireds
0, # transition time (ZCL time in 10ths of a second)
expect_reply=True,
manufacturer=None,
tries=1,
tsn=None,
)
eWeLink_state = hass.states.get(eWeLink_light_entity_id)
assert eWeLink_state.state == STATE_ON
assert eWeLink_state.attributes["color_temp"] == 235
assert eWeLink_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
@ -463,7 +1260,7 @@ async def async_test_level_on_off_from_hass(
4,
level_cluster.commands_by_name["move_to_level_with_on_off"].schema,
10,
1,
0,
expect_reply=True,
manufacturer=None,
tries=1,
@ -601,7 +1398,10 @@ async def test_zha_group_light_entity(
# test that the lights were created and are off
group_state = hass.states.get(group_entity_id)
assert group_state.state == STATE_OFF
assert group_state.attributes["supported_color_modes"] == [ColorMode.HS]
assert group_state.attributes["supported_color_modes"] == [
ColorMode.COLOR_TEMP,
ColorMode.HS,
]
# Light which is off has no color mode
assert "color_mode" not in group_state.attributes
@ -629,7 +1429,10 @@ async def test_zha_group_light_entity(
# Check state
group_state = hass.states.get(group_entity_id)
assert group_state.state == STATE_ON
assert group_state.attributes["supported_color_modes"] == [ColorMode.HS]
assert group_state.attributes["supported_color_modes"] == [
ColorMode.COLOR_TEMP,
ColorMode.HS,
]
assert group_state.attributes["color_mode"] == ColorMode.HS
# test long flashing the lights from the HA