Home Assistant Unofficial Reference 2024.12.1
light.py
Go to the documentation of this file.
1 """Light platform support for yeelight."""
2 
3 from __future__ import annotations
4 
5 from collections.abc import Callable, Coroutine
6 import logging
7 import math
8 from typing import Any, Concatenate
9 
10 import voluptuous as vol
11 import yeelight
12 from yeelight import Flow, RGBTransition, SleepTransition, flows
13 from yeelight.aio import AsyncBulb
14 from yeelight.enums import BulbType, LightType, PowerMode, SceneClass
15 from yeelight.main import BulbException
16 
18  ATTR_BRIGHTNESS,
19  ATTR_COLOR_TEMP,
20  ATTR_EFFECT,
21  ATTR_FLASH,
22  ATTR_HS_COLOR,
23  ATTR_KELVIN,
24  ATTR_RGB_COLOR,
25  ATTR_TRANSITION,
26  FLASH_LONG,
27  FLASH_SHORT,
28  ColorMode,
29  LightEntity,
30  LightEntityFeature,
31 )
32 from homeassistant.config_entries import ConfigEntry
33 from homeassistant.const import ATTR_ENTITY_ID, ATTR_MODE, CONF_NAME
34 from homeassistant.core import HomeAssistant, callback
35 from homeassistant.exceptions import HomeAssistantError
36 from homeassistant.helpers import entity_platform
38 from homeassistant.helpers.dispatcher import async_dispatcher_connect
39 from homeassistant.helpers.entity_platform import AddEntitiesCallback
40 from homeassistant.helpers.event import async_call_later
41 from homeassistant.helpers.typing import VolDictType
42 import homeassistant.util.color as color_util
43 from homeassistant.util.color import (
44  color_temperature_kelvin_to_mired as kelvin_to_mired,
45  color_temperature_mired_to_kelvin as mired_to_kelvin,
46 )
47 
48 from . import YEELIGHT_FLOW_TRANSITION_SCHEMA
49 from .const import (
50  ACTION_RECOVER,
51  ATTR_ACTION,
52  ATTR_COUNT,
53  ATTR_MODE_MUSIC,
54  ATTR_TRANSITIONS,
55  CONF_FLOW_PARAMS,
56  CONF_MODE_MUSIC,
57  CONF_NIGHTLIGHT_SWITCH,
58  CONF_SAVE_ON_CHANGE,
59  CONF_TRANSITION,
60  DATA_CONFIG_ENTRIES,
61  DATA_CUSTOM_EFFECTS,
62  DATA_DEVICE,
63  DATA_UPDATED,
64  DOMAIN,
65  MODELS_WITH_DELAYED_ON_TRANSITION,
66  POWER_STATE_CHANGE_TIME,
67 )
68 from .device import YeelightDevice
69 from .entity import YeelightEntity
70 
71 _LOGGER = logging.getLogger(__name__)
72 
73 ATTR_MINUTES = "minutes"
74 
75 SERVICE_SET_MODE = "set_mode"
76 SERVICE_SET_MUSIC_MODE = "set_music_mode"
77 SERVICE_START_FLOW = "start_flow"
78 SERVICE_SET_COLOR_SCENE = "set_color_scene"
79 SERVICE_SET_HSV_SCENE = "set_hsv_scene"
80 SERVICE_SET_COLOR_TEMP_SCENE = "set_color_temp_scene"
81 SERVICE_SET_COLOR_FLOW_SCENE = "set_color_flow_scene"
82 SERVICE_SET_AUTO_DELAY_OFF_SCENE = "set_auto_delay_off_scene"
83 
84 EFFECT_DISCO = "Disco"
85 EFFECT_TEMP = "Slow Temp"
86 EFFECT_STROBE = "Strobe epilepsy!"
87 EFFECT_STROBE_COLOR = "Strobe color"
88 EFFECT_ALARM = "Alarm"
89 EFFECT_POLICE = "Police"
90 EFFECT_POLICE2 = "Police2"
91 EFFECT_CHRISTMAS = "Christmas"
92 EFFECT_RGB = "RGB"
93 EFFECT_RANDOM_LOOP = "Random Loop"
94 EFFECT_FAST_RANDOM_LOOP = "Fast Random Loop"
95 EFFECT_LSD = "LSD"
96 EFFECT_SLOWDOWN = "Slowdown"
97 EFFECT_WHATSAPP = "WhatsApp"
98 EFFECT_FACEBOOK = "Facebook"
99 EFFECT_TWITTER = "Twitter"
100 EFFECT_STOP = "Stop"
101 EFFECT_HOME = "Home"
102 EFFECT_NIGHT_MODE = "Night Mode"
103 EFFECT_DATE_NIGHT = "Date Night"
104 EFFECT_MOVIE = "Movie"
105 EFFECT_SUNRISE = "Sunrise"
106 EFFECT_SUNSET = "Sunset"
107 EFFECT_ROMANCE = "Romance"
108 EFFECT_HAPPY_BIRTHDAY = "Happy Birthday"
109 EFFECT_CANDLE_FLICKER = "Candle Flicker"
110 EFFECT_TEA_TIME = "Tea Time"
111 
112 YEELIGHT_TEMP_ONLY_EFFECT_LIST = [EFFECT_TEMP, EFFECT_STOP]
113 
114 YEELIGHT_MONO_EFFECT_LIST = [
115  EFFECT_DISCO,
116  EFFECT_STROBE,
117  EFFECT_ALARM,
118  EFFECT_POLICE2,
119  EFFECT_WHATSAPP,
120  EFFECT_FACEBOOK,
121  EFFECT_TWITTER,
122  EFFECT_HOME,
123  EFFECT_CANDLE_FLICKER,
124  EFFECT_TEA_TIME,
125  *YEELIGHT_TEMP_ONLY_EFFECT_LIST,
126 ]
127 
128 YEELIGHT_COLOR_EFFECT_LIST = [
129  EFFECT_STROBE_COLOR,
130  EFFECT_POLICE,
131  EFFECT_CHRISTMAS,
132  EFFECT_RGB,
133  EFFECT_RANDOM_LOOP,
134  EFFECT_FAST_RANDOM_LOOP,
135  EFFECT_LSD,
136  EFFECT_SLOWDOWN,
137  EFFECT_NIGHT_MODE,
138  EFFECT_DATE_NIGHT,
139  EFFECT_MOVIE,
140  EFFECT_SUNRISE,
141  EFFECT_SUNSET,
142  EFFECT_ROMANCE,
143  EFFECT_HAPPY_BIRTHDAY,
144  *YEELIGHT_MONO_EFFECT_LIST,
145 ]
146 
147 EFFECTS_MAP = {
148  EFFECT_DISCO: flows.disco,
149  EFFECT_TEMP: flows.temp,
150  EFFECT_STROBE: flows.strobe,
151  EFFECT_STROBE_COLOR: flows.strobe_color,
152  EFFECT_ALARM: flows.alarm,
153  EFFECT_POLICE: flows.police,
154  EFFECT_POLICE2: flows.police2,
155  EFFECT_CHRISTMAS: flows.christmas,
156  EFFECT_RGB: flows.rgb,
157  EFFECT_RANDOM_LOOP: flows.random_loop,
158  EFFECT_LSD: flows.lsd,
159  EFFECT_SLOWDOWN: flows.slowdown,
160  EFFECT_HOME: flows.home,
161  EFFECT_NIGHT_MODE: flows.night_mode,
162  EFFECT_DATE_NIGHT: flows.date_night,
163  EFFECT_MOVIE: flows.movie,
164  EFFECT_SUNRISE: flows.sunrise,
165  EFFECT_SUNSET: flows.sunset,
166  EFFECT_ROMANCE: flows.romance,
167  EFFECT_HAPPY_BIRTHDAY: flows.happy_birthday,
168  EFFECT_CANDLE_FLICKER: flows.candle_flicker,
169  EFFECT_TEA_TIME: flows.tea_time,
170 }
171 
172 VALID_BRIGHTNESS = vol.All(vol.Coerce(int), vol.Range(min=1, max=100))
173 
174 SERVICE_SCHEMA_SET_MODE: VolDictType = {
175  vol.Required(ATTR_MODE): vol.In([mode.name.lower() for mode in PowerMode])
176 }
177 
178 SERVICE_SCHEMA_SET_MUSIC_MODE: VolDictType = {vol.Required(ATTR_MODE_MUSIC): cv.boolean}
179 
180 SERVICE_SCHEMA_START_FLOW = YEELIGHT_FLOW_TRANSITION_SCHEMA
181 
182 SERVICE_SCHEMA_SET_COLOR_SCENE: VolDictType = {
183  vol.Required(ATTR_RGB_COLOR): vol.All(
184  vol.Coerce(tuple), vol.ExactSequence((cv.byte, cv.byte, cv.byte))
185  ),
186  vol.Required(ATTR_BRIGHTNESS): VALID_BRIGHTNESS,
187 }
188 
189 SERVICE_SCHEMA_SET_HSV_SCENE: VolDictType = {
190  vol.Required(ATTR_HS_COLOR): vol.All(
191  vol.Coerce(tuple),
192  vol.ExactSequence(
193  (
194  vol.All(vol.Coerce(float), vol.Range(min=0, max=359)),
195  vol.All(vol.Coerce(float), vol.Range(min=0, max=100)),
196  )
197  ),
198  ),
199  vol.Required(ATTR_BRIGHTNESS): VALID_BRIGHTNESS,
200 }
201 
202 SERVICE_SCHEMA_SET_COLOR_TEMP_SCENE: VolDictType = {
203  vol.Required(ATTR_KELVIN): vol.All(vol.Coerce(int), vol.Range(min=1700, max=6500)),
204  vol.Required(ATTR_BRIGHTNESS): VALID_BRIGHTNESS,
205 }
206 
207 SERVICE_SCHEMA_SET_COLOR_FLOW_SCENE = YEELIGHT_FLOW_TRANSITION_SCHEMA
208 
209 SERVICE_SCHEMA_SET_AUTO_DELAY_OFF_SCENE: VolDictType = {
210  vol.Required(ATTR_MINUTES): vol.All(vol.Coerce(int), vol.Range(min=1, max=60)),
211  vol.Required(ATTR_BRIGHTNESS): VALID_BRIGHTNESS,
212 }
213 
214 
215 @callback
216 def _transitions_config_parser(transitions):
217  """Parse transitions config into initialized objects."""
218  transition_objects = []
219  for transition_config in transitions:
220  transition, params = list(transition_config.items())[0]
221  transition_objects.append(getattr(yeelight, transition)(*params))
222 
223  return transition_objects
224 
225 
226 @callback
227 def _parse_custom_effects(effects_config) -> dict[str, dict[str, Any]]:
228  effects = {}
229  for config in effects_config:
230  params = config[CONF_FLOW_PARAMS]
231  action = Flow.actions[params[ATTR_ACTION]]
232  transitions = _transitions_config_parser(params[ATTR_TRANSITIONS])
233 
234  effects[config[CONF_NAME]] = {
235  ATTR_COUNT: params[ATTR_COUNT],
236  ATTR_ACTION: action,
237  ATTR_TRANSITIONS: transitions,
238  }
239 
240  return effects
241 
242 
243 def _async_cmd[_YeelightBaseLightT: YeelightBaseLight, **_P, _R](
244  func: Callable[Concatenate[_YeelightBaseLightT, _P], Coroutine[Any, Any, _R]],
245 ) -> Callable[Concatenate[_YeelightBaseLightT, _P], Coroutine[Any, Any, _R | None]]:
246  """Define a wrapper to catch exceptions from the bulb."""
247 
248  async def _async_wrap(
249  self: _YeelightBaseLightT, *args: _P.args, **kwargs: _P.kwargs
250  ) -> _R | None:
251  for attempts in range(2):
252  try:
253  _LOGGER.debug("Calling %s with %s %s", func, args, kwargs)
254  return await func(self, *args, **kwargs)
255  except TimeoutError as ex:
256  # The wifi likely dropped, so we want to retry once since
257  # python-yeelight will auto reconnect
258  if attempts == 0:
259  continue
260  raise HomeAssistantError(
261  f"Timed out when calling {func.__name__} for bulb "
262  f"{self.device.name} at {self.device.host}: {str(ex) or type(ex)}"
263  ) from ex
264  except OSError as ex:
265  # A network error happened, the bulb is likely offline now
266  self.device.async_mark_unavailable()
267  self.async_state_changed()
268  raise HomeAssistantError(
269  f"Error when calling {func.__name__} for bulb "
270  f"{self.device.name} at {self.device.host}: {str(ex) or type(ex)}"
271  ) from ex
272  except BulbException as ex:
273  # The bulb likely responded but had an error
274  raise HomeAssistantError(
275  f"Error when calling {func.__name__} for bulb "
276  f"{self.device.name} at {self.device.host}: {str(ex) or type(ex)}"
277  ) from ex
278  return None
279 
280  return _async_wrap
281 
282 
284  hass: HomeAssistant,
285  config_entry: ConfigEntry,
286  async_add_entities: AddEntitiesCallback,
287 ) -> None:
288  """Set up Yeelight from a config entry."""
289  custom_effects = _parse_custom_effects(hass.data[DOMAIN][DATA_CUSTOM_EFFECTS])
290 
291  device = hass.data[DOMAIN][DATA_CONFIG_ENTRIES][config_entry.entry_id][DATA_DEVICE]
292  _LOGGER.debug("Adding %s", device.name)
293 
294  nl_switch_light = device.config.get(CONF_NIGHTLIGHT_SWITCH)
295 
296  lights = []
297 
298  device_type = device.type
299 
300  def _lights_setup_helper(klass):
301  lights.append(klass(device, config_entry, custom_effects=custom_effects))
302 
303  if device_type == BulbType.White:
304  _lights_setup_helper(YeelightGenericLight)
305  elif device_type == BulbType.Color:
306  if nl_switch_light and device.is_nightlight_supported:
307  _lights_setup_helper(YeelightColorLightWithNightlightSwitch)
308  _lights_setup_helper(YeelightNightLightModeWithoutBrightnessControl)
309  else:
310  _lights_setup_helper(YeelightColorLightWithoutNightlightSwitchLight)
311  elif device_type == BulbType.WhiteTemp:
312  if nl_switch_light and device.is_nightlight_supported:
313  _lights_setup_helper(YeelightWithNightLight)
314  _lights_setup_helper(YeelightNightLightMode)
315  else:
316  _lights_setup_helper(YeelightWhiteTempWithoutNightlightSwitch)
317  elif device_type == BulbType.WhiteTempMood:
318  if nl_switch_light and device.is_nightlight_supported:
319  _lights_setup_helper(YeelightNightLightModeWithAmbientSupport)
320  _lights_setup_helper(YeelightWithAmbientAndNightlight)
321  else:
322  _lights_setup_helper(YeelightWithAmbientWithoutNightlight)
323  _lights_setup_helper(YeelightAmbientLight)
324  else:
325  _lights_setup_helper(YeelightGenericLight)
326  _LOGGER.warning(
327  "Cannot determine device type for %s, %s. Falling back to white only",
328  device.host,
329  device.name,
330  )
331 
332  async_add_entities(lights)
334 
335 
336 @callback
337 def _async_setup_services(hass: HomeAssistant):
338  """Set up custom services."""
339 
340  async def _async_start_flow(entity, service_call):
341  params = {**service_call.data}
342  params.pop(ATTR_ENTITY_ID)
343  params[ATTR_TRANSITIONS] = _transitions_config_parser(params[ATTR_TRANSITIONS])
344  await entity.async_start_flow(**params)
345 
346  async def _async_set_color_scene(entity, service_call):
347  await entity.async_set_scene(
348  SceneClass.COLOR,
349  *service_call.data[ATTR_RGB_COLOR],
350  service_call.data[ATTR_BRIGHTNESS],
351  )
352 
353  async def _async_set_hsv_scene(entity, service_call):
354  await entity.async_set_scene(
355  SceneClass.HSV,
356  *service_call.data[ATTR_HS_COLOR],
357  service_call.data[ATTR_BRIGHTNESS],
358  )
359 
360  async def _async_set_color_temp_scene(entity, service_call):
361  await entity.async_set_scene(
362  SceneClass.CT,
363  service_call.data[ATTR_KELVIN],
364  service_call.data[ATTR_BRIGHTNESS],
365  )
366 
367  async def _async_set_color_flow_scene(entity, service_call):
368  flow = Flow(
369  count=service_call.data[ATTR_COUNT],
370  action=Flow.actions[service_call.data[ATTR_ACTION]],
371  transitions=_transitions_config_parser(service_call.data[ATTR_TRANSITIONS]),
372  )
373  await entity.async_set_scene(SceneClass.CF, flow)
374 
375  async def _async_set_auto_delay_off_scene(entity, service_call):
376  await entity.async_set_scene(
377  SceneClass.AUTO_DELAY_OFF,
378  service_call.data[ATTR_BRIGHTNESS],
379  service_call.data[ATTR_MINUTES],
380  )
381 
382  platform = entity_platform.async_get_current_platform()
383 
384  platform.async_register_entity_service(
385  SERVICE_SET_MODE, SERVICE_SCHEMA_SET_MODE, "async_set_mode"
386  )
387  platform.async_register_entity_service(
388  SERVICE_START_FLOW, SERVICE_SCHEMA_START_FLOW, _async_start_flow
389  )
390  platform.async_register_entity_service(
391  SERVICE_SET_COLOR_SCENE, SERVICE_SCHEMA_SET_COLOR_SCENE, _async_set_color_scene
392  )
393  platform.async_register_entity_service(
394  SERVICE_SET_HSV_SCENE, SERVICE_SCHEMA_SET_HSV_SCENE, _async_set_hsv_scene
395  )
396  platform.async_register_entity_service(
397  SERVICE_SET_COLOR_TEMP_SCENE,
398  SERVICE_SCHEMA_SET_COLOR_TEMP_SCENE,
399  _async_set_color_temp_scene,
400  )
401  platform.async_register_entity_service(
402  SERVICE_SET_COLOR_FLOW_SCENE,
403  SERVICE_SCHEMA_SET_COLOR_FLOW_SCENE,
404  _async_set_color_flow_scene,
405  )
406  platform.async_register_entity_service(
407  SERVICE_SET_AUTO_DELAY_OFF_SCENE,
408  SERVICE_SCHEMA_SET_AUTO_DELAY_OFF_SCENE,
409  _async_set_auto_delay_off_scene,
410  )
411  platform.async_register_entity_service(
412  SERVICE_SET_MUSIC_MODE, SERVICE_SCHEMA_SET_MUSIC_MODE, "async_set_music_mode"
413  )
414 
415 
417  """Abstract Yeelight light."""
418 
419  _attr_color_mode = ColorMode.BRIGHTNESS
420  _attr_supported_color_modes = {ColorMode.BRIGHTNESS}
421  _attr_supported_features = (
422  LightEntityFeature.TRANSITION
423  | LightEntityFeature.FLASH
424  | LightEntityFeature.EFFECT
425  )
426  _attr_should_poll = False
427 
428  def __init__(
429  self,
430  device: YeelightDevice,
431  entry: ConfigEntry,
432  custom_effects: dict[str, dict[str, Any]] | None = None,
433  ) -> None:
434  """Initialize the Yeelight light."""
435  super().__init__(device, entry)
436 
437  self.configconfig = device.config
438 
439  self._color_temp_color_temp: int | None = None
440  self._effect_effect = None
441 
442  model_specs = self._bulb_bulb.get_model_specs()
443  self._attr_min_mireds_attr_min_mireds = kelvin_to_mired(model_specs["color_temp"]["max"])
444  self._attr_max_mireds_attr_max_mireds = kelvin_to_mired(model_specs["color_temp"]["min"])
445 
446  self._light_type_light_type = LightType.Main
447 
448  if custom_effects:
449  self._custom_effects_custom_effects = custom_effects
450  else:
451  self._custom_effects_custom_effects = {}
452 
453  self._unexpected_state_check_unexpected_state_check = None
454 
455  @callback
456  def async_state_changed(self) -> None:
457  """Call when the device changes state."""
458  if not self._device_device.available:
459  self._async_cancel_pending_state_check_async_cancel_pending_state_check()
460  self.async_write_ha_stateasync_write_ha_state()
461 
462  async def async_added_to_hass(self) -> None:
463  """Handle entity which will be added."""
464  self.async_on_removeasync_on_remove(
466  self.hasshass,
467  DATA_UPDATED.format(self._device_device.host),
468  self.async_state_changedasync_state_changed,
469  )
470  )
471  await super().async_added_to_hass()
472 
473  @property
474  def effect_list(self) -> list[str]:
475  """Return the list of supported effects."""
476  return self._predefined_effects_predefined_effects + self.custom_effects_namescustom_effects_names
477 
478  @property
479  def color_temp(self) -> int | None:
480  """Return the color temperature."""
481  if temp_in_k := self._get_property_get_property("ct"):
482  self._color_temp_color_temp = kelvin_to_mired(int(temp_in_k))
483  return self._color_temp_color_temp
484 
485  @property
486  def is_on(self) -> bool:
487  """Return true if device is on."""
488  return self._get_property_get_property(self._power_property_power_property) == "on"
489 
490  @property
491  def brightness(self) -> int:
492  """Return the brightness of this light between 1..255."""
493  # Always use "bright" as property name in music mode
494  # Since music mode states are only caches in upstream library
495  # and the cache key is always "bright" for brightness
496  brightness_property = (
497  "bright" if self._bulb_bulb.music_mode else self._brightness_property_brightness_property
498  )
499  brightness = self._get_property_get_property(brightness_property) or 0
500  return round(255 * (int(brightness) / 100))
501 
502  @property
503  def custom_effects(self) -> dict[str, dict[str, Any]]:
504  """Return dict with custom effects."""
505  return self._custom_effects_custom_effects
506 
507  @property
508  def custom_effects_names(self) -> list[str]:
509  """Return list with custom effects names."""
510  return list(self.custom_effectscustom_effects)
511 
512  @property
513  def light_type(self) -> LightType:
514  """Return light type."""
515  return self._light_type_light_type
516 
517  @property
518  def hs_color(self) -> tuple[float, float] | None:
519  """Return the color property."""
520  hue = self._get_property_get_property("hue")
521  sat = self._get_property_get_property("sat")
522  if hue is None or sat is None:
523  return None
524 
525  return (int(hue), int(sat))
526 
527  @property
528  def rgb_color(self) -> tuple[int, int, int] | None:
529  """Return the color property."""
530  if (rgb := self._get_property_get_property("rgb")) is None:
531  return None
532 
533  rgb = int(rgb)
534  blue = rgb & 0xFF
535  green = (rgb >> 8) & 0xFF
536  red = (rgb >> 16) & 0xFF
537 
538  return (red, green, blue)
539 
540  @property
541  def effect(self) -> str | None:
542  """Return the current effect."""
543  return self._effect_effect if self.devicedevice.is_color_flow_enabled else None
544 
545  @property
546  def _bulb(self) -> AsyncBulb:
547  return self.devicedevice.bulb
548 
549  @property
550  def _properties(self) -> dict:
551  return self._bulb_bulb.last_properties if self._bulb_bulb else {}
552 
553  def _get_property(self, prop: str, default=None):
554  return self._properties_properties.get(prop, default)
555 
556  @property
557  def _brightness_property(self) -> str:
558  return "bright"
559 
560  @property
561  def _power_property(self) -> str:
562  return "power"
563 
564  @property
565  def _turn_on_power_mode(self) -> PowerMode:
566  return PowerMode.LAST
567 
568  @property
569  def _predefined_effects(self) -> list[str]:
570  return YEELIGHT_MONO_EFFECT_LIST
571 
572  @property
573  def extra_state_attributes(self) -> dict[str, Any]:
574  """Return the device specific state attributes."""
575  attributes = {
576  "flowing": self.devicedevice.is_color_flow_enabled,
577  "music_mode": self._bulb_bulb.music_mode,
578  }
579 
580  if self.devicedevice.is_nightlight_supported:
581  attributes["night_light"] = self.devicedevice.is_nightlight_enabled
582 
583  return attributes
584 
585  @property
586  def device(self) -> YeelightDevice:
587  """Return yeelight device."""
588  return self._device_device
589 
590  async def async_update(self) -> None:
591  """Update light properties."""
592  await self.devicedevice.async_update(True)
593 
594  async def async_set_music_mode(self, music_mode) -> None:
595  """Set the music mode on or off."""
596  try:
597  await self._async_set_music_mode_async_set_music_mode(music_mode)
598  except AssertionError as ex:
599  _LOGGER.error("Unable to turn on music mode, consider disabling it: %s", ex)
600 
601  @_async_cmd
602  async def _async_set_music_mode(self, music_mode) -> None:
603  """Set the music mode on or off wrapped with _async_cmd."""
604  bulb = self._bulb_bulb
605  if music_mode:
606  await bulb.async_start_music()
607  else:
608  await bulb.async_stop_music()
609 
610  @_async_cmd
611  async def async_set_brightness(self, brightness, duration) -> None:
612  """Set bulb brightness."""
613  if not brightness:
614  return
615  if (
616  math.floor(self.brightnessbrightnessbrightness) == math.floor(brightness)
617  and self._bulb_bulb.model not in MODELS_WITH_DELAYED_ON_TRANSITION
618  ):
619  _LOGGER.debug("brightness already set to: %s", brightness)
620  # Already set, and since we get pushed updates
621  # we avoid setting it again to ensure we do not
622  # hit the rate limit
623  return
624 
625  _LOGGER.debug("Setting brightness: %s", brightness)
626  await self._bulb_bulb.async_set_brightness(
627  brightness / 255 * 100, duration=duration, light_type=self.light_typelight_type
628  )
629 
630  @_async_cmd
631  async def async_set_hs(self, hs_color, duration) -> None:
632  """Set bulb's color."""
633  if (
634  not hs_color
635  or not self.supported_color_modessupported_color_modes
636  or ColorMode.HS not in self.supported_color_modessupported_color_modes
637  ):
638  return
639  if (
640  not self.devicedevice.is_color_flow_enabled
641  and self.color_modecolor_modecolor_mode == ColorMode.HS
642  and self.hs_colorhs_colorhs_colorhs_color == hs_color
643  ):
644  _LOGGER.debug("HS already set to: %s", hs_color)
645  # Already set, and since we get pushed updates
646  # we avoid setting it again to ensure we do not
647  # hit the rate limit
648  return
649 
650  _LOGGER.debug("Setting HS: %s", hs_color)
651  await self._bulb_bulb.async_set_hsv(
652  hs_color[0], hs_color[1], duration=duration, light_type=self.light_typelight_type
653  )
654 
655  @_async_cmd
656  async def async_set_rgb(self, rgb, duration) -> None:
657  """Set bulb's color."""
658  if (
659  not rgb
660  or not self.supported_color_modessupported_color_modes
661  or ColorMode.RGB not in self.supported_color_modessupported_color_modes
662  ):
663  return
664  if (
665  not self.devicedevice.is_color_flow_enabled
666  and self.color_modecolor_modecolor_mode == ColorMode.RGB
667  and self.rgb_colorrgb_colorrgb_colorrgb_color == rgb
668  ):
669  _LOGGER.debug("RGB already set to: %s", rgb)
670  # Already set, and since we get pushed updates
671  # we avoid setting it again to ensure we do not
672  # hit the rate limit
673  return
674 
675  _LOGGER.debug("Setting RGB: %s", rgb)
676  await self._bulb_bulb.async_set_rgb(
677  *rgb, duration=duration, light_type=self.light_typelight_type
678  )
679 
680  @_async_cmd
681  async def async_set_colortemp(self, colortemp, duration) -> None:
682  """Set bulb's color temperature."""
683  if (
684  not colortemp
685  or not self.supported_color_modessupported_color_modes
686  or ColorMode.COLOR_TEMP not in self.supported_color_modessupported_color_modes
687  ):
688  return
689  temp_in_k = mired_to_kelvin(colortemp)
690 
691  if (
692  not self.devicedevice.is_color_flow_enabled
693  and self.color_modecolor_modecolor_mode == ColorMode.COLOR_TEMP
694  and self.color_tempcolor_tempcolor_tempcolor_temp == colortemp
695  ):
696  _LOGGER.debug("Color temp already set to: %s", temp_in_k)
697  # Already set, and since we get pushed updates
698  # we avoid setting it again to ensure we do not
699  # hit the rate limit
700  return
701 
702  await self._bulb_bulb.async_set_color_temp(
703  temp_in_k, duration=duration, light_type=self.light_typelight_type
704  )
705 
706  @_async_cmd
707  async def async_set_default(self) -> None:
708  """Set current options as default."""
709  await self._bulb_bulb.async_set_default()
710 
711  @_async_cmd
712  async def async_set_flash(self, flash) -> None:
713  """Activate flash."""
714  if not flash:
715  return
716  if int(self._get_property_get_property("color_mode")) != 1 or not self.hs_colorhs_colorhs_colorhs_color:
717  _LOGGER.error("Flash supported currently only in RGB mode")
718  return
719 
720  transition = int(self.configconfig[CONF_TRANSITION])
721  if flash == FLASH_LONG:
722  count = 1
723  duration = transition * 5
724  if flash == FLASH_SHORT:
725  count = 1
726  duration = transition * 2
727 
728  red, green, blue = color_util.color_hs_to_RGB(*self.hs_colorhs_colorhs_colorhs_color)
729 
730  transitions = []
731  transitions.append(RGBTransition(255, 0, 0, brightness=10, duration=duration))
732  transitions.append(SleepTransition(duration=transition))
733  transitions.append(
734  RGBTransition(
735  red, green, blue, brightness=self.brightnessbrightnessbrightness, duration=duration
736  )
737  )
738 
739  flow = Flow(count=count, transitions=transitions)
740  await self._bulb_bulb.async_start_flow(flow, light_type=self.light_typelight_type)
741 
742  @_async_cmd
743  async def async_set_effect(self, effect) -> None:
744  """Activate effect."""
745  if not effect:
746  return
747 
748  if effect == EFFECT_STOP:
749  await self._bulb_bulb.async_stop_flow(light_type=self.light_typelight_type)
750  return
751 
752  if effect in self.custom_effects_namescustom_effects_names:
753  flow = Flow(**self.custom_effectscustom_effects[effect])
754  elif effect in EFFECTS_MAP:
755  flow = EFFECTS_MAP[effect]()
756  elif effect == EFFECT_FAST_RANDOM_LOOP:
757  flow = flows.random_loop(duration=250)
758  elif effect == EFFECT_WHATSAPP:
759  flow = flows.pulse(37, 211, 102, count=2)
760  elif effect == EFFECT_FACEBOOK:
761  flow = flows.pulse(59, 89, 152, count=2)
762  elif effect == EFFECT_TWITTER:
763  flow = flows.pulse(0, 172, 237, count=2)
764  else:
765  return
766 
767  await self._bulb_bulb.async_start_flow(flow, light_type=self.light_typelight_type)
768  self._effect_effect = effect
769 
770  @_async_cmd
771  async def _async_turn_on(self, duration) -> None:
772  """Turn on the bulb for with a transition duration wrapped with _async_cmd."""
773  await self._bulb_bulb.async_turn_on(
774  duration=duration,
775  light_type=self.light_typelight_type,
776  power_mode=self._turn_on_power_mode_turn_on_power_mode,
777  )
778 
779  async def async_turn_on(self, **kwargs: Any) -> None:
780  """Turn the bulb on."""
781  brightness = kwargs.get(ATTR_BRIGHTNESS)
782  colortemp = kwargs.get(ATTR_COLOR_TEMP)
783  hs_color = kwargs.get(ATTR_HS_COLOR)
784  rgb = kwargs.get(ATTR_RGB_COLOR)
785  flash = kwargs.get(ATTR_FLASH)
786  effect = kwargs.get(ATTR_EFFECT)
787 
788  duration = int(self.configconfig[CONF_TRANSITION]) # in ms
789  if ATTR_TRANSITION in kwargs: # passed kwarg overrides config
790  duration = int(kwargs[ATTR_TRANSITION] * 1000) # kwarg in s
791 
792  if not self.is_onis_onis_on:
793  await self._async_turn_on_async_turn_on(duration)
794 
795  if self.configconfig[CONF_MODE_MUSIC] and not self._bulb_bulb.music_mode:
796  await self.async_set_music_modeasync_set_music_mode(True)
797 
798  await self.async_set_hsasync_set_hs(hs_color, duration)
799  await self.async_set_rgbasync_set_rgb(rgb, duration)
800  await self.async_set_colortempasync_set_colortemp(colortemp, duration)
801  await self.async_set_brightnessasync_set_brightness(brightness, duration)
802  await self.async_set_flashasync_set_flash(flash)
803  await self.async_set_effectasync_set_effect(effect)
804 
805  # save the current state if we had a manual change.
806  if self.configconfig[CONF_SAVE_ON_CHANGE] and (brightness or colortemp or rgb):
807  await self.async_set_defaultasync_set_default()
808 
809  self._async_schedule_state_check_async_schedule_state_check(True)
810 
811  @callback
813  """Cancel a pending state check."""
814  if self._unexpected_state_check_unexpected_state_check:
815  self._unexpected_state_check_unexpected_state_check()
816  self._unexpected_state_check_unexpected_state_check = None
817 
818  @callback
819  def _async_schedule_state_check(self, expected_power_state):
820  """Schedule a poll if the change failed to get pushed back to us.
821 
822  Some devices (mainly nightlights) will not send back the on state
823  so we need to force a refresh.
824  """
825  self._async_cancel_pending_state_check_async_cancel_pending_state_check()
826 
827  async def _async_update_if_state_unexpected(*_):
828  self._unexpected_state_check_unexpected_state_check = None
829  if self.is_onis_onis_on != expected_power_state:
830  await self.devicedevice.async_update(True)
831 
832  self._unexpected_state_check_unexpected_state_check = async_call_later(
833  self.hasshass, POWER_STATE_CHANGE_TIME, _async_update_if_state_unexpected
834  )
835 
836  @_async_cmd
837  async def _async_turn_off(self, duration) -> None:
838  """Turn off with a given transition duration wrapped with _async_cmd."""
839  await self._bulb_bulb.async_turn_off(duration=duration, light_type=self.light_typelight_type)
840 
841  async def async_turn_off(self, **kwargs: Any) -> None:
842  """Turn off."""
843  if not self.is_onis_onis_on:
844  return
845 
846  duration = int(self.configconfig[CONF_TRANSITION]) # in ms
847  if ATTR_TRANSITION in kwargs: # passed kwarg overrides config
848  duration = int(kwargs[ATTR_TRANSITION] * 1000) # kwarg in s
849 
850  await self._async_turn_off_async_turn_off(duration)
851  self._async_schedule_state_check_async_schedule_state_check(False)
852 
853  @_async_cmd
854  async def async_set_mode(self, mode: str):
855  """Set a power mode."""
856  await self._bulb_bulb.async_set_power_mode(PowerMode[mode.upper()])
857  self._async_schedule_state_check_async_schedule_state_check(True)
858 
859  @_async_cmd
860  async def async_start_flow(self, transitions, count=0, action=ACTION_RECOVER):
861  """Start flow."""
862  flow = Flow(count=count, action=Flow.actions[action], transitions=transitions)
863  await self._bulb_bulb.async_start_flow(flow, light_type=self.light_typelight_type)
864 
865  @_async_cmd
866  async def async_set_scene(self, scene_class, *args):
867  """Set the light directly to the specified state.
868 
869  If the light is off, it will first be turned on.
870  """
871  await self._bulb_bulb.async_set_scene(scene_class, *args)
872 
873 
875  """Representation of a generic Yeelight."""
876 
877  _attr_name = None
878 
879 
881  """Representation of a Color Yeelight light support."""
882 
883  _attr_supported_color_modes = {ColorMode.COLOR_TEMP, ColorMode.HS, ColorMode.RGB}
884 
885  @property
886  def color_mode(self) -> ColorMode:
887  """Return the color mode."""
888  color_mode = int(self._get_property_get_property("color_mode"))
889  if color_mode == 1: # RGB
890  return ColorMode.RGB
891  if color_mode == 2: # color temperature
892  return ColorMode.COLOR_TEMP
893  if color_mode == 3: # hsv
894  return ColorMode.HS
895  _LOGGER.debug("Light reported unknown color mode: %s", color_mode)
896  return ColorMode.UNKNOWN
897 
898  @property
899  def _predefined_effects(self) -> list[str]:
900  return YEELIGHT_COLOR_EFFECT_LIST
901 
902 
904  """Representation of a White temp Yeelight light."""
905 
906  _attr_name = None
907  _attr_color_mode = ColorMode.COLOR_TEMP
908  _attr_supported_color_modes = {ColorMode.COLOR_TEMP}
909 
910  @property
911  def _predefined_effects(self) -> list[str]:
912  return YEELIGHT_TEMP_ONLY_EFFECT_LIST
913 
914 
916  """Representation of a Yeelight nightlight support."""
917 
918  @property
919  def _turn_on_power_mode(self) -> PowerMode:
920  return PowerMode.NORMAL
921 
922 
924  """A mix-in for yeelights without a nightlight switch."""
925 
926  @property
927  def _brightness_property(self) -> str:
928  # If the nightlight is not active, we do not
929  # want to "current_brightness" since it will check
930  # "bg_power" and main light could still be on
931  if self.devicedevice.is_nightlight_enabled:
932  return "nl_br"
933  return super()._brightness_property
934 
935  @property
936  def color_temp(self) -> int | None:
937  """Return the color temperature."""
938  if self.devicedevice.is_nightlight_enabled:
939  # Enabling the nightlight locks the colortemp to max
940  return self.max_miredsmax_mireds
941  return super().color_temp
942 
943 
945  YeelightColorLightSupport, YeelightWithoutNightlightSwitchMixIn
946 ):
947  """Representation of a Color Yeelight light."""
948 
949 
951  YeelightColorLightWithoutNightlightSwitch
952 ):
953  """Representation of a Color Yeelight light."""
954 
955  _attr_name = None
956 
957 
959  YeelightNightLightSupport, YeelightColorLightSupport, YeelightBaseLight
960 ):
961  """Representation of a Yeelight with rgb support and nightlight.
962 
963  It represents case when nightlight switch is set to light.
964  """
965 
966  _attr_name = None
967 
968  @property
969  def is_on(self) -> bool:
970  """Return true if device is on."""
971  return super().is_on and not self.devicedevice.is_nightlight_enabled
972 
973 
975  YeelightWhiteTempLightSupport, YeelightWithoutNightlightSwitchMixIn
976 ):
977  """White temp light, when nightlight switch is not set to light."""
978 
979  _attr_name = None
980 
981 
983  YeelightNightLightSupport, YeelightWhiteTempLightSupport, YeelightBaseLight
984 ):
985  """Representation of a Yeelight with temp only support and nightlight.
986 
987  It represents case when nightlight switch is set to light.
988  """
989 
990  _attr_name = None
991 
992  @property
993  def is_on(self) -> bool:
994  """Return true if device is on."""
995  return super().is_on and not self.devicedevice.is_nightlight_enabled
996 
997 
999  """Representation of a Yeelight when in nightlight mode."""
1000 
1001  _attr_color_mode = ColorMode.BRIGHTNESS
1002  _attr_supported_color_modes = {ColorMode.BRIGHTNESS}
1003  _attr_translation_key = "nightlight"
1004 
1005  @property
1006  def unique_id(self) -> str:
1007  """Return a unique ID."""
1008  unique = super().unique_id
1009  return f"{unique}-nightlight"
1010 
1011  @property
1012  def is_on(self) -> bool:
1013  """Return true if device is on."""
1014  return super().is_on and self.devicedevice.is_nightlight_enabled
1015 
1016  @property
1017  def _brightness_property(self) -> str:
1018  return "nl_br"
1019 
1020  @property
1021  def _turn_on_power_mode(self) -> PowerMode:
1022  return PowerMode.MOONLIGHT
1023 
1024  @property
1025  def supported_features(self) -> LightEntityFeature:
1026  """Flag no supported features."""
1027  return LightEntityFeature(0)
1028 
1029 
1031  """Representation of a Yeelight, with ambient support, when in nightlight mode."""
1032 
1033  @property
1034  def _power_property(self) -> str:
1035  return "main_power"
1036 
1037 
1039  """Representation of a Yeelight, when in nightlight mode.
1040 
1041  It represents case when nightlight mode brightness control is not supported.
1042  """
1043 
1044  _attr_color_mode = ColorMode.ONOFF
1045  _attr_supported_color_modes = {ColorMode.ONOFF}
1046 
1047 
1049  """Representation of a Yeelight which has ambilight support.
1050 
1051  And nightlight switch type is none.
1052  """
1053 
1054  _attr_name = None
1055 
1056  @property
1057  def _power_property(self) -> str:
1058  return "main_power"
1059 
1060 
1062  """Representation of a Yeelight which has ambilight support.
1063 
1064  And nightlight switch type is set to light.
1065  """
1066 
1067  _attr_name = None
1068 
1069  @property
1070  def _power_property(self) -> str:
1071  return "main_power"
1072 
1073 
1075  """Representation of a Yeelight ambient light."""
1076 
1077  _attr_translation_key = "ambilight"
1078 
1079  PROPERTIES_MAPPING = {"color_mode": "bg_lmode"}
1080 
1081  def __init__(self, *args, **kwargs):
1082  """Initialize the Yeelight Ambient light."""
1083  super().__init__(*args, **kwargs)
1084  self._attr_min_mireds_attr_min_mireds_attr_min_mireds = kelvin_to_mired(6500)
1085  self._attr_max_mireds_attr_max_mireds_attr_max_mireds = kelvin_to_mired(1700)
1086 
1087  self._light_type_light_type_light_type = LightType.Ambient
1088 
1089  @property
1090  def unique_id(self) -> str:
1091  """Return a unique ID."""
1092  unique = super().unique_id
1093  return f"{unique}-ambilight"
1094 
1095  @property
1096  def _brightness_property(self) -> str:
1097  return "bright"
1098 
1099  def _get_property(self, prop: str, default=None):
1100  if not (bg_prop := self.PROPERTIES_MAPPINGPROPERTIES_MAPPING.get(prop)):
1101  bg_prop = f"bg_{prop}"
1102 
1103  return super()._get_property(bg_prop, default)
tuple[int, int, int]|None rgb_color(self)
Definition: __init__.py:957
set[ColorMode]|set[str]|None supported_color_modes(self)
Definition: __init__.py:1302
tuple[float, float]|None hs_color(self)
Definition: __init__.py:947
ColorMode|str|None color_mode(self)
Definition: __init__.py:909
def _get_property(self, str prop, default=None)
Definition: light.py:1099
dict[str, dict[str, Any]] custom_effects(self)
Definition: light.py:503
def _async_schedule_state_check(self, expected_power_state)
Definition: light.py:819
tuple[int, int, int]|None rgb_color(self)
Definition: light.py:528
def _get_property(self, str prop, default=None)
Definition: light.py:553
None __init__(self, YeelightDevice device, ConfigEntry entry, dict[str, dict[str, Any]]|None custom_effects=None)
Definition: light.py:433
def async_start_flow(self, transitions, count=0, action=ACTION_RECOVER)
Definition: light.py:860
def async_set_scene(self, scene_class, *args)
Definition: light.py:866
None async_set_brightness(self, brightness, duration)
Definition: light.py:611
None async_set_hs(self, hs_color, duration)
Definition: light.py:631
None async_set_colortemp(self, colortemp, duration)
Definition: light.py:681
None async_on_remove(self, CALLBACK_TYPE func)
Definition: entity.py:1331
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
def _async_setup_services(HomeAssistant hass)
Definition: light.py:337
None async_setup_entry(HomeAssistant hass, ConfigEntry config_entry, AddEntitiesCallback async_add_entities)
Definition: light.py:287
def _transitions_config_parser(transitions)
Definition: light.py:216
dict[str, dict[str, Any]] _parse_custom_effects(effects_config)
Definition: light.py:227
Callable[[], None] async_dispatcher_connect(HomeAssistant hass, str signal, Callable[..., Any] target)
Definition: dispatcher.py:103
CALLBACK_TYPE async_call_later(HomeAssistant hass, float|timedelta delay, HassJob[[datetime], Coroutine[Any, Any, None]|None]|Callable[[datetime], Coroutine[Any, Any, None]|None] action)
Definition: event.py:1597