"""Test the Yeelight light.""" import asyncio from datetime import timedelta import logging import socket from unittest.mock import ANY, AsyncMock, MagicMock, call, patch import pytest from yeelight import ( BulbException, BulbType, HSVTransition, LightType, PowerMode, RGBTransition, SceneClass, SleepTransition, TemperatureTransition, transitions, ) from yeelight.flow import Action, Flow from yeelight.main import _MODEL_SPECS from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_FLASH, ATTR_HS_COLOR, ATTR_KELVIN, ATTR_RGB_COLOR, ATTR_TRANSITION, FLASH_LONG, FLASH_SHORT, SERVICE_TURN_OFF, SERVICE_TURN_ON, LightEntityFeature, ) from homeassistant.components.yeelight.const import ( ATTR_COUNT, ATTR_MODE_MUSIC, ATTR_TRANSITIONS, CONF_CUSTOM_EFFECTS, CONF_FLOW_PARAMS, CONF_MODE_MUSIC, CONF_NIGHTLIGHT_SWITCH, CONF_SAVE_ON_CHANGE, CONF_TRANSITION, DEFAULT_MODE_MUSIC, DEFAULT_NIGHTLIGHT_SWITCH, DEFAULT_SAVE_ON_CHANGE, DEFAULT_TRANSITION, DOMAIN, YEELIGHT_HSV_TRANSACTION, YEELIGHT_RGB_TRANSITION, YEELIGHT_SLEEP_TRANSACTION, YEELIGHT_TEMPERATURE_TRANSACTION, ) from homeassistant.components.yeelight.light import ( ATTR_MINUTES, ATTR_MODE, EFFECT_CANDLE_FLICKER, EFFECT_DATE_NIGHT, EFFECT_DISCO, EFFECT_FACEBOOK, EFFECT_FAST_RANDOM_LOOP, EFFECT_HAPPY_BIRTHDAY, EFFECT_HOME, EFFECT_MOVIE, EFFECT_NIGHT_MODE, EFFECT_ROMANCE, EFFECT_STOP, EFFECT_SUNRISE, EFFECT_SUNSET, EFFECT_TWITTER, EFFECT_WHATSAPP, SERVICE_SET_AUTO_DELAY_OFF_SCENE, SERVICE_SET_COLOR_FLOW_SCENE, SERVICE_SET_COLOR_SCENE, SERVICE_SET_COLOR_TEMP_SCENE, SERVICE_SET_HSV_SCENE, SERVICE_SET_MODE, SERVICE_SET_MUSIC_MODE, SERVICE_START_FLOW, YEELIGHT_COLOR_EFFECT_LIST, YEELIGHT_MONO_EFFECT_LIST, YEELIGHT_TEMP_ONLY_EFFECT_LIST, ) from homeassistant.const import ( ATTR_ENTITY_ID, CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON, STATE_UNAVAILABLE, ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util from homeassistant.util.color import ( color_hs_to_RGB, color_hs_to_xy, color_RGB_to_hs, color_RGB_to_xy, color_temperature_kelvin_to_mired, color_temperature_mired_to_kelvin, ) from . import ( CAPABILITIES, ENTITY_LIGHT, ENTITY_NIGHTLIGHT, IP_ADDRESS, MODULE, NAME, PROPERTIES, UNIQUE_FRIENDLY_NAME, _mocked_bulb, _patch_discovery, _patch_discovery_interval, ) from tests.common import MockConfigEntry, async_fire_time_changed CONFIG_ENTRY_DATA = { CONF_HOST: IP_ADDRESS, CONF_TRANSITION: DEFAULT_TRANSITION, CONF_MODE_MUSIC: DEFAULT_MODE_MUSIC, CONF_SAVE_ON_CHANGE: DEFAULT_SAVE_ON_CHANGE, CONF_NIGHTLIGHT_SWITCH: DEFAULT_NIGHTLIGHT_SWITCH, } SUPPORT_YEELIGHT = ( LightEntityFeature.TRANSITION | LightEntityFeature.FLASH | LightEntityFeature.EFFECT ) async def test_services(hass: HomeAssistant, caplog: pytest.LogCaptureFixture) -> None: """Test Yeelight services.""" assert await async_setup_component(hass, "homeassistant", {}) config_entry = MockConfigEntry( domain=DOMAIN, data={ **CONFIG_ENTRY_DATA, CONF_MODE_MUSIC: True, CONF_SAVE_ON_CHANGE: True, CONF_NIGHTLIGHT_SWITCH: True, }, ) config_entry.add_to_hass(hass) mocked_bulb = _mocked_bulb() with _patch_discovery(), _patch_discovery_interval(), patch( f"{MODULE}.AsyncBulb", return_value=mocked_bulb ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() assert hass.states.get(ENTITY_LIGHT).state == STATE_ON assert hass.states.get(ENTITY_NIGHTLIGHT).state == STATE_OFF async def _async_test_service( service, data, method, payload=None, domain=DOMAIN, failure_side_effect=HomeAssistantError, ): err_count = len([x for x in caplog.records if x.levelno == logging.ERROR]) # success if method.startswith("async_"): mocked_method = AsyncMock() else: mocked_method = MagicMock() setattr(mocked_bulb, method, mocked_method) await hass.services.async_call(domain, service, data, blocking=True) if payload is None: mocked_method.assert_called_once() elif isinstance(payload, list): mocked_method.assert_called_once_with(*payload) else: mocked_method.assert_called_once_with(**payload) assert ( len([x for x in caplog.records if x.levelno == logging.ERROR]) == err_count ) # failure if failure_side_effect: if method.startswith("async_"): mocked_method = AsyncMock(side_effect=failure_side_effect) else: mocked_method = MagicMock(side_effect=failure_side_effect) setattr(mocked_bulb, method, mocked_method) with pytest.raises(failure_side_effect): await hass.services.async_call(domain, service, data, blocking=True) # turn_on rgb_color brightness = 100 rgb_color = (0, 128, 255) transition = 2 mocked_bulb.last_properties["power"] = "off" await hass.services.async_call( "light", SERVICE_TURN_ON, { ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_BRIGHTNESS: brightness, ATTR_RGB_COLOR: rgb_color, ATTR_FLASH: FLASH_LONG, ATTR_EFFECT: EFFECT_STOP, ATTR_TRANSITION: transition, }, blocking=True, ) mocked_bulb.async_turn_on.assert_called_once_with( duration=transition * 1000, light_type=LightType.Main, power_mode=PowerMode.NORMAL, ) mocked_bulb.async_turn_on.reset_mock() mocked_bulb.async_start_music.assert_called_once() mocked_bulb.async_start_music.reset_mock() mocked_bulb.async_set_brightness.assert_called_once_with( brightness / 255 * 100, duration=transition * 1000, light_type=LightType.Main ) mocked_bulb.async_set_brightness.reset_mock() mocked_bulb.async_set_color_temp.assert_not_called() mocked_bulb.async_set_color_temp.reset_mock() mocked_bulb.async_set_hsv.assert_not_called() mocked_bulb.async_set_hsv.reset_mock() mocked_bulb.async_set_rgb.assert_called_once_with( *rgb_color, duration=transition * 1000, light_type=LightType.Main ) mocked_bulb.async_set_rgb.reset_mock() mocked_bulb.async_start_flow.assert_called_once() # flash mocked_bulb.async_start_flow.reset_mock() mocked_bulb.async_stop_flow.assert_called_once_with(light_type=LightType.Main) mocked_bulb.async_stop_flow.reset_mock() # turn_on hs_color brightness = 100 hs_color = (180, 100) transition = 2 await hass.services.async_call( "light", SERVICE_TURN_ON, { ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_BRIGHTNESS: brightness, ATTR_HS_COLOR: hs_color, ATTR_FLASH: FLASH_LONG, ATTR_EFFECT: EFFECT_STOP, ATTR_TRANSITION: transition, }, blocking=True, ) mocked_bulb.async_turn_on.assert_called_once_with( duration=transition * 1000, light_type=LightType.Main, power_mode=PowerMode.NORMAL, ) mocked_bulb.async_turn_on.reset_mock() mocked_bulb.async_start_music.assert_called_once() mocked_bulb.async_start_music.reset_mock() mocked_bulb.async_set_brightness.assert_called_once_with( brightness / 255 * 100, duration=transition * 1000, light_type=LightType.Main ) mocked_bulb.async_set_brightness.reset_mock() mocked_bulb.async_set_color_temp.assert_not_called() mocked_bulb.async_set_color_temp.reset_mock() mocked_bulb.async_set_hsv.assert_called_once_with( *hs_color, duration=transition * 1000, light_type=LightType.Main ) mocked_bulb.async_set_hsv.reset_mock() mocked_bulb.async_set_rgb.assert_not_called() mocked_bulb.async_set_rgb.reset_mock() mocked_bulb.async_start_flow.assert_called_once() # flash mocked_bulb.async_start_flow.reset_mock() mocked_bulb.async_stop_flow.assert_called_once_with(light_type=LightType.Main) mocked_bulb.async_stop_flow.reset_mock() # turn_on color_temp brightness = 100 color_temp = 200 transition = 1 mocked_bulb.last_properties["power"] = "off" await hass.services.async_call( "light", SERVICE_TURN_ON, { ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_BRIGHTNESS: brightness, ATTR_COLOR_TEMP: color_temp, ATTR_FLASH: FLASH_LONG, ATTR_EFFECT: EFFECT_STOP, ATTR_TRANSITION: transition, }, blocking=True, ) mocked_bulb.async_turn_on.assert_called_once_with( duration=transition * 1000, light_type=LightType.Main, power_mode=PowerMode.NORMAL, ) mocked_bulb.async_turn_on.reset_mock() mocked_bulb.async_start_music.assert_called_once() mocked_bulb.async_set_brightness.assert_called_once_with( brightness / 255 * 100, duration=transition * 1000, light_type=LightType.Main ) mocked_bulb.async_set_color_temp.assert_called_once_with( color_temperature_mired_to_kelvin(color_temp), duration=transition * 1000, light_type=LightType.Main, ) mocked_bulb.async_set_hsv.assert_not_called() mocked_bulb.async_set_rgb.assert_not_called() mocked_bulb.async_start_flow.assert_called_once() # flash mocked_bulb.async_stop_flow.assert_called_once_with(light_type=LightType.Main) # turn_on color_temp - flash short brightness = 100 color_temp = 200 transition = 1 mocked_bulb.async_start_music.reset_mock() mocked_bulb.async_set_brightness.reset_mock() mocked_bulb.async_set_color_temp.reset_mock() mocked_bulb.async_start_flow.reset_mock() mocked_bulb.async_stop_flow.reset_mock() mocked_bulb.last_properties["power"] = "off" await hass.services.async_call( "light", SERVICE_TURN_ON, { ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_BRIGHTNESS: brightness, ATTR_COLOR_TEMP: color_temp, ATTR_FLASH: FLASH_SHORT, ATTR_EFFECT: EFFECT_STOP, ATTR_TRANSITION: transition, }, blocking=True, ) mocked_bulb.async_turn_on.assert_called_once_with( duration=transition * 1000, light_type=LightType.Main, power_mode=PowerMode.NORMAL, ) mocked_bulb.async_turn_on.reset_mock() mocked_bulb.async_start_music.assert_called_once() mocked_bulb.async_set_brightness.assert_called_once_with( brightness / 255 * 100, duration=transition * 1000, light_type=LightType.Main ) mocked_bulb.async_set_color_temp.assert_called_once_with( color_temperature_mired_to_kelvin(color_temp), duration=transition * 1000, light_type=LightType.Main, ) mocked_bulb.async_set_hsv.assert_not_called() mocked_bulb.async_set_rgb.assert_not_called() mocked_bulb.async_start_flow.assert_called_once() # flash mocked_bulb.async_stop_flow.assert_called_once_with(light_type=LightType.Main) # turn_on nightlight await _async_test_service( SERVICE_TURN_ON, {ATTR_ENTITY_ID: ENTITY_NIGHTLIGHT}, "async_turn_on", payload={ "duration": DEFAULT_TRANSITION, "light_type": LightType.Main, "power_mode": PowerMode.MOONLIGHT, }, domain="light", ) mocked_bulb.last_properties["power"] = "on" assert hass.states.get(ENTITY_LIGHT).state != STATE_UNAVAILABLE # turn_off await _async_test_service( SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_TRANSITION: transition}, "async_turn_off", domain="light", payload={"duration": transition * 1000, "light_type": LightType.Main}, ) # set_mode mode = "rgb" await _async_test_service( SERVICE_SET_MODE, {ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_MODE: "rgb"}, "async_set_power_mode", [PowerMode[mode.upper()]], ) # start_flow await _async_test_service( SERVICE_START_FLOW, { ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_TRANSITIONS: [{YEELIGHT_TEMPERATURE_TRANSACTION: [1900, 2000, 60]}], }, "async_start_flow", ) # set_color_scene await _async_test_service( SERVICE_SET_COLOR_SCENE, { ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_RGB_COLOR: [10, 20, 30], ATTR_BRIGHTNESS: 50, }, "async_set_scene", [SceneClass.COLOR, 10, 20, 30, 50], ) # set_hsv_scene await _async_test_service( SERVICE_SET_HSV_SCENE, {ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_HS_COLOR: [180, 50], ATTR_BRIGHTNESS: 50}, "async_set_scene", [SceneClass.HSV, 180, 50, 50], ) # set_color_temp_scene await _async_test_service( SERVICE_SET_COLOR_TEMP_SCENE, {ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_KELVIN: 4000, ATTR_BRIGHTNESS: 50}, "async_set_scene", [SceneClass.CT, 4000, 50], ) # set_color_flow_scene await _async_test_service( SERVICE_SET_COLOR_FLOW_SCENE, { ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_TRANSITIONS: [{YEELIGHT_TEMPERATURE_TRANSACTION: [1900, 2000, 60]}], }, "async_set_scene", ) # set_auto_delay_off_scene await _async_test_service( SERVICE_SET_AUTO_DELAY_OFF_SCENE, {ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_MINUTES: 1, ATTR_BRIGHTNESS: 50}, "async_set_scene", [SceneClass.AUTO_DELAY_OFF, 50, 1], ) # set_music_mode failure enable mocked_bulb.async_start_music = MagicMock(side_effect=AssertionError) assert "Unable to turn on music mode, consider disabling it" not in caplog.text await hass.services.async_call( DOMAIN, SERVICE_SET_MUSIC_MODE, {ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_MODE_MUSIC: "true"}, blocking=True, ) assert mocked_bulb.async_start_music.mock_calls == [call()] assert "Unable to turn on music mode, consider disabling it" in caplog.text # set_music_mode disable await _async_test_service( SERVICE_SET_MUSIC_MODE, {ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_MODE_MUSIC: "false"}, "async_stop_music", failure_side_effect=None, ) # set_music_mode success enable await _async_test_service( SERVICE_SET_MUSIC_MODE, {ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_MODE_MUSIC: "true"}, "async_start_music", failure_side_effect=None, ) # test _cmd wrapper error handler mocked_bulb.last_properties["power"] = "off" mocked_bulb.available = True await hass.services.async_call( "homeassistant", "update_entity", {ATTR_ENTITY_ID: ENTITY_LIGHT}, blocking=True, ) assert hass.states.get(ENTITY_LIGHT).state == STATE_OFF mocked_bulb.async_turn_on = AsyncMock() mocked_bulb.async_set_brightness = AsyncMock(side_effect=BulbException) with pytest.raises(HomeAssistantError): await hass.services.async_call( "light", SERVICE_TURN_ON, {ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_BRIGHTNESS: 50}, blocking=True, ) assert hass.states.get(ENTITY_LIGHT).state == STATE_OFF mocked_bulb.async_set_brightness = AsyncMock(side_effect=asyncio.TimeoutError) with pytest.raises(HomeAssistantError): await hass.services.async_call( "light", SERVICE_TURN_ON, {ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_BRIGHTNESS: 55}, blocking=True, ) assert hass.states.get(ENTITY_LIGHT).state == STATE_OFF mocked_bulb.async_set_brightness = AsyncMock(side_effect=socket.error) with pytest.raises(HomeAssistantError): await hass.services.async_call( "light", SERVICE_TURN_ON, {ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_BRIGHTNESS: 55}, blocking=True, ) assert hass.states.get(ENTITY_LIGHT).state == STATE_UNAVAILABLE async def test_update_errors( hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: """Test update errors.""" assert await async_setup_component(hass, "homeassistant", {}) config_entry = MockConfigEntry( domain=DOMAIN, data={ **CONFIG_ENTRY_DATA, CONF_MODE_MUSIC: True, CONF_SAVE_ON_CHANGE: True, CONF_NIGHTLIGHT_SWITCH: True, }, ) config_entry.add_to_hass(hass) mocked_bulb = _mocked_bulb() with _patch_discovery(), _patch_discovery_interval(), patch( f"{MODULE}.AsyncBulb", return_value=mocked_bulb ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() assert hass.states.get(ENTITY_LIGHT).state == STATE_ON assert hass.states.get(ENTITY_NIGHTLIGHT).state == STATE_OFF # Timeout usually means the bulb is overloaded with commands # but will still respond eventually. mocked_bulb.async_turn_off = AsyncMock(side_effect=asyncio.TimeoutError) with pytest.raises(HomeAssistantError): await hass.services.async_call( "light", SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_LIGHT}, blocking=True, ) assert hass.states.get(ENTITY_LIGHT).state == STATE_ON # socket.error usually means the bulb dropped the connection # or lost wifi, then came back online and forced the existing # connection closed with a TCP RST mocked_bulb.async_turn_off = AsyncMock(side_effect=socket.error) with pytest.raises(HomeAssistantError): await hass.services.async_call( "light", SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_LIGHT}, blocking=True, ) assert hass.states.get(ENTITY_LIGHT).state == STATE_UNAVAILABLE async def test_state_already_set_avoid_ratelimit(hass: HomeAssistant) -> None: """Ensure we suppress state changes that will increase the rate limit when there is no change.""" mocked_bulb = _mocked_bulb() properties = {**PROPERTIES} properties.pop("active_mode") properties.pop("nl_br") properties["color_mode"] = "3" # HSV mocked_bulb.last_properties = properties mocked_bulb.bulb_type = BulbType.Color config_entry = MockConfigEntry( domain=DOMAIN, data={**CONFIG_ENTRY_DATA, CONF_NIGHTLIGHT_SWITCH: False} ) config_entry.add_to_hass(hass) with _patch_discovery(), _patch_discovery_interval(), patch( f"{MODULE}.AsyncBulb", return_value=mocked_bulb ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() # We use asyncio.create_task now to avoid # blocking starting so we need to block again await hass.async_block_till_done() await hass.services.async_call( "light", SERVICE_TURN_ON, { ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_HS_COLOR: (PROPERTIES["hue"], PROPERTIES["sat"]), }, blocking=True, ) assert mocked_bulb.async_set_hsv.mock_calls == [] assert mocked_bulb.async_set_rgb.mock_calls == [] assert mocked_bulb.async_set_color_temp.mock_calls == [] assert mocked_bulb.async_set_brightness.mock_calls == [] mocked_bulb.last_properties["color_mode"] = 1 rgb = int(PROPERTIES["rgb"]) blue = rgb & 0xFF green = (rgb >> 8) & 0xFF red = (rgb >> 16) & 0xFF await hass.services.async_call( "light", SERVICE_TURN_ON, {ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_RGB_COLOR: (red, green, blue)}, blocking=True, ) assert mocked_bulb.async_set_hsv.mock_calls == [] assert mocked_bulb.async_set_rgb.mock_calls == [] assert mocked_bulb.async_set_color_temp.mock_calls == [] assert mocked_bulb.async_set_brightness.mock_calls == [] mocked_bulb.async_set_rgb.reset_mock() mocked_bulb.last_properties["flowing"] = "1" await hass.services.async_call( "light", SERVICE_TURN_ON, {ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_RGB_COLOR: (red, green, blue)}, blocking=True, ) assert mocked_bulb.async_set_hsv.mock_calls == [] assert mocked_bulb.async_set_rgb.mock_calls == [ call(255, 0, 0, duration=350, light_type=ANY) ] assert mocked_bulb.async_set_color_temp.mock_calls == [] assert mocked_bulb.async_set_brightness.mock_calls == [] mocked_bulb.async_set_rgb.reset_mock() mocked_bulb.last_properties["flowing"] = "0" # color model needs a workaround (see MODELS_WITH_DELAYED_ON_TRANSITION) mocked_bulb.model = "color" await hass.services.async_call( "light", SERVICE_TURN_ON, { ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_BRIGHTNESS_PCT: PROPERTIES["bright"], }, blocking=True, ) assert mocked_bulb.async_set_hsv.mock_calls == [] assert mocked_bulb.async_set_rgb.mock_calls == [] assert mocked_bulb.async_set_color_temp.mock_calls == [] assert mocked_bulb.async_set_brightness.mock_calls == [ call(pytest.approx(50.1, 0.1), duration=350, light_type=ANY) ] mocked_bulb.async_set_brightness.reset_mock() mocked_bulb.model = "colora" # colora does not need a workaround await hass.services.async_call( "light", SERVICE_TURN_ON, { ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_BRIGHTNESS_PCT: PROPERTIES["bright"], }, blocking=True, ) assert mocked_bulb.async_set_hsv.mock_calls == [] assert mocked_bulb.async_set_rgb.mock_calls == [] assert mocked_bulb.async_set_color_temp.mock_calls == [] assert mocked_bulb.async_set_brightness.mock_calls == [] await hass.services.async_call( "light", SERVICE_TURN_ON, {ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_COLOR_TEMP: 250}, blocking=True, ) assert mocked_bulb.async_set_hsv.mock_calls == [] assert mocked_bulb.async_set_rgb.mock_calls == [] # Should call for the color mode change assert mocked_bulb.async_set_color_temp.mock_calls == [ call(4000, duration=350, light_type=ANY) ] assert mocked_bulb.async_set_brightness.mock_calls == [] mocked_bulb.async_set_color_temp.reset_mock() mocked_bulb.last_properties["color_mode"] = 2 await hass.services.async_call( "light", SERVICE_TURN_ON, {ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_COLOR_TEMP: 250}, blocking=True, ) assert mocked_bulb.async_set_hsv.mock_calls == [] assert mocked_bulb.async_set_rgb.mock_calls == [] assert mocked_bulb.async_set_color_temp.mock_calls == [] assert mocked_bulb.async_set_brightness.mock_calls == [] mocked_bulb.last_properties["flowing"] = "1" await hass.services.async_call( "light", SERVICE_TURN_ON, {ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_COLOR_TEMP: 250}, blocking=True, ) assert mocked_bulb.async_set_hsv.mock_calls == [] assert mocked_bulb.async_set_rgb.mock_calls == [] assert mocked_bulb.async_set_color_temp.mock_calls == [ call(4000, duration=350, light_type=ANY) ] assert mocked_bulb.async_set_brightness.mock_calls == [] mocked_bulb.async_set_color_temp.reset_mock() mocked_bulb.last_properties["flowing"] = "0" mocked_bulb.last_properties["color_mode"] = 3 # This last change should generate a call even though # the color mode is the same since the HSV has changed await hass.services.async_call( "light", SERVICE_TURN_ON, {ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_HS_COLOR: (5, 5)}, blocking=True, ) assert mocked_bulb.async_set_hsv.mock_calls == [ call(5.0, 5.0, duration=350, light_type=ANY) ] assert mocked_bulb.async_set_rgb.mock_calls == [] assert mocked_bulb.async_set_color_temp.mock_calls == [] assert mocked_bulb.async_set_brightness.mock_calls == [] mocked_bulb.async_set_hsv.reset_mock() await hass.services.async_call( "light", SERVICE_TURN_ON, {ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_HS_COLOR: (100, 35)}, blocking=True, ) assert mocked_bulb.async_set_hsv.mock_calls == [] assert mocked_bulb.async_set_rgb.mock_calls == [] assert mocked_bulb.async_set_color_temp.mock_calls == [] assert mocked_bulb.async_set_brightness.mock_calls == [] mocked_bulb.last_properties["flowing"] = "1" await hass.services.async_call( "light", SERVICE_TURN_ON, {ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_HS_COLOR: (100, 35)}, blocking=True, ) assert mocked_bulb.async_set_hsv.mock_calls == [ call(100.0, 35.0, duration=350, light_type=ANY) ] assert mocked_bulb.async_set_rgb.mock_calls == [] assert mocked_bulb.async_set_color_temp.mock_calls == [] assert mocked_bulb.async_set_brightness.mock_calls == [] mocked_bulb.last_properties["flowing"] = "0" async def test_device_types( hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: """Test different device types.""" mocked_bulb = _mocked_bulb() properties = {**PROPERTIES} properties.pop("active_mode") properties["color_mode"] = "3" # HSV mocked_bulb.last_properties = properties async def _async_setup(config_entry): with _patch_discovery(), patch(f"{MODULE}.AsyncBulb", return_value=mocked_bulb): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() # We use asyncio.create_task now to avoid # blocking starting so we need to block again await hass.async_block_till_done() async def _async_test( bulb_type, model, target_properties, nightlight_entity_properties=None, name=UNIQUE_FRIENDLY_NAME, entity_id=ENTITY_LIGHT, nightlight_mode_properties=None, ): config_entry = MockConfigEntry( domain=DOMAIN, data={**CONFIG_ENTRY_DATA, CONF_NIGHTLIGHT_SWITCH: False} ) config_entry.add_to_hass(hass) mocked_bulb.bulb_type = bulb_type model_specs = _MODEL_SPECS.get(model) type(mocked_bulb).get_model_specs = MagicMock(return_value=model_specs) original_nightlight_brightness = mocked_bulb.last_properties["nl_br"] mocked_bulb.last_properties["nl_br"] = "0" await _async_setup(config_entry) state = hass.states.get(entity_id) assert state.state == "on" target_properties["friendly_name"] = name target_properties["flowing"] = False target_properties["night_light"] = False target_properties["music_mode"] = False assert dict(state.attributes) == target_properties await hass.config_entries.async_unload(config_entry.entry_id) await config_entry.async_remove(hass) registry = er.async_get(hass) registry.async_clear_config_entry(config_entry.entry_id) mocked_bulb.last_properties["nl_br"] = original_nightlight_brightness # nightlight as a setting of the main entity if nightlight_mode_properties is not None: mocked_bulb.last_properties["active_mode"] = True config_entry.add_to_hass(hass) await _async_setup(config_entry) state = hass.states.get(entity_id) assert state.state == "on" nightlight_mode_properties["friendly_name"] = name nightlight_mode_properties["flowing"] = False nightlight_mode_properties["night_light"] = True nightlight_mode_properties["music_mode"] = False assert dict(state.attributes) == nightlight_mode_properties await hass.config_entries.async_unload(config_entry.entry_id) await config_entry.async_remove(hass) registry.async_clear_config_entry(config_entry.entry_id) await hass.async_block_till_done() mocked_bulb.last_properties.pop("active_mode") # nightlight as a separate entity if nightlight_entity_properties is not None: config_entry = MockConfigEntry( domain=DOMAIN, data={**CONFIG_ENTRY_DATA, CONF_NIGHTLIGHT_SWITCH: True} ) config_entry.add_to_hass(hass) await _async_setup(config_entry) assert hass.states.get(entity_id).state == "off" state = hass.states.get(f"{entity_id}_nightlight") assert state.state == "on" nightlight_entity_properties["friendly_name"] = f"{name} Nightlight" nightlight_entity_properties["icon"] = "mdi:weather-night" nightlight_entity_properties["flowing"] = False nightlight_entity_properties["night_light"] = True nightlight_entity_properties["music_mode"] = False assert dict(state.attributes) == nightlight_entity_properties await hass.config_entries.async_unload(config_entry.entry_id) await config_entry.async_remove(hass) registry.async_clear_config_entry(config_entry.entry_id) await hass.async_block_till_done() bright = round(255 * int(PROPERTIES["bright"]) / 100) ct = int(PROPERTIES["ct"]) ct_mired = color_temperature_kelvin_to_mired(int(PROPERTIES["ct"])) hue = int(PROPERTIES["hue"]) sat = int(PROPERTIES["sat"]) rgb = int(PROPERTIES["rgb"]) rgb_color = ((rgb >> 16) & 0xFF, (rgb >> 8) & 0xFF, rgb & 0xFF) hs_color = (hue, sat) bg_bright = round(255 * int(PROPERTIES["bg_bright"]) / 100) bg_ct = int(PROPERTIES["bg_ct"]) bg_ct_kelvin = color_temperature_kelvin_to_mired(int(PROPERTIES["bg_ct"])) bg_hue = int(PROPERTIES["bg_hue"]) bg_sat = int(PROPERTIES["bg_sat"]) bg_rgb = int(PROPERTIES["bg_rgb"]) bg_hs_color = (bg_hue, bg_sat) bg_rgb_color = ((bg_rgb >> 16) & 0xFF, (bg_rgb >> 8) & 0xFF, bg_rgb & 0xFF) nl_br = round(255 * int(PROPERTIES["nl_br"]) / 100) # Default await _async_test( None, "mono", { "effect_list": YEELIGHT_MONO_EFFECT_LIST, "effect": None, "supported_features": SUPPORT_YEELIGHT, "brightness": bright, "color_mode": "brightness", "supported_color_modes": ["brightness"], }, ) # White await _async_test( BulbType.White, "mono", { "effect_list": YEELIGHT_MONO_EFFECT_LIST, "supported_features": SUPPORT_YEELIGHT, "effect": None, "brightness": bright, "color_mode": "brightness", "supported_color_modes": ["brightness"], }, ) # Color - color mode CT mocked_bulb.last_properties["color_mode"] = "2" # CT model_specs = _MODEL_SPECS["color"] await _async_test( BulbType.Color, "color", { "effect_list": YEELIGHT_COLOR_EFFECT_LIST, "effect": None, "supported_features": SUPPORT_YEELIGHT, "min_color_temp_kelvin": model_specs["color_temp"]["min"], "max_color_temp_kelvin": color_temperature_mired_to_kelvin( color_temperature_kelvin_to_mired(model_specs["color_temp"]["max"]) ), "min_mireds": color_temperature_kelvin_to_mired( model_specs["color_temp"]["max"] ), "max_mireds": color_temperature_kelvin_to_mired( model_specs["color_temp"]["min"] ), "brightness": bright, "color_temp_kelvin": ct, "color_temp": ct_mired, "color_mode": "color_temp", "supported_color_modes": ["color_temp", "hs", "rgb"], "hs_color": (26.812, 34.87), "rgb_color": (255, 205, 166), "xy_color": (0.421, 0.364), }, nightlight_entity_properties={ "supported_features": 0, "color_mode": "onoff", "supported_color_modes": ["onoff"], }, nightlight_mode_properties={ "effect_list": YEELIGHT_COLOR_EFFECT_LIST, "effect": None, "supported_features": SUPPORT_YEELIGHT, "hs_color": (28.401, 100.0), "rgb_color": (255, 120, 0), "xy_color": (0.621, 0.367), "min_color_temp_kelvin": model_specs["color_temp"]["min"], "max_color_temp_kelvin": color_temperature_mired_to_kelvin( color_temperature_kelvin_to_mired(model_specs["color_temp"]["max"]) ), "min_mireds": color_temperature_kelvin_to_mired( model_specs["color_temp"]["max"] ), "max_mireds": color_temperature_kelvin_to_mired( model_specs["color_temp"]["min"] ), "brightness": nl_br, "color_mode": "color_temp", "supported_color_modes": ["color_temp", "hs", "rgb"], "color_temp_kelvin": model_specs["color_temp"]["min"], "color_temp": color_temperature_kelvin_to_mired( model_specs["color_temp"]["min"] ), }, ) # Color - color mode HS mocked_bulb.last_properties["color_mode"] = "3" # HSV model_specs = _MODEL_SPECS["color"] await _async_test( BulbType.Color, "color", { "effect_list": YEELIGHT_COLOR_EFFECT_LIST, "effect": None, "supported_features": SUPPORT_YEELIGHT, "min_color_temp_kelvin": model_specs["color_temp"]["min"], "max_color_temp_kelvin": color_temperature_mired_to_kelvin( color_temperature_kelvin_to_mired(model_specs["color_temp"]["max"]) ), "min_mireds": color_temperature_kelvin_to_mired( model_specs["color_temp"]["max"] ), "max_mireds": color_temperature_kelvin_to_mired( model_specs["color_temp"]["min"] ), "brightness": bright, "hs_color": hs_color, "rgb_color": color_hs_to_RGB(*hs_color), "xy_color": color_hs_to_xy(*hs_color), "color_temp": None, "color_temp_kelvin": None, "color_mode": "hs", "supported_color_modes": ["color_temp", "hs", "rgb"], }, nightlight_entity_properties={ "supported_features": 0, "color_mode": "onoff", "supported_color_modes": ["onoff"], }, ) # Color - color mode RGB mocked_bulb.last_properties["color_mode"] = "1" # RGB model_specs = _MODEL_SPECS["color"] await _async_test( BulbType.Color, "color", { "effect_list": YEELIGHT_COLOR_EFFECT_LIST, "effect": None, "supported_features": SUPPORT_YEELIGHT, "min_color_temp_kelvin": model_specs["color_temp"]["min"], "max_color_temp_kelvin": color_temperature_mired_to_kelvin( color_temperature_kelvin_to_mired(model_specs["color_temp"]["max"]) ), "min_mireds": color_temperature_kelvin_to_mired( model_specs["color_temp"]["max"] ), "max_mireds": color_temperature_kelvin_to_mired( model_specs["color_temp"]["min"] ), "brightness": bright, "hs_color": color_RGB_to_hs(*rgb_color), "rgb_color": rgb_color, "xy_color": color_RGB_to_xy(*rgb_color), "color_temp": None, "color_temp_kelvin": None, "color_mode": "rgb", "supported_color_modes": ["color_temp", "hs", "rgb"], }, nightlight_entity_properties={ "supported_features": 0, "color_mode": "onoff", "supported_color_modes": ["onoff"], }, ) # Color - color mode HS but no hue mocked_bulb.last_properties["color_mode"] = "3" # HSV mocked_bulb.last_properties["hue"] = None model_specs = _MODEL_SPECS["color"] await _async_test( BulbType.Color, "color", { "effect_list": YEELIGHT_COLOR_EFFECT_LIST, "effect": None, "supported_features": SUPPORT_YEELIGHT, "min_color_temp_kelvin": model_specs["color_temp"]["min"], "max_color_temp_kelvin": color_temperature_mired_to_kelvin( color_temperature_kelvin_to_mired(model_specs["color_temp"]["max"]) ), "min_mireds": color_temperature_kelvin_to_mired( model_specs["color_temp"]["max"] ), "max_mireds": color_temperature_kelvin_to_mired( model_specs["color_temp"]["min"] ), "brightness": bright, "hs_color": None, "rgb_color": None, "xy_color": None, "color_temp": None, "color_temp_kelvin": None, "color_mode": "hs", "supported_color_modes": ["color_temp", "hs", "rgb"], }, nightlight_entity_properties={ "supported_features": 0, "color_mode": "onoff", "supported_color_modes": ["onoff"], }, ) # Color - color mode RGB but no color mocked_bulb.last_properties["color_mode"] = "1" # RGB mocked_bulb.last_properties["rgb"] = None model_specs = _MODEL_SPECS["color"] await _async_test( BulbType.Color, "color", { "effect_list": YEELIGHT_COLOR_EFFECT_LIST, "effect": None, "supported_features": SUPPORT_YEELIGHT, "min_color_temp_kelvin": model_specs["color_temp"]["min"], "max_color_temp_kelvin": color_temperature_mired_to_kelvin( color_temperature_kelvin_to_mired(model_specs["color_temp"]["max"]) ), "min_mireds": color_temperature_kelvin_to_mired( model_specs["color_temp"]["max"] ), "max_mireds": color_temperature_kelvin_to_mired( model_specs["color_temp"]["min"] ), "brightness": bright, "hs_color": None, "rgb_color": None, "xy_color": None, "color_temp": None, "color_temp_kelvin": None, "color_mode": "rgb", "supported_color_modes": ["color_temp", "hs", "rgb"], }, nightlight_entity_properties={ "supported_features": 0, "color_mode": "onoff", "supported_color_modes": ["onoff"], }, ) # Color - unsupported color_mode mocked_bulb.last_properties["color_mode"] = 4 # Unsupported model_specs = _MODEL_SPECS["color"] await _async_test( BulbType.Color, "color", { "effect_list": YEELIGHT_COLOR_EFFECT_LIST, "effect": None, "supported_features": SUPPORT_YEELIGHT, "min_color_temp_kelvin": model_specs["color_temp"]["min"], "max_color_temp_kelvin": color_temperature_mired_to_kelvin( color_temperature_kelvin_to_mired(model_specs["color_temp"]["max"]) ), "min_mireds": color_temperature_kelvin_to_mired( model_specs["color_temp"]["max"] ), "max_mireds": color_temperature_kelvin_to_mired( model_specs["color_temp"]["min"] ), "brightness": None, "hs_color": None, "rgb_color": None, "xy_color": None, "color_temp": None, "color_temp_kelvin": None, "color_mode": "unknown", "supported_color_modes": ["color_temp", "hs", "rgb"], }, { "supported_features": 0, "color_mode": "onoff", "supported_color_modes": ["onoff"], }, ) assert "Light reported unknown color mode: 4" in caplog.text # WhiteTemp model_specs = _MODEL_SPECS["ceiling1"] await _async_test( BulbType.WhiteTemp, "ceiling1", { "effect_list": YEELIGHT_TEMP_ONLY_EFFECT_LIST, "effect": None, "supported_features": SUPPORT_YEELIGHT, "min_color_temp_kelvin": color_temperature_mired_to_kelvin( color_temperature_kelvin_to_mired(model_specs["color_temp"]["min"]) ), "max_color_temp_kelvin": color_temperature_mired_to_kelvin( color_temperature_kelvin_to_mired(model_specs["color_temp"]["max"]) ), "min_mireds": color_temperature_kelvin_to_mired( model_specs["color_temp"]["max"] ), "max_mireds": color_temperature_kelvin_to_mired( model_specs["color_temp"]["min"] ), "brightness": bright, "color_temp_kelvin": ct, "color_temp": ct_mired, "color_mode": "color_temp", "supported_color_modes": ["color_temp"], "hs_color": (26.812, 34.87), "rgb_color": (255, 205, 166), "xy_color": (0.421, 0.364), }, nightlight_entity_properties={ "supported_features": 0, "brightness": nl_br, "color_mode": "brightness", "supported_color_modes": ["brightness"], }, nightlight_mode_properties={ "effect_list": YEELIGHT_TEMP_ONLY_EFFECT_LIST, "effect": None, "supported_features": SUPPORT_YEELIGHT, "min_color_temp_kelvin": color_temperature_mired_to_kelvin( color_temperature_kelvin_to_mired(model_specs["color_temp"]["min"]) ), "max_color_temp_kelvin": color_temperature_mired_to_kelvin( color_temperature_kelvin_to_mired(model_specs["color_temp"]["max"]) ), "min_mireds": color_temperature_kelvin_to_mired( model_specs["color_temp"]["max"] ), "max_mireds": color_temperature_kelvin_to_mired( model_specs["color_temp"]["min"] ), "brightness": nl_br, "color_temp_kelvin": color_temperature_mired_to_kelvin( color_temperature_kelvin_to_mired(model_specs["color_temp"]["min"]) ), "color_temp": color_temperature_kelvin_to_mired( model_specs["color_temp"]["min"] ), "color_mode": "color_temp", "supported_color_modes": ["color_temp"], "hs_color": (28.391, 65.659), "rgb_color": (255, 166, 87), "xy_color": (0.526, 0.387), }, ) # WhiteTempMood properties.pop("power") properties["main_power"] = "on" model_specs = _MODEL_SPECS["ceiling4"] await _async_test( BulbType.WhiteTempMood, "ceiling4", { "friendly_name": NAME, "effect_list": YEELIGHT_TEMP_ONLY_EFFECT_LIST, "effect": None, "flowing": False, "night_light": True, "supported_features": SUPPORT_YEELIGHT, "min_color_temp_kelvin": color_temperature_mired_to_kelvin( color_temperature_kelvin_to_mired(model_specs["color_temp"]["min"]) ), "max_color_temp_kelvin": color_temperature_mired_to_kelvin( color_temperature_kelvin_to_mired(model_specs["color_temp"]["max"]) ), "min_mireds": color_temperature_kelvin_to_mired( model_specs["color_temp"]["max"] ), "max_mireds": color_temperature_kelvin_to_mired( model_specs["color_temp"]["min"] ), "brightness": bright, "color_temp_kelvin": ct, "color_temp": ct_mired, "color_mode": "color_temp", "supported_color_modes": ["color_temp"], "hs_color": (26.812, 34.87), "rgb_color": (255, 205, 166), "xy_color": (0.421, 0.364), }, nightlight_entity_properties={ "supported_features": 0, "brightness": nl_br, "color_mode": "brightness", "supported_color_modes": ["brightness"], }, nightlight_mode_properties={ "friendly_name": NAME, "effect_list": YEELIGHT_TEMP_ONLY_EFFECT_LIST, "effect": None, "flowing": False, "night_light": True, "supported_features": SUPPORT_YEELIGHT, "min_color_temp_kelvin": color_temperature_mired_to_kelvin( color_temperature_kelvin_to_mired(model_specs["color_temp"]["min"]) ), "max_color_temp_kelvin": color_temperature_mired_to_kelvin( color_temperature_kelvin_to_mired(model_specs["color_temp"]["max"]) ), "min_mireds": color_temperature_kelvin_to_mired( model_specs["color_temp"]["max"] ), "max_mireds": color_temperature_kelvin_to_mired( model_specs["color_temp"]["min"] ), "brightness": nl_br, "color_temp_kelvin": color_temperature_mired_to_kelvin( color_temperature_kelvin_to_mired(model_specs["color_temp"]["min"]) ), "color_temp": color_temperature_kelvin_to_mired( model_specs["color_temp"]["min"] ), "color_mode": "color_temp", "supported_color_modes": ["color_temp"], "hs_color": (28.391, 65.659), "rgb_color": (255, 166, 87), "xy_color": (0.526, 0.387), }, ) # Background light - color mode CT mocked_bulb.last_properties["bg_lmode"] = "2" # CT await _async_test( BulbType.WhiteTempMood, "ceiling4", { "effect_list": YEELIGHT_COLOR_EFFECT_LIST, "effect": None, "supported_features": SUPPORT_YEELIGHT, "min_color_temp_kelvin": 1700, "max_color_temp_kelvin": color_temperature_mired_to_kelvin( color_temperature_kelvin_to_mired(6500) ), "min_mireds": color_temperature_kelvin_to_mired(6500), "max_mireds": color_temperature_kelvin_to_mired(1700), "brightness": bg_bright, "color_temp_kelvin": bg_ct, "color_temp": bg_ct_kelvin, "color_mode": "color_temp", "supported_color_modes": ["color_temp", "hs", "rgb"], "hs_color": (27.001, 19.243), "rgb_color": (255, 228, 205), "xy_color": (0.372, 0.35), }, name=f"{UNIQUE_FRIENDLY_NAME} Ambilight", entity_id=f"{ENTITY_LIGHT}_ambilight", ) # Background light - color mode HS mocked_bulb.last_properties["bg_lmode"] = "3" # HS await _async_test( BulbType.WhiteTempMood, "ceiling4", { "effect_list": YEELIGHT_COLOR_EFFECT_LIST, "effect": None, "supported_features": SUPPORT_YEELIGHT, "min_color_temp_kelvin": 1700, "max_color_temp_kelvin": color_temperature_mired_to_kelvin( color_temperature_kelvin_to_mired(6500) ), "min_mireds": color_temperature_kelvin_to_mired(6500), "max_mireds": color_temperature_kelvin_to_mired(1700), "brightness": bg_bright, "hs_color": bg_hs_color, "rgb_color": color_hs_to_RGB(*bg_hs_color), "xy_color": color_hs_to_xy(*bg_hs_color), "color_temp": None, "color_temp_kelvin": None, "color_mode": "hs", "supported_color_modes": ["color_temp", "hs", "rgb"], }, name=f"{UNIQUE_FRIENDLY_NAME} Ambilight", entity_id=f"{ENTITY_LIGHT}_ambilight", ) # Background light - color mode RGB mocked_bulb.last_properties["bg_lmode"] = "1" # RGB await _async_test( BulbType.WhiteTempMood, "ceiling4", { "effect_list": YEELIGHT_COLOR_EFFECT_LIST, "effect": None, "supported_features": SUPPORT_YEELIGHT, "min_color_temp_kelvin": 1700, "max_color_temp_kelvin": color_temperature_mired_to_kelvin( color_temperature_kelvin_to_mired(6500) ), "min_mireds": color_temperature_kelvin_to_mired(6500), "max_mireds": color_temperature_kelvin_to_mired(1700), "brightness": bg_bright, "hs_color": color_RGB_to_hs(*bg_rgb_color), "rgb_color": bg_rgb_color, "xy_color": color_RGB_to_xy(*bg_rgb_color), "color_temp": None, "color_temp_kelvin": None, "color_mode": "rgb", "supported_color_modes": ["color_temp", "hs", "rgb"], }, name=f"{UNIQUE_FRIENDLY_NAME} Ambilight", entity_id=f"{ENTITY_LIGHT}_ambilight", ) async def test_effects(hass: HomeAssistant) -> None: """Test effects.""" assert await async_setup_component( hass, DOMAIN, { DOMAIN: { CONF_CUSTOM_EFFECTS: [ { CONF_NAME: "mock_effect", CONF_FLOW_PARAMS: { ATTR_COUNT: 3, ATTR_TRANSITIONS: [ {YEELIGHT_HSV_TRANSACTION: [300, 50, 500, 50]}, {YEELIGHT_RGB_TRANSITION: [100, 100, 100, 300, 30]}, {YEELIGHT_TEMPERATURE_TRANSACTION: [3000, 200, 20]}, {YEELIGHT_SLEEP_TRANSACTION: [800]}, ], }, } ] } }, ) config_entry = MockConfigEntry(domain=DOMAIN, data=CONFIG_ENTRY_DATA) config_entry.add_to_hass(hass) mocked_bulb = _mocked_bulb() with _patch_discovery(), _patch_discovery_interval(), patch( f"{MODULE}.AsyncBulb", return_value=mocked_bulb ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() assert hass.states.get(ENTITY_LIGHT).attributes.get( "effect_list" ) == YEELIGHT_COLOR_EFFECT_LIST + ["mock_effect"] async def _async_test_effect(name, target=None, called=True): async_mocked_start_flow = AsyncMock() mocked_bulb.async_start_flow = async_mocked_start_flow await hass.services.async_call( "light", SERVICE_TURN_ON, {ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_EFFECT: name}, blocking=True, ) if not called: return async_mocked_start_flow.assert_called_once() if target is None: return args, _ = async_mocked_start_flow.call_args flow = args[0] assert flow.count == target.count assert flow.action == target.action assert str(flow.transitions) == str(target.transitions) effects = { "mock_effect": Flow( count=3, transitions=[ HSVTransition(300, 50, 500, 50), RGBTransition(100, 100, 100, 300, 30), TemperatureTransition(3000, 200, 20), SleepTransition(800), ], ), EFFECT_DISCO: Flow(transitions=transitions.disco()), EFFECT_FAST_RANDOM_LOOP: None, EFFECT_WHATSAPP: Flow(count=2, transitions=transitions.pulse(37, 211, 102)), EFFECT_FACEBOOK: Flow(count=2, transitions=transitions.pulse(59, 89, 152)), EFFECT_TWITTER: Flow(count=2, transitions=transitions.pulse(0, 172, 237)), EFFECT_HOME: Flow( count=0, action=Action.recover, transitions=[ TemperatureTransition(degrees=3200, duration=500, brightness=80) ], ), EFFECT_NIGHT_MODE: Flow( count=0, action=Action.recover, transitions=[RGBTransition(0xFF, 0x99, 0x00, duration=500, brightness=1)], ), EFFECT_DATE_NIGHT: Flow( count=0, action=Action.recover, transitions=[RGBTransition(0xFF, 0x66, 0x00, duration=500, brightness=50)], ), EFFECT_MOVIE: Flow( count=0, action=Action.recover, transitions=[ RGBTransition( red=0x14, green=0x14, blue=0x32, duration=500, brightness=50 ) ], ), EFFECT_SUNRISE: Flow( count=1, action=Action.stay, transitions=[ RGBTransition( red=0xFF, green=0x4D, blue=0x00, duration=50, brightness=1 ), TemperatureTransition(degrees=1700, duration=360000, brightness=10), TemperatureTransition(degrees=2700, duration=540000, brightness=100), ], ), EFFECT_SUNSET: Flow( count=1, action=Action.off, transitions=[ TemperatureTransition(degrees=2700, duration=50, brightness=10), TemperatureTransition(degrees=1700, duration=180000, brightness=5), RGBTransition( red=0xFF, green=0x4C, blue=0x00, duration=420000, brightness=1 ), ], ), EFFECT_ROMANCE: Flow( count=0, action=Action.stay, transitions=[ RGBTransition( red=0x59, green=0x15, blue=0x6D, duration=4000, brightness=1 ), RGBTransition( red=0x66, green=0x14, blue=0x2A, duration=4000, brightness=1 ), ], ), EFFECT_HAPPY_BIRTHDAY: Flow( count=0, action=Action.stay, transitions=[ RGBTransition( red=0xDC, green=0x50, blue=0x19, duration=1996, brightness=80 ), RGBTransition( red=0xDC, green=0x78, blue=0x1E, duration=1996, brightness=80 ), RGBTransition( red=0xAA, green=0x32, blue=0x14, duration=1996, brightness=80 ), ], ), EFFECT_CANDLE_FLICKER: Flow( count=0, action=Action.recover, transitions=[ TemperatureTransition(degrees=2700, duration=800, brightness=50), TemperatureTransition(degrees=2700, duration=800, brightness=30), TemperatureTransition(degrees=2700, duration=1200, brightness=80), TemperatureTransition(degrees=2700, duration=800, brightness=60), TemperatureTransition(degrees=2700, duration=1200, brightness=90), TemperatureTransition(degrees=2700, duration=2400, brightness=50), TemperatureTransition(degrees=2700, duration=1200, brightness=80), TemperatureTransition(degrees=2700, duration=800, brightness=60), TemperatureTransition(degrees=2700, duration=400, brightness=70), ], ), } for name, target in effects.items(): await _async_test_effect(name, target) await _async_test_effect("not_existed", called=False) async def test_ambilight_with_nightlight_disabled(hass: HomeAssistant) -> None: """Test that main light on ambilights with the nightlight disabled shows the correct brightness.""" mocked_bulb = _mocked_bulb() properties = {**PROPERTIES} capabilities = {**CAPABILITIES} capabilities["model"] = "ceiling10" properties["color_mode"] = "3" # HSV properties["bg_power"] = "off" properties["bg_lmode"] = "2" # CT mocked_bulb.last_properties = properties mocked_bulb.bulb_type = BulbType.WhiteTempMood main_light_entity_id = "light.yeelight_ceiling10_0x15243f" config_entry = MockConfigEntry( domain=DOMAIN, data={**CONFIG_ENTRY_DATA, CONF_NIGHTLIGHT_SWITCH: False}, options={**CONFIG_ENTRY_DATA, CONF_NIGHTLIGHT_SWITCH: False}, ) config_entry.add_to_hass(hass) with _patch_discovery(capabilities=capabilities), patch( f"{MODULE}.AsyncBulb", return_value=mocked_bulb ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() # We use asyncio.create_task now to avoid # blocking starting so we need to block again await hass.async_block_till_done() state = hass.states.get(main_light_entity_id) assert state.state == "on" # bg_power off should not set the brightness to 0 assert state.attributes[ATTR_BRIGHTNESS] == 128 async def test_state_fails_to_update_triggers_update(hass: HomeAssistant) -> None: """Ensure we call async_get_properties if the turn on/off fails to update the state.""" mocked_bulb = _mocked_bulb() properties = {**PROPERTIES} properties.pop("active_mode") properties["color_mode"] = "3" # HSV mocked_bulb.last_properties = properties mocked_bulb.bulb_type = BulbType.Color config_entry = MockConfigEntry( domain=DOMAIN, data={**CONFIG_ENTRY_DATA, CONF_NIGHTLIGHT_SWITCH: False} ) config_entry.add_to_hass(hass) with _patch_discovery(), _patch_discovery_interval(), patch( f"{MODULE}.AsyncBulb", return_value=mocked_bulb ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() # We use asyncio.create_task now to avoid # blocking starting so we need to block again await hass.async_block_till_done() assert len(mocked_bulb.async_get_properties.mock_calls) == 1 mocked_bulb.last_properties["power"] = "off" await hass.services.async_call( "light", SERVICE_TURN_ON, { ATTR_ENTITY_ID: ENTITY_LIGHT, }, blocking=True, ) assert len(mocked_bulb.async_turn_on.mock_calls) == 1 async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=1)) await hass.async_block_till_done() assert len(mocked_bulb.async_get_properties.mock_calls) == 2 mocked_bulb.last_properties["power"] = "on" for _ in range(5): await hass.services.async_call( "light", SERVICE_TURN_OFF, { ATTR_ENTITY_ID: ENTITY_LIGHT, }, blocking=True, ) assert len(mocked_bulb.async_turn_off.mock_calls) == 5 # Even with five calls we only do one state request # since each successive call should cancel the unexpected # state check async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=2)) await hass.async_block_till_done() assert len(mocked_bulb.async_get_properties.mock_calls) == 3 # But if the state is correct no calls await hass.services.async_call( "light", SERVICE_TURN_ON, { ATTR_ENTITY_ID: ENTITY_LIGHT, }, blocking=True, ) assert len(mocked_bulb.async_turn_on.mock_calls) == 1 async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=3)) await hass.async_block_till_done() assert len(mocked_bulb.async_get_properties.mock_calls) == 3