"""Test ZHA light.""" from datetime import timedelta from unittest.mock import AsyncMock, call, patch, sentinel import pytest from zigpy.profiles import zha from zigpy.zcl.clusters import general, lighting import zigpy.zcl.foundation as zcl_f from homeassistant.components.light import ( DOMAIN as LIGHT_DOMAIN, FLASH_LONG, FLASH_SHORT, ColorMode, ) from homeassistant.components.zha.core.const import ( CONF_ALWAYS_PREFER_XY_COLOR_MODE, CONF_GROUP_MEMBERS_ASSUME_STATE, ZHA_OPTIONS, ) from homeassistant.components.zha.core.group import GroupMember from homeassistant.components.zha.core.helpers import get_zha_gateway from homeassistant.components.zha.light import FLASH_EFFECTS from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er import homeassistant.util.dt as dt_util from .common import ( async_enable_traffic, async_find_group_entity_id, async_shift_time, async_test_rejoin, async_wait_for_updates, find_entity_id, patch_zha_config, send_attributes_report, update_attribute_cache, ) from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_PROFILE, SIG_EP_TYPE from tests.common import ( async_fire_time_changed, async_mock_load_restore_state_from_storage, ) IEEE_GROUPABLE_DEVICE = "01:2d:6f:00:0a:90:69:e8" IEEE_GROUPABLE_DEVICE2 = "02:2d:6f:00:0a:90:69:e9" IEEE_GROUPABLE_DEVICE3 = "03:2d:6f:00:0a:90:69:e7" LIGHT_ON_OFF = { 1: { SIG_EP_PROFILE: zha.PROFILE_ID, SIG_EP_TYPE: zha.DeviceType.ON_OFF_LIGHT, SIG_EP_INPUT: [ general.Basic.cluster_id, general.Identify.cluster_id, general.OnOff.cluster_id, ], SIG_EP_OUTPUT: [general.Ota.cluster_id], } } LIGHT_LEVEL = { 1: { SIG_EP_PROFILE: zha.PROFILE_ID, SIG_EP_TYPE: zha.DeviceType.DIMMABLE_LIGHT, SIG_EP_INPUT: [ general.Basic.cluster_id, general.LevelControl.cluster_id, general.OnOff.cluster_id, ], SIG_EP_OUTPUT: [general.Ota.cluster_id], } } LIGHT_COLOR = { 1: { SIG_EP_PROFILE: zha.PROFILE_ID, SIG_EP_TYPE: zha.DeviceType.COLOR_DIMMABLE_LIGHT, SIG_EP_INPUT: [ general.Basic.cluster_id, general.Identify.cluster_id, general.LevelControl.cluster_id, general.OnOff.cluster_id, lighting.Color.cluster_id, ], SIG_EP_OUTPUT: [general.Ota.cluster_id], } } @pytest.fixture(autouse=True) def light_platform_only(): """Only set up the light and required base platforms to speed up tests.""" with patch( "homeassistant.components.zha.PLATFORMS", ( Platform.BINARY_SENSOR, Platform.DEVICE_TRACKER, Platform.BUTTON, Platform.LIGHT, Platform.SENSOR, Platform.NUMBER, Platform.SELECT, ), ): yield @pytest.fixture async def coordinator(hass, zigpy_device_mock, zha_device_joined): """Test ZHA light platform.""" zigpy_device = zigpy_device_mock( { 1: { SIG_EP_INPUT: [general.Groups.cluster_id], SIG_EP_OUTPUT: [], SIG_EP_TYPE: zha.DeviceType.COLOR_DIMMABLE_LIGHT, SIG_EP_PROFILE: zha.PROFILE_ID, } }, ieee="00:15:8d:00:02:32:4f:32", nwk=0x0000, node_descriptor=b"\xf8\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff", ) zha_device = await zha_device_joined(zigpy_device) zha_device.available = True return zha_device @pytest.fixture async def device_light_1(hass, zigpy_device_mock, zha_device_joined): """Test ZHA light platform.""" 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=IEEE_GROUPABLE_DEVICE, nwk=0xB79D, ) color_cluster = zigpy_device.endpoints[1].light_color color_cluster.PLUGGED_ATTR_READS = { "color_capabilities": lighting.Color.ColorCapabilities.Color_temperature | lighting.Color.ColorCapabilities.XY_attributes } zha_device = await zha_device_joined(zigpy_device) zha_device.available = True return zha_device @pytest.fixture async def device_light_2(hass, zigpy_device_mock, zha_device_joined): """Test ZHA light platform.""" 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=IEEE_GROUPABLE_DEVICE2, manufacturer="sengled", nwk=0xC79E, ) color_cluster = zigpy_device.endpoints[1].light_color color_cluster.PLUGGED_ATTR_READS = { "color_capabilities": lighting.Color.ColorCapabilities.Color_temperature | lighting.Color.ColorCapabilities.XY_attributes } zha_device = await zha_device_joined(zigpy_device) zha_device.available = True return zha_device @pytest.fixture async def device_light_3(hass, zigpy_device_mock, zha_device_joined): """Test ZHA light platform.""" 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=IEEE_GROUPABLE_DEVICE3, nwk=0xB89F, ) zha_device = await zha_device_joined(zigpy_device) zha_device.available = True 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": lighting.Color.ColorCapabilities.Color_temperature | lighting.Color.ColorCapabilities.XY_attributes, "color_temp_physical_min": 0, "color_temp_physical_max": 0, } zha_device = await zha_device_joined(zigpy_device) zha_device.available = True return zha_device async def test_light_refresh( hass: HomeAssistant, zigpy_device_mock, zha_device_joined_restored ) -> None: """Test ZHA light platform refresh.""" # create zigpy devices zigpy_device = zigpy_device_mock(LIGHT_ON_OFF) on_off_cluster = zigpy_device.endpoints[1].on_off on_off_cluster.PLUGGED_ATTR_READS = {"on_off": 0} zha_device = await zha_device_joined_restored(zigpy_device) entity_id = find_entity_id(Platform.LIGHT, zha_device, hass) # allow traffic to flow through the gateway and device await async_enable_traffic(hass, [zha_device]) on_off_cluster.read_attributes.reset_mock() # not enough time passed async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=20)) await hass.async_block_till_done() assert on_off_cluster.read_attributes.call_count == 0 assert on_off_cluster.read_attributes.await_count == 0 assert hass.states.get(entity_id).state == STATE_OFF # 1 interval - 1 call on_off_cluster.PLUGGED_ATTR_READS = {"on_off": 1} async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=80)) await hass.async_block_till_done() assert on_off_cluster.read_attributes.call_count == 1 assert on_off_cluster.read_attributes.await_count == 1 assert hass.states.get(entity_id).state == STATE_ON # 2 intervals - 2 calls on_off_cluster.PLUGGED_ATTR_READS = {"on_off": 0} async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=80)) await hass.async_block_till_done() assert on_off_cluster.read_attributes.call_count == 2 assert on_off_cluster.read_attributes.await_count == 2 assert hass.states.get(entity_id).state == STATE_OFF @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]), ) @pytest.mark.parametrize( ("device", "reporting"), [(LIGHT_ON_OFF, (1, 0, 0)), (LIGHT_LEVEL, (1, 1, 0)), (LIGHT_COLOR, (1, 1, 6))], ) async def test_light( hass: HomeAssistant, zigpy_device_mock, zha_device_joined_restored, device, reporting, ) -> None: """Test ZHA light platform.""" # create zigpy devices zigpy_device = zigpy_device_mock(device) zha_device = await zha_device_joined_restored(zigpy_device) entity_id = find_entity_id(Platform.LIGHT, zha_device, hass) assert entity_id is not None cluster_on_off = zigpy_device.endpoints[1].on_off cluster_level = getattr(zigpy_device.endpoints[1], "level", None) cluster_color = getattr(zigpy_device.endpoints[1], "light_color", None) cluster_identify = getattr(zigpy_device.endpoints[1], "identify", None) assert hass.states.get(entity_id).state == STATE_OFF await async_enable_traffic(hass, [zha_device], enabled=False) # test that the lights were created and that they are unavailable assert hass.states.get(entity_id).state == STATE_UNAVAILABLE # allow traffic to flow through the gateway and device await async_enable_traffic(hass, [zha_device]) # test that the lights were created and are off assert hass.states.get(entity_id).state == STATE_OFF # test turning the lights on and off from the light await async_test_on_off_from_light(hass, cluster_on_off, entity_id) # test turning the lights on and off from the HA await async_test_on_off_from_hass(hass, cluster_on_off, entity_id) # test short flashing the lights from the HA if cluster_identify: await async_test_flash_from_hass(hass, cluster_identify, entity_id, FLASH_SHORT) # test long flashing the lights from the HA if cluster_identify: await async_test_flash_from_hass(hass, cluster_identify, entity_id, FLASH_LONG) # test dimming the lights on and off from the HA if cluster_level: await async_test_level_on_off_from_hass( hass, cluster_on_off, cluster_level, entity_id ) await async_shift_time(hass) # test getting a brightness change from the network await async_test_on_from_light(hass, cluster_on_off, entity_id) await async_test_dimmer_from_light( hass, cluster_level, entity_id, 150, STATE_ON ) # test rejoin await async_test_off_from_hass(hass, cluster_on_off, entity_id) clusters = [c for c in (cluster_on_off, cluster_level, cluster_color) if c] await async_test_rejoin(hass, zigpy_device, clusters, reporting) @pytest.mark.parametrize( ("plugged_attr_reads", "config_override", "expected_state"), [ # HS light without cached hue or saturation ( { "color_capabilities": ( lighting.Color.ColorCapabilities.Hue_and_saturation ), }, {(ZHA_OPTIONS, CONF_ALWAYS_PREFER_XY_COLOR_MODE): False}, {}, ), # HS light with cached hue ( { "color_capabilities": ( lighting.Color.ColorCapabilities.Hue_and_saturation ), "current_hue": 100, }, {(ZHA_OPTIONS, CONF_ALWAYS_PREFER_XY_COLOR_MODE): False}, {}, ), # HS light with cached saturation ( { "color_capabilities": ( lighting.Color.ColorCapabilities.Hue_and_saturation ), "current_saturation": 100, }, {(ZHA_OPTIONS, CONF_ALWAYS_PREFER_XY_COLOR_MODE): False}, {}, ), # HS light with both ( { "color_capabilities": ( lighting.Color.ColorCapabilities.Hue_and_saturation ), "current_hue": 100, "current_saturation": 100, }, {(ZHA_OPTIONS, CONF_ALWAYS_PREFER_XY_COLOR_MODE): False}, {}, ), ], ) async def test_light_initialization( hass: HomeAssistant, zigpy_device_mock, zha_device_joined_restored, plugged_attr_reads, config_override, expected_state, ) -> None: """Test ZHA light initialization with cached attributes and color modes.""" # create zigpy devices zigpy_device = zigpy_device_mock(LIGHT_COLOR) # mock attribute reads zigpy_device.endpoints[1].light_color.PLUGGED_ATTR_READS = plugged_attr_reads with patch_zha_config("light", config_override): zha_device = await zha_device_joined_restored(zigpy_device) entity_id = find_entity_id(Platform.LIGHT, zha_device, hass) assert entity_id is not None # TODO ensure hue and saturation are properly set on startup @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: HomeAssistant, device_light_1, device_light_2, eWeLink_light, coordinator ) -> None: """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 = find_entity_id(Platform.LIGHT, device_light_1, hass) device_2_entity_id = find_entity_id(Platform.LIGHT, device_light_2, hass) eWeLink_light_entity_id = 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 and no brightness 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}, 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, 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=254, # default "full on" brightness transition_time=0, expect_reply=True, manufacturer=None, tsn=None, ) light1_state = hass.states.get(device_1_entity_id) assert light1_state.state == STATE_ON assert light1_state.attributes["brightness"] == 254 # test 0 length transition with no color and no brightness provided again, but for "force on" lights eWeLink_cluster_on_off.request.reset_mock() eWeLink_cluster_level.request.reset_mock() await hass.services.async_call( LIGHT_DOMAIN, "turn_on", {"entity_id": eWeLink_light_entity_id, "transition": 0}, blocking=True, ) assert eWeLink_cluster_on_off.request.call_count == 1 assert eWeLink_cluster_on_off.request.await_count == 1 assert eWeLink_cluster_on_off.request.call_args_list[0] == call( False, eWeLink_cluster_on_off.commands_by_name["on"].id, eWeLink_cluster_on_off.commands_by_name["on"].schema, expect_reply=True, manufacturer=None, tsn=None, ) assert eWeLink_cluster_color.request.call_count == 0 assert eWeLink_cluster_color.request.await_count == 0 assert eWeLink_cluster_level.request.call_count == 1 assert eWeLink_cluster_level.request.await_count == 1 assert eWeLink_cluster_level.request.call_args == call( False, eWeLink_cluster_level.commands_by_name["move_to_level_with_on_off"].id, eWeLink_cluster_level.commands_by_name["move_to_level_with_on_off"].schema, level=254, # default "full on" brightness transition_time=0, expect_reply=True, manufacturer=None, tsn=None, ) eWeLink_state = hass.states.get(eWeLink_light_entity_id) assert eWeLink_state.state == STATE_ON assert eWeLink_state.attributes["brightness"] == 254 eWeLink_cluster_on_off.request.reset_mock() eWeLink_cluster_level.request.reset_mock() # test 0 length transition with brightness, but 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, 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=50, transition_time=0, expect_reply=True, manufacturer=None, 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.5, "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, 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=18, transition_time=35, expect_reply=True, manufacturer=None, 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=432, transition_time=35, expect_reply=True, manufacturer=None, 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, 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=0, transition_time=0, expect_reply=True, manufacturer=None, 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 (new_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, 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, 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, # no transition when new_color_provided_while_off expect_reply=True, manufacturer=None, 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=25, transition_time=10, expect_reply=True, manufacturer=None, 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 (new_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, 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, 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=236, transition_time=0, # no transition when new_color_provided_while_off expect_reply=True, manufacturer=None, 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=25, transition_time=0, expect_reply=True, manufacturer=None, 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, dev1_cluster_on_off.commands_by_name["on"].id, dev1_cluster_on_off.commands_by_name["on"].schema, expect_reply=True, manufacturer=None, 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=236, transition_time=0, # no transition when new_color_provided_while_off expect_reply=True, manufacturer=None, 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, dev2_cluster_level.commands_by_name["move_to_level_with_on_off"].id, dev2_cluster_level.commands_by_name["move_to_level_with_on_off"].schema, level=100, transition_time=1, # transition time - sengled light uses default minimum expect_reply=True, manufacturer=None, 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 (new_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, dev2_cluster_level.commands_by_name["move_to_level_with_on_off"].id, dev2_cluster_level.commands_by_name["move_to_level_with_on_off"].schema, level=2, transition_time=1, expect_reply=True, manufacturer=None, tsn=None, ) assert dev2_cluster_color.request.call_args == call( False, dev2_cluster_color.commands_by_name["move_to_color_temp"].id, dev2_cluster_color.commands_by_name["move_to_color_temp"].schema, color_temp_mireds=235, transition_time=1, # sengled transition == 1 when new_color_provided_while_off expect_reply=True, manufacturer=None, tsn=None, ) assert dev2_cluster_level.request.call_args_list[1] == call( False, dev2_cluster_level.commands_by_name["move_to_level"].id, dev2_cluster_level.commands_by_name["move_to_level"].schema, level=25, transition_time=10, expect_reply=True, manufacturer=None, 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 (new_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_cluster_handler = zha_group.endpoint[general.OnOff.cluster_id] group_level_cluster_handler = zha_group.endpoint[general.LevelControl.cluster_id] group_color_cluster_handler = zha_group.endpoint[lighting.Color.cluster_id] assert group_on_off_cluster_handler.request.call_count == 0 assert group_on_off_cluster_handler.request.await_count == 0 assert group_color_cluster_handler.request.call_count == 1 assert group_color_cluster_handler.request.await_count == 1 assert group_level_cluster_handler.request.call_count == 1 assert group_level_cluster_handler.request.await_count == 1 # groups are omitted from the 3 call dance for new_color_provided_while_off assert group_color_cluster_handler.request.call_args == call( False, dev2_cluster_color.commands_by_name["move_to_color_temp"].id, dev2_cluster_color.commands_by_name["move_to_color_temp"].schema, color_temp_mireds=235, transition_time=10, # sengled transition == 1 when new_color_provided_while_off expect_reply=True, manufacturer=None, tsn=None, ) assert group_level_cluster_handler.request.call_args == call( False, dev2_cluster_level.commands_by_name["move_to_level_with_on_off"].id, dev2_cluster_level.commands_by_name["move_to_level_with_on_off"].schema, level=25, transition_time=10, expect_reply=True, manufacturer=None, 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_cluster_handler.request.reset_mock() group_color_cluster_handler.request.reset_mock() group_level_cluster_handler.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, dev2_cluster_level.commands_by_name["move_to_level_with_on_off"].id, dev2_cluster_level.commands_by_name["move_to_level_with_on_off"].schema, level=0, transition_time=20, # transition time expect_reply=True, manufacturer=None, 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, dev2_cluster_level.commands_by_name["move_to_level_with_on_off"].id, dev2_cluster_level.commands_by_name["move_to_level_with_on_off"].schema, level=25, transition_time=1, # transition time - sengled light uses default minimum expect_reply=True, manufacturer=None, 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 (new_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, eWeLink_cluster_on_off.commands_by_name["on"].id, eWeLink_cluster_on_off.commands_by_name["on"].schema, expect_reply=True, manufacturer=None, 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, 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 assert eWeLink_state.attributes["min_mireds"] == 153 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: HomeAssistant, device_light_1) -> None: """Test turning on the light and sending color commands before on/level commands for supporting lights.""" device_1_entity_id = 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, 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, 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, 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, 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, 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 await send_attributes_report(hass, cluster, {1: 0, 0: 1, 2: 3}) await async_wait_for_updates(hass) assert hass.states.get(entity_id).state == STATE_ON # turn off at light await send_attributes_report(hass, cluster, {1: 1, 0: 0, 2: 3}) await async_wait_for_updates(hass) assert hass.states.get(entity_id).state == STATE_OFF async def async_test_on_from_light(hass, cluster, entity_id): """Test on off functionality from the light.""" # turn on at light await send_attributes_report(hass, cluster, {1: -1, 0: 1, 2: 2}) await async_wait_for_updates(hass) assert hass.states.get(entity_id).state == STATE_ON async def async_test_on_off_from_hass(hass, cluster, entity_id): """Test on off functionality from hass.""" # turn on via UI cluster.request.reset_mock() await hass.services.async_call( LIGHT_DOMAIN, "turn_on", {"entity_id": entity_id}, blocking=True ) assert cluster.request.call_count == 1 assert cluster.request.await_count == 1 assert cluster.request.call_args == call( False, cluster.commands_by_name["on"].id, cluster.commands_by_name["on"].schema, expect_reply=True, manufacturer=None, tsn=None, ) await async_test_off_from_hass(hass, cluster, entity_id) async def async_test_off_from_hass(hass, cluster, entity_id): """Test turning off the light from Home Assistant.""" # turn off via UI cluster.request.reset_mock() await hass.services.async_call( LIGHT_DOMAIN, "turn_off", {"entity_id": entity_id}, blocking=True ) assert cluster.request.call_count == 1 assert cluster.request.await_count == 1 assert cluster.request.call_args == call( False, cluster.commands_by_name["off"].id, cluster.commands_by_name["off"].schema, expect_reply=True, manufacturer=None, tsn=None, ) async def async_test_level_on_off_from_hass( hass, on_off_cluster, level_cluster, entity_id, expected_default_transition: int = 0 ): """Test on off functionality from hass.""" on_off_cluster.request.reset_mock() level_cluster.request.reset_mock() await async_shift_time(hass) # turn on via UI await hass.services.async_call( LIGHT_DOMAIN, "turn_on", {"entity_id": entity_id}, blocking=True ) assert on_off_cluster.request.call_count == 1 assert on_off_cluster.request.await_count == 1 assert level_cluster.request.call_count == 0 assert level_cluster.request.await_count == 0 assert on_off_cluster.request.call_args == call( False, on_off_cluster.commands_by_name["on"].id, on_off_cluster.commands_by_name["on"].schema, expect_reply=True, manufacturer=None, tsn=None, ) on_off_cluster.request.reset_mock() level_cluster.request.reset_mock() await async_shift_time(hass) await hass.services.async_call( LIGHT_DOMAIN, "turn_on", {"entity_id": entity_id, "transition": 10}, blocking=True, ) assert on_off_cluster.request.call_count == 0 assert on_off_cluster.request.await_count == 0 assert level_cluster.request.call_count == 1 assert level_cluster.request.await_count == 1 assert level_cluster.request.call_args == call( False, level_cluster.commands_by_name["move_to_level_with_on_off"].id, level_cluster.commands_by_name["move_to_level_with_on_off"].schema, level=254, transition_time=100, expect_reply=True, manufacturer=None, tsn=None, ) on_off_cluster.request.reset_mock() level_cluster.request.reset_mock() await hass.services.async_call( LIGHT_DOMAIN, "turn_on", {"entity_id": entity_id, "brightness": 10}, blocking=True, ) # the onoff cluster is now not used when brightness is present by default assert on_off_cluster.request.call_count == 0 assert on_off_cluster.request.await_count == 0 assert level_cluster.request.call_count == 1 assert level_cluster.request.await_count == 1 assert level_cluster.request.call_args == call( False, level_cluster.commands_by_name["move_to_level_with_on_off"].id, level_cluster.commands_by_name["move_to_level_with_on_off"].schema, level=10, transition_time=int(expected_default_transition), expect_reply=True, manufacturer=None, tsn=None, ) on_off_cluster.request.reset_mock() level_cluster.request.reset_mock() await async_test_off_from_hass(hass, on_off_cluster, entity_id) async def async_test_dimmer_from_light(hass, cluster, entity_id, level, expected_state): """Test dimmer functionality from the light.""" await send_attributes_report( hass, cluster, {1: level + 10, 0: level, 2: level - 10 or 22} ) await async_wait_for_updates(hass) assert hass.states.get(entity_id).state == expected_state # hass uses None for brightness of 0 in state attributes if level == 0: level = None assert hass.states.get(entity_id).attributes.get("brightness") == level async def async_test_flash_from_hass(hass, cluster, entity_id, flash): """Test flash functionality from hass.""" # turn on via UI cluster.request.reset_mock() await hass.services.async_call( LIGHT_DOMAIN, "turn_on", {"entity_id": entity_id, "flash": flash}, blocking=True, ) assert cluster.request.call_count == 1 assert cluster.request.await_count == 1 assert cluster.request.call_args == call( False, cluster.commands_by_name["trigger_effect"].id, cluster.commands_by_name["trigger_effect"].schema, effect_id=FLASH_EFFECTS[flash], effect_variant=general.Identify.EffectVariant.Default, expect_reply=True, manufacturer=None, tsn=None, ) @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]), ) @patch( "homeassistant.components.zha.entity.DEFAULT_UPDATE_GROUP_FROM_CHILD_DELAY", new=0, ) async def test_zha_group_light_entity( hass: HomeAssistant, device_light_1, device_light_2, device_light_3, coordinator ) -> None: """Test the light entity for a ZHA group.""" 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 = find_entity_id(Platform.LIGHT, device_light_1, hass) device_2_entity_id = find_entity_id(Platform.LIGHT, device_light_2, hass) device_3_entity_id = find_entity_id(Platform.LIGHT, device_light_3, hass) assert device_1_entity_id not in (device_2_entity_id, device_3_entity_id) assert device_2_entity_id != device_3_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 assert device_3_entity_id not in zha_group.member_entity_ids group_cluster_on_off = zha_group.endpoint[general.OnOff.cluster_id] group_cluster_level = zha_group.endpoint[general.LevelControl.cluster_id] group_cluster_identify = zha_group.endpoint[general.Identify.cluster_id] dev1_cluster_on_off = device_light_1.device.endpoints[1].on_off dev2_cluster_on_off = device_light_2.device.endpoints[1].on_off dev3_cluster_on_off = device_light_3.device.endpoints[1].on_off dev1_cluster_level = device_light_1.device.endpoints[1].level await async_enable_traffic( hass, [device_light_1, device_light_2, device_light_3], enabled=False ) await async_wait_for_updates(hass) # test that the lights were created and that they are unavailable assert hass.states.get(group_entity_id).state == STATE_UNAVAILABLE # allow traffic to flow through the gateway and device await async_enable_traffic(hass, [device_light_1, device_light_2, device_light_3]) 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 assert group_state.attributes["supported_color_modes"] == [ ColorMode.COLOR_TEMP, ColorMode.XY, ] # Light which is off has no color mode assert group_state.attributes["color_mode"] is None # test turning the lights on and off from the HA await async_test_on_off_from_hass(hass, group_cluster_on_off, group_entity_id) await async_shift_time(hass) # test short flashing the lights from the HA await async_test_flash_from_hass( hass, group_cluster_identify, group_entity_id, FLASH_SHORT ) await async_shift_time(hass) # test turning the lights on and off from the light await async_test_on_off_from_light(hass, dev1_cluster_on_off, group_entity_id) # test turning the lights on and off from the HA await async_test_level_on_off_from_hass( hass, group_cluster_on_off, group_cluster_level, group_entity_id, expected_default_transition=1, # a Sengled light is in that group and needs a minimum 0.1s transition ) await async_shift_time(hass) # test getting a brightness change from the network await async_test_on_from_light(hass, dev1_cluster_on_off, group_entity_id) await async_test_dimmer_from_light( hass, dev1_cluster_level, group_entity_id, 150, STATE_ON ) # Check state group_state = hass.states.get(group_entity_id) assert group_state.state == STATE_ON assert group_state.attributes["supported_color_modes"] == [ ColorMode.COLOR_TEMP, ColorMode.XY, ] assert group_state.attributes["color_mode"] == ColorMode.XY # test long flashing the lights from the HA await async_test_flash_from_hass( hass, group_cluster_identify, group_entity_id, FLASH_LONG ) await async_shift_time(hass) assert len(zha_group.members) == 2 # test some of the group logic to make sure we key off states correctly await send_attributes_report(hass, dev1_cluster_on_off, {0: 1}) await send_attributes_report(hass, dev2_cluster_on_off, {0: 1}) await hass.async_block_till_done() # test that group light is on assert hass.states.get(device_1_entity_id).state == STATE_ON assert hass.states.get(device_2_entity_id).state == STATE_ON assert hass.states.get(group_entity_id).state == STATE_ON await send_attributes_report(hass, dev1_cluster_on_off, {0: 0}) await hass.async_block_till_done() # test that group light is still on assert hass.states.get(device_1_entity_id).state == STATE_OFF assert hass.states.get(device_2_entity_id).state == STATE_ON assert hass.states.get(group_entity_id).state == STATE_ON await send_attributes_report(hass, dev2_cluster_on_off, {0: 0}) await async_wait_for_updates(hass) # test that group light is now off assert hass.states.get(device_1_entity_id).state == STATE_OFF assert hass.states.get(device_2_entity_id).state == STATE_OFF assert hass.states.get(group_entity_id).state == STATE_OFF await send_attributes_report(hass, dev1_cluster_on_off, {0: 1}) await async_wait_for_updates(hass) # test that group light is now back on assert hass.states.get(device_1_entity_id).state == STATE_ON assert hass.states.get(device_2_entity_id).state == STATE_OFF assert hass.states.get(group_entity_id).state == STATE_ON # turn it off to test a new member add being tracked await send_attributes_report(hass, dev1_cluster_on_off, {0: 0}) await async_wait_for_updates(hass) assert hass.states.get(device_1_entity_id).state == STATE_OFF assert hass.states.get(device_2_entity_id).state == STATE_OFF assert hass.states.get(group_entity_id).state == STATE_OFF # add a new member and test that his state is also tracked await zha_group.async_add_members([GroupMember(device_light_3.ieee, 1)]) await send_attributes_report(hass, dev3_cluster_on_off, {0: 1}) await async_wait_for_updates(hass) assert device_3_entity_id in zha_group.member_entity_ids assert len(zha_group.members) == 3 assert hass.states.get(device_1_entity_id).state == STATE_OFF assert hass.states.get(device_2_entity_id).state == STATE_OFF assert hass.states.get(device_3_entity_id).state == STATE_ON assert hass.states.get(group_entity_id).state == STATE_ON # make the group have only 1 member and now there should be no entity await zha_group.async_remove_members( [GroupMember(device_light_2.ieee, 1), GroupMember(device_light_3.ieee, 1)] ) assert len(zha_group.members) == 1 assert hass.states.get(group_entity_id) is None assert device_2_entity_id not in zha_group.member_entity_ids assert device_3_entity_id not in zha_group.member_entity_ids # make sure the entity registry entry is still there entity_registry = er.async_get(hass) assert entity_registry.async_get(group_entity_id) is not None # add a member back and ensure that the group entity was created again await zha_group.async_add_members([GroupMember(device_light_3.ieee, 1)]) await send_attributes_report(hass, dev3_cluster_on_off, {0: 1}) await async_wait_for_updates(hass) assert len(zha_group.members) == 2 assert hass.states.get(group_entity_id).state == STATE_ON # add a 3rd member and ensure we still have an entity and we track the new one await send_attributes_report(hass, dev1_cluster_on_off, {0: 0}) await send_attributes_report(hass, dev3_cluster_on_off, {0: 0}) await async_wait_for_updates(hass) assert hass.states.get(group_entity_id).state == STATE_OFF # this will test that _reprobe_group is used correctly await zha_group.async_add_members( [GroupMember(device_light_2.ieee, 1), GroupMember(coordinator.ieee, 1)] ) await send_attributes_report(hass, dev2_cluster_on_off, {0: 1}) await async_wait_for_updates(hass) assert len(zha_group.members) == 4 assert hass.states.get(group_entity_id).state == STATE_ON await zha_group.async_remove_members([GroupMember(coordinator.ieee, 1)]) await hass.async_block_till_done() assert hass.states.get(group_entity_id).state == STATE_ON assert len(zha_group.members) == 3 # remove the group and ensure that there is no entity and that the entity registry is cleaned up assert entity_registry.async_get(group_entity_id) is not None await zha_gateway.async_remove_zigpy_group(zha_group.group_id) assert hass.states.get(group_entity_id) is None assert entity_registry.async_get(group_entity_id) is None @patch( "zigpy.zcl.clusters.general.OnOff.request", new=AsyncMock(return_value=[sentinel.data, zcl_f.Status.SUCCESS]), ) @patch( "homeassistant.components.zha.light.ASSUME_UPDATE_GROUP_FROM_CHILD_DELAY", new=0, ) async def test_group_member_assume_state( hass: HomeAssistant, zigpy_device_mock, zha_device_joined, coordinator, device_light_1, device_light_2, ) -> None: """Test the group members assume state function.""" with patch_zha_config( "light", {(ZHA_OPTIONS, CONF_GROUP_MEMBERS_ASSUME_STATE): True} ): 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 = find_entity_id(Platform.LIGHT, device_light_1, hass) device_2_entity_id = find_entity_id(Platform.LIGHT, device_light_2, 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 group_cluster_on_off = zha_group.endpoint[general.OnOff.cluster_id] await async_enable_traffic( hass, [device_light_1, device_light_2], enabled=False ) await async_wait_for_updates(hass) # test that the lights were created and that they are unavailable assert hass.states.get(group_entity_id).state == STATE_UNAVAILABLE # 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 group_cluster_on_off.request.reset_mock() await async_shift_time(hass) # turn on via UI await hass.services.async_call( LIGHT_DOMAIN, "turn_on", {"entity_id": group_entity_id}, blocking=True ) # members also instantly assume STATE_ON assert hass.states.get(device_1_entity_id).state == STATE_ON assert hass.states.get(device_2_entity_id).state == STATE_ON assert hass.states.get(group_entity_id).state == STATE_ON # turn off via UI await hass.services.async_call( LIGHT_DOMAIN, "turn_off", {"entity_id": group_entity_id}, blocking=True ) # members also instantly assume STATE_OFF assert hass.states.get(device_1_entity_id).state == STATE_OFF assert hass.states.get(device_2_entity_id).state == STATE_OFF assert hass.states.get(group_entity_id).state == STATE_OFF # remove the group and ensure that there is no entity and that the entity registry is cleaned up entity_registry = er.async_get(hass) assert entity_registry.async_get(group_entity_id) is not None await zha_gateway.async_remove_zigpy_group(zha_group.group_id) assert hass.states.get(group_entity_id) is None assert entity_registry.async_get(group_entity_id) is None @pytest.mark.parametrize( ("restored_state", "expected_state"), [ ( STATE_ON, { "brightness": None, "off_with_transition": None, "off_brightness": None, "color_mode": ColorMode.XY, # color_mode defaults to what the light supports when restored with ON state "color_temp": None, "xy_color": None, "hs_color": None, "effect": None, }, ), ( STATE_OFF, { "brightness": None, "off_with_transition": None, "off_brightness": None, "color_mode": None, "color_temp": None, "xy_color": None, "hs_color": None, "effect": None, }, ), ], ) async def test_restore_light_state( hass: HomeAssistant, zigpy_device_mock, core_rs, zha_device_restored, restored_state, expected_state, ) -> None: """Test ZHA light restores without throwing an error when attributes are None.""" # restore state with None values attributes = { "brightness": None, "off_with_transition": None, "off_brightness": None, "color_mode": None, "color_temp": None, "xy_color": None, "hs_color": None, "effect": None, } entity_id = "light.fakemanufacturer_fakemodel_light" core_rs( entity_id, state=restored_state, attributes=attributes, ) await async_mock_load_restore_state_from_storage(hass) zigpy_device = zigpy_device_mock(LIGHT_COLOR) zha_device = await zha_device_restored(zigpy_device) entity_id = find_entity_id(Platform.LIGHT, zha_device, hass) assert entity_id is not None assert hass.states.get(entity_id).state == restored_state # compare actual restored state to expected state for attribute, expected_value in expected_state.items(): assert hass.states.get(entity_id).attributes.get(attribute) == expected_value