"""Test the Yeelight light.""" import logging from yeelight import ( BulbException, BulbType, HSVTransition, LightType, PowerMode, RGBTransition, SceneClass, SleepTransition, TemperatureTransition, transitions, ) from yeelight.flow import Flow from yeelight.main import _MODEL_SPECS from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_FLASH, ATTR_HS_COLOR, ATTR_KELVIN, ATTR_RGB_COLOR, ATTR_TRANSITION, FLASH_LONG, SERVICE_TURN_OFF, SERVICE_TURN_ON, ) from homeassistant.components.yeelight import ( ATTR_COUNT, ATTR_TRANSITIONS, CONF_CUSTOM_EFFECTS, CONF_FLOW_PARAMS, CONF_NIGHTLIGHT_SWITCH_TYPE, DEFAULT_TRANSITION, DOMAIN, NIGHTLIGHT_SWITCH_TYPE_LIGHT, YEELIGHT_HSV_TRANSACTION, YEELIGHT_RGB_TRANSITION, YEELIGHT_SLEEP_TRANSACTION, YEELIGHT_TEMPERATURE_TRANSACTION, ) from homeassistant.components.yeelight.light import ( ATTR_MINUTES, ATTR_MODE, EFFECT_DISCO, EFFECT_FACEBOOK, EFFECT_FAST_RANDOM_LOOP, EFFECT_STOP, 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_START_FLOW, SUPPORT_YEELIGHT, SUPPORT_YEELIGHT_RGB, SUPPORT_YEELIGHT_WHITE_TEMP, YEELIGHT_COLOR_EFFECT_LIST, YEELIGHT_MONO_EFFECT_LIST, YEELIGHT_TEMP_ONLY_EFFECT_LIST, ) from homeassistant.const import ATTR_ENTITY_ID, CONF_DEVICES, CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component 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, MODULE, NAME, PROPERTIES, YAML_CONFIGURATION, _mocked_bulb, ) from tests.async_mock import MagicMock, patch async def test_services(hass: HomeAssistant, caplog): """Test Yeelight services.""" mocked_bulb = _mocked_bulb() with patch(f"{MODULE}.Bulb", return_value=mocked_bulb): await async_setup_component(hass, DOMAIN, YAML_CONFIGURATION) await hass.async_block_till_done() async def _async_test_service(service, data, method, payload=None, domain=DOMAIN): err_count = len([x for x in caplog.records if x.levelno == logging.ERROR]) # success mocked_method = MagicMock() setattr(type(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 type(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 mocked_method = MagicMock(side_effect=BulbException) setattr(type(mocked_bulb), method, mocked_method) await hass.services.async_call(domain, service, data, blocking=True) assert ( len([x for x in caplog.records if x.levelno == logging.ERROR]) == err_count + 1 ) # turn_on brightness = 100 color_temp = 200 transition = 1 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.turn_on.assert_called_once_with( duration=transition * 1000, light_type=LightType.Main, power_mode=PowerMode.NORMAL, ) mocked_bulb.turn_on.reset_mock() mocked_bulb.start_music.assert_called_once() mocked_bulb.set_brightness.assert_called_once_with( brightness / 255 * 100, duration=transition * 1000, light_type=LightType.Main ) mocked_bulb.set_color_temp.assert_called_once_with( color_temperature_mired_to_kelvin(color_temp), duration=transition * 1000, light_type=LightType.Main, ) mocked_bulb.start_flow.assert_called_once() # flash mocked_bulb.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}, "turn_on", payload={ "duration": DEFAULT_TRANSITION, "light_type": LightType.Main, "power_mode": PowerMode.MOONLIGHT, }, domain="light", ) # turn_off await _async_test_service( SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_TRANSITION: transition}, "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"}, "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]}], }, "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, }, "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}, "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}, "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]}], }, "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}, "set_scene", [SceneClass.AUTO_DELAY_OFF, 50, 1], ) # test _cmd wrapper error handler err_count = len([x for x in caplog.records if x.levelno == logging.ERROR]) type(mocked_bulb).turn_on = MagicMock() type(mocked_bulb).set_brightness = MagicMock(side_effect=BulbException) await hass.services.async_call( "light", SERVICE_TURN_ON, {ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_BRIGHTNESS: 50}, blocking=True, ) assert ( len([x for x in caplog.records if x.levelno == logging.ERROR]) == err_count + 1 ) async def test_device_types(hass: HomeAssistant): """Test different device types.""" properties = {**PROPERTIES} properties.pop("active_mode") properties["color_mode"] = "3" def _create_mocked_bulb(bulb_type, model, unique_id): capabilities = {**CAPABILITIES} capabilities["id"] = f"yeelight.{unique_id}" mocked_bulb = _mocked_bulb() mocked_bulb.bulb_type = bulb_type mocked_bulb.last_properties = properties mocked_bulb.capabilities = capabilities model_specs = _MODEL_SPECS.get(model) type(mocked_bulb).get_model_specs = MagicMock(return_value=model_specs) return mocked_bulb types = { "default": (None, "mono"), "white": (BulbType.White, "mono"), "color": (BulbType.Color, "color"), "white_temp": (BulbType.WhiteTemp, "ceiling1"), "white_temp_mood": (BulbType.WhiteTempMood, "ceiling4"), "ambient": (BulbType.WhiteTempMood, "ceiling4"), } devices = {} mocked_bulbs = [] unique_id = 0 for name, (bulb_type, model) in types.items(): devices[f"{name}.yeelight"] = {CONF_NAME: name} devices[f"{name}_nightlight.yeelight"] = { CONF_NAME: f"{name}_nightlight", CONF_NIGHTLIGHT_SWITCH_TYPE: NIGHTLIGHT_SWITCH_TYPE_LIGHT, } mocked_bulbs.append(_create_mocked_bulb(bulb_type, model, unique_id)) mocked_bulbs.append(_create_mocked_bulb(bulb_type, model, unique_id + 1)) unique_id += 2 with patch(f"{MODULE}.Bulb", side_effect=mocked_bulbs): await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_DEVICES: devices}}) await hass.async_block_till_done() async def _async_test( name, bulb_type, model, target_properties, nightlight_properties=None, entity_name=None, entity_id=None, ): if entity_id is None: entity_id = f"light.{name}" state = hass.states.get(entity_id) assert state.state == "on" target_properties["friendly_name"] = entity_name or name target_properties["flowing"] = False target_properties["night_light"] = True assert dict(state.attributes) == target_properties # nightlight if nightlight_properties is None: return name += "_nightlight" entity_id = f"light.{name}" assert hass.states.get(entity_id).state == "off" state = hass.states.get(f"{entity_id}_nightlight") assert state.state == "on" nightlight_properties["friendly_name"] = f"{name} nightlight" nightlight_properties["icon"] = "mdi:weather-night" nightlight_properties["flowing"] = False nightlight_properties["night_light"] = True assert dict(state.attributes) == nightlight_properties bright = round(255 * int(PROPERTIES["bright"]) / 100) current_brightness = round(255 * int(PROPERTIES["current_brightness"]) / 100) ct = color_temperature_kelvin_to_mired(int(PROPERTIES["ct"])) hue = int(PROPERTIES["hue"]) sat = int(PROPERTIES["sat"]) hs_color = (round(hue / 360 * 65536, 3), round(sat / 100 * 255, 3)) rgb_color = color_hs_to_RGB(*hs_color) xy_color = color_hs_to_xy(*hs_color) bg_bright = round(255 * int(PROPERTIES["bg_bright"]) / 100) bg_ct = color_temperature_kelvin_to_mired(int(PROPERTIES["bg_ct"])) bg_rgb = int(PROPERTIES["bg_rgb"]) bg_rgb_color = ((bg_rgb >> 16) & 0xFF, (bg_rgb >> 8) & 0xFF, bg_rgb & 0xFF) bg_hs_color = color_RGB_to_hs(*bg_rgb_color) bg_xy_color = color_RGB_to_xy(*bg_rgb_color) nl_br = round(255 * int(PROPERTIES["nl_br"]) / 100) # Default await _async_test( "default", None, "mono", { "effect_list": YEELIGHT_MONO_EFFECT_LIST, "supported_features": SUPPORT_YEELIGHT, "brightness": bright, }, ) # White await _async_test( "white", BulbType.White, "mono", { "effect_list": YEELIGHT_MONO_EFFECT_LIST, "supported_features": SUPPORT_YEELIGHT, "brightness": bright, }, ) # Color model_specs = _MODEL_SPECS["color"] await _async_test( "color", BulbType.Color, "color", { "effect_list": YEELIGHT_COLOR_EFFECT_LIST, "supported_features": SUPPORT_YEELIGHT_RGB, "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": current_brightness, "color_temp": ct, "hs_color": hs_color, "rgb_color": rgb_color, "xy_color": xy_color, }, {"supported_features": 0}, ) # WhiteTemp model_specs = _MODEL_SPECS["ceiling1"] await _async_test( "white_temp", BulbType.WhiteTemp, "ceiling1", { "effect_list": YEELIGHT_TEMP_ONLY_EFFECT_LIST, "supported_features": SUPPORT_YEELIGHT_WHITE_TEMP, "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": current_brightness, "color_temp": ct, }, { "effect_list": YEELIGHT_TEMP_ONLY_EFFECT_LIST, "supported_features": SUPPORT_YEELIGHT, "brightness": nl_br, }, ) # WhiteTempMood model_specs = _MODEL_SPECS["ceiling4"] await _async_test( "white_temp_mood", BulbType.WhiteTempMood, "ceiling4", { "friendly_name": NAME, "effect_list": YEELIGHT_TEMP_ONLY_EFFECT_LIST, "flowing": False, "night_light": True, "supported_features": SUPPORT_YEELIGHT_WHITE_TEMP, "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": current_brightness, "color_temp": ct, }, { "effect_list": YEELIGHT_TEMP_ONLY_EFFECT_LIST, "supported_features": SUPPORT_YEELIGHT, "brightness": nl_br, }, ) await _async_test( "ambient", BulbType.WhiteTempMood, "ceiling4", { "effect_list": YEELIGHT_COLOR_EFFECT_LIST, "supported_features": SUPPORT_YEELIGHT_RGB, "min_mireds": color_temperature_kelvin_to_mired(6500), "max_mireds": color_temperature_kelvin_to_mired(1700), "brightness": bg_bright, "color_temp": bg_ct, "hs_color": bg_hs_color, "rgb_color": bg_rgb_color, "xy_color": bg_xy_color, }, entity_name="ambient ambilight", entity_id="light.ambient_ambilight", ) async def test_effects(hass: HomeAssistant): """Test effects.""" yaml_configuration = { DOMAIN: { CONF_DEVICES: YAML_CONFIGURATION[DOMAIN][CONF_DEVICES], 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]}, ], }, }, ], } } mocked_bulb = _mocked_bulb() with patch(f"{MODULE}.Bulb", return_value=mocked_bulb): assert await async_setup_component(hass, DOMAIN, yaml_configuration) 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): mocked_start_flow = MagicMock() type(mocked_bulb).start_flow = 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 mocked_start_flow.assert_called_once() if target is None: return args, _ = 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)), } for name, target in effects.items(): await _async_test_effect(name, target) await _async_test_effect("not_existed", called=False)