core/tests/components/yeelight/test_light.py

1657 lines
59 KiB
Python

"""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