Home Assistant Unofficial Reference 2024.12.1
light.py
Go to the documentation of this file.
1 """Support for LIFX lights."""
2 
3 from __future__ import annotations
4 
5 import asyncio
6 from datetime import datetime, timedelta
7 from typing import Any
8 
9 import aiolifx_effects as aiolifx_effects_module
10 import voluptuous as vol
11 
13  ATTR_EFFECT,
14  ATTR_TRANSITION,
15  LIGHT_TURN_ON_SCHEMA,
16  ColorMode,
17  LightEntity,
18  LightEntityFeature,
19 )
20 from homeassistant.config_entries import ConfigEntry
21 from homeassistant.const import ATTR_ENTITY_ID, Platform
22 from homeassistant.core import CALLBACK_TYPE, HomeAssistant
23 from homeassistant.exceptions import HomeAssistantError
24 from homeassistant.helpers import entity_platform
26 from homeassistant.helpers.entity_platform import AddEntitiesCallback
27 from homeassistant.helpers.event import async_call_later
28 from homeassistant.helpers.typing import VolDictType
29 
30 from .const import (
31  _LOGGER,
32  ATTR_DURATION,
33  ATTR_INFRARED,
34  ATTR_POWER,
35  ATTR_ZONES,
36  DATA_LIFX_MANAGER,
37  DOMAIN,
38  INFRARED_BRIGHTNESS,
39  LIFX_CEILING_PRODUCT_IDS,
40 )
41 from .coordinator import FirmwareEffect, LIFXUpdateCoordinator
42 from .entity import LIFXEntity
43 from .manager import (
44  SERVICE_EFFECT_COLORLOOP,
45  SERVICE_EFFECT_FLAME,
46  SERVICE_EFFECT_MORPH,
47  SERVICE_EFFECT_MOVE,
48  SERVICE_EFFECT_PULSE,
49  SERVICE_EFFECT_SKY,
50  SERVICE_EFFECT_STOP,
51  LIFXManager,
52 )
53 from .util import convert_8_to_16, convert_16_to_8, find_hsbk, lifx_features, merge_hsbk
54 
55 LIFX_STATE_SETTLE_DELAY = 0.3
56 
57 SERVICE_LIFX_SET_STATE = "set_state"
58 
59 LIFX_SET_STATE_SCHEMA: VolDictType = {
60  **LIGHT_TURN_ON_SCHEMA,
61  ATTR_INFRARED: vol.All(vol.Coerce(int), vol.Clamp(min=0, max=255)),
62  ATTR_ZONES: vol.All(cv.ensure_list, [cv.positive_int]),
63  ATTR_POWER: cv.boolean,
64 }
65 
66 
67 SERVICE_LIFX_SET_HEV_CYCLE_STATE = "set_hev_cycle_state"
68 
69 LIFX_SET_HEV_CYCLE_STATE_SCHEMA: VolDictType = {
70  ATTR_POWER: vol.Required(cv.boolean),
71  ATTR_DURATION: vol.All(vol.Coerce(float), vol.Clamp(min=0, max=86400)),
72 }
73 
74 HSBK_HUE = 0
75 HSBK_SATURATION = 1
76 HSBK_BRIGHTNESS = 2
77 HSBK_KELVIN = 3
78 
79 
81  hass: HomeAssistant,
82  entry: ConfigEntry,
83  async_add_entities: AddEntitiesCallback,
84 ) -> None:
85  """Set up LIFX from a config entry."""
86  domain_data = hass.data[DOMAIN]
87  coordinator: LIFXUpdateCoordinator = domain_data[entry.entry_id]
88  manager: LIFXManager = domain_data[DATA_LIFX_MANAGER]
89  device = coordinator.device
90  platform = entity_platform.async_get_current_platform()
91  platform.async_register_entity_service(
92  SERVICE_LIFX_SET_STATE,
93  LIFX_SET_STATE_SCHEMA,
94  "set_state",
95  )
96  platform.async_register_entity_service(
97  SERVICE_LIFX_SET_HEV_CYCLE_STATE,
98  LIFX_SET_HEV_CYCLE_STATE_SCHEMA,
99  "set_hev_cycle_state",
100  )
101  if lifx_features(device)["matrix"]:
102  if device.product in LIFX_CEILING_PRODUCT_IDS:
103  entity: LIFXLight = LIFXCeiling(coordinator, manager, entry)
104  else:
105  entity = LIFXMatrix(coordinator, manager, entry)
106  elif lifx_features(device)["extended_multizone"]:
107  entity = LIFXExtendedMultiZone(coordinator, manager, entry)
108  elif lifx_features(device)["multizone"]:
109  entity = LIFXMultiZone(coordinator, manager, entry)
110  elif lifx_features(device)["color"]:
111  entity = LIFXColor(coordinator, manager, entry)
112  else:
113  entity = LIFXWhite(coordinator, manager, entry)
114  async_add_entities([entity])
115 
116 
118  """Representation of a LIFX light."""
119 
120  _attr_supported_features = LightEntityFeature.TRANSITION | LightEntityFeature.EFFECT
121  _attr_name = None
122 
123  def __init__(
124  self,
125  coordinator: LIFXUpdateCoordinator,
126  manager: LIFXManager,
127  entry: ConfigEntry,
128  ) -> None:
129  """Initialize the light."""
130  super().__init__(coordinator)
131 
132  self.mac_addrmac_addr = self.bulbbulb.mac_addr
133  bulb_features = lifx_features(self.bulbbulb)
134  self.managermanager = manager
135  self.effects_conductor: aiolifx_effects_module.Conductor = (
136  manager.effects_conductor
137  )
138  self.postponed_updatepostponed_update: CALLBACK_TYPE | None = None
139  self.entryentry = entry
140  self._attr_unique_id_attr_unique_id = self.coordinator.serial_number
141  self._attr_min_color_temp_kelvin_attr_min_color_temp_kelvin = bulb_features["min_kelvin"]
142  self._attr_max_color_temp_kelvin_attr_max_color_temp_kelvin = bulb_features["max_kelvin"]
143  if bulb_features["min_kelvin"] != bulb_features["max_kelvin"]:
144  color_mode = ColorMode.COLOR_TEMP
145  else:
146  color_mode = ColorMode.BRIGHTNESS
147 
148  self._attr_color_mode_attr_color_mode = color_mode
149  self._attr_supported_color_modes_attr_supported_color_modes = {color_mode}
150  self._attr_effect_attr_effect = None
151 
152  @property
153  def brightness(self) -> int:
154  """Return the brightness of this light between 0..255."""
155  fade = self.bulbbulb.power_level / 65535
156  return convert_16_to_8(int(fade * self.bulbbulb.color[HSBK_BRIGHTNESS]))
157 
158  @property
159  def color_temp_kelvin(self) -> int | None:
160  """Return the color temperature of this light in kelvin."""
161  return int(self.bulbbulb.color[HSBK_KELVIN])
162 
163  @property
164  def is_on(self) -> bool:
165  """Return true if light is on."""
166  return bool(self.bulbbulb.power_level != 0)
167 
168  @property
169  def effect(self) -> str | None:
170  """Return the name of the currently running effect."""
171  if effect := self.effects_conductor.effect(self.bulbbulb):
172  return f"effect_{effect.name}"
173  if effect := self.coordinator.async_get_active_effect():
174  return f"effect_{FirmwareEffect(effect).name.lower()}"
175  return None
176 
177  async def update_during_transition(self, when: int) -> None:
178  """Update state at the start and end of a transition."""
179  self._cancel_postponed_update_cancel_postponed_update()
180 
181  # Transition has started
182  self.async_write_ha_stateasync_write_ha_state()
183 
184  # The state reply we get back may be stale so we also request
185  # a refresh to get a fresh state
186  # https://lan.developer.lifx.com/docs/changing-a-device
187  await self.coordinator.async_request_refresh()
188 
189  # Transition has ended
190  if when > 0:
191 
192  async def _async_refresh(now: datetime) -> None:
193  """Refresh the state."""
194  await self.coordinator.async_refresh()
195 
196  self.postponed_updatepostponed_update = async_call_later(
197  self.hasshasshass,
198  timedelta(milliseconds=when),
199  _async_refresh,
200  )
201 
202  async def async_turn_on(self, **kwargs: Any) -> None:
203  """Turn the light on."""
204  await self.set_stateset_state(**{**kwargs, ATTR_POWER: True})
205 
206  async def async_turn_off(self, **kwargs: Any) -> None:
207  """Turn the light off."""
208  await self.set_stateset_state(**{**kwargs, ATTR_POWER: False})
209 
210  async def set_state(self, **kwargs: Any) -> None:
211  """Set a color on the light and turn it on/off."""
212  self.coordinator.async_set_updated_data(None)
213  # Cancel any pending refreshes
214  bulb = self.bulbbulb
215 
216  await self.effects_conductor.stop([bulb])
217 
218  if ATTR_EFFECT in kwargs:
219  await self.default_effectdefault_effect(**kwargs)
220  return
221 
222  if ATTR_INFRARED in kwargs:
223  infrared_entity_id = self.coordinator.async_get_entity_id(
224  Platform.SELECT, INFRARED_BRIGHTNESS
225  )
226  _LOGGER.warning(
227  (
228  "The 'infrared' attribute of 'lifx.set_state' is deprecated:"
229  " call 'select.select_option' targeting '%s' instead"
230  ),
231  infrared_entity_id,
232  )
233  bulb.set_infrared(convert_8_to_16(kwargs[ATTR_INFRARED]))
234 
235  if ATTR_TRANSITION in kwargs:
236  fade = int(kwargs[ATTR_TRANSITION] * 1000)
237  else:
238  fade = 0
239 
240  # These are both False if ATTR_POWER is not set
241  power_on = kwargs.get(ATTR_POWER, False)
242  power_off = not kwargs.get(ATTR_POWER, True)
243 
244  hsbk = find_hsbk(self.hasshasshass, **kwargs)
245 
246  if not self.is_onis_onis_on:
247  if power_off:
248  await self.set_powerset_power(False)
249  # If fading on with color, set color immediately
250  if hsbk and power_on:
251  await self.set_colorset_color(hsbk, kwargs)
252  await self.set_powerset_power(True, duration=fade)
253  elif hsbk:
254  await self.set_colorset_color(hsbk, kwargs, duration=fade)
255  elif power_on:
256  await self.set_powerset_power(True, duration=fade)
257  else:
258  if power_on:
259  await self.set_powerset_power(True)
260  if hsbk:
261  await self.set_colorset_color(hsbk, kwargs, duration=fade)
262  if power_off:
263  await self.set_powerset_power(False, duration=fade)
264 
265  # Avoid state ping-pong by holding off updates as the state settles
266  await asyncio.sleep(LIFX_STATE_SETTLE_DELAY)
267 
268  # Update when the transition starts and ends
269  await self.update_during_transitionupdate_during_transition(fade)
270 
272  self, power: bool, duration: int | None = None
273  ) -> None:
274  """Set the state of the HEV LEDs on a LIFX Clean bulb."""
275  if lifx_features(self.bulbbulb)["hev"] is False:
276  raise HomeAssistantError(
277  "This device does not support setting HEV cycle state"
278  )
279 
280  await self.coordinator.async_set_hev_cycle_state(power, duration or 0)
281  await self.update_during_transitionupdate_during_transition(duration or 0)
282 
283  async def set_power(
284  self,
285  pwr: bool,
286  duration: int = 0,
287  ) -> None:
288  """Send a power change to the bulb."""
289  try:
290  await self.coordinator.async_set_power(pwr, duration)
291  except TimeoutError as ex:
292  raise HomeAssistantError(f"Timeout setting power for {self.name}") from ex
293 
294  async def set_color(
295  self,
296  hsbk: list[float | int | None],
297  kwargs: dict[str, Any],
298  duration: int = 0,
299  ) -> None:
300  """Send a color change to the bulb."""
301  merged_hsbk = merge_hsbk(self.bulbbulb.color, hsbk)
302  try:
303  await self.coordinator.async_set_color(merged_hsbk, duration)
304  except TimeoutError as ex:
305  raise HomeAssistantError(f"Timeout setting color for {self.name}") from ex
306 
307  async def get_color(
308  self,
309  ) -> None:
310  """Send a get color message to the bulb."""
311  try:
312  await self.coordinator.async_get_color()
313  except TimeoutError as ex:
314  raise HomeAssistantError(
315  f"Timeout setting getting color for {self.name}"
316  ) from ex
317 
318  async def default_effect(self, **kwargs: Any) -> None:
319  """Start an effect with default parameters."""
320  await self.hasshasshass.services.async_call(
321  DOMAIN,
322  kwargs[ATTR_EFFECT],
323  {ATTR_ENTITY_ID: self.entity_identity_id},
324  context=self._context_context,
325  )
326 
327  async def async_added_to_hass(self) -> None:
328  """Register callbacks."""
329  self.async_on_removeasync_on_remove(
330  self.managermanager.async_register_entity(self.entity_identity_id, self.entryentry.entry_id)
331  )
332  return await super().async_added_to_hass()
333 
334  def _cancel_postponed_update(self) -> None:
335  """Cancel postponed update, if applicable."""
336  if self.postponed_updatepostponed_update:
337  self.postponed_updatepostponed_update()
338  self.postponed_updatepostponed_update = None
339 
340  async def async_will_remove_from_hass(self) -> None:
341  """Run when entity will be removed from hass."""
342  self._cancel_postponed_update_cancel_postponed_update()
343  return await super().async_will_remove_from_hass()
344 
345 
347  """Representation of a white-only LIFX light."""
348 
349  _attr_effect_list = [SERVICE_EFFECT_PULSE, SERVICE_EFFECT_STOP]
350 
351 
353  """Representation of a color LIFX light."""
354 
355  _attr_effect_list = [
356  SERVICE_EFFECT_COLORLOOP,
357  SERVICE_EFFECT_PULSE,
358  SERVICE_EFFECT_STOP,
359  ]
360 
361  @property
362  def supported_color_modes(self) -> set[ColorMode]:
363  """Return the supported color modes."""
364  return {ColorMode.COLOR_TEMP, ColorMode.HS}
365 
366  @property
367  def color_mode(self) -> ColorMode:
368  """Return the color mode of the light."""
369  has_sat = self.bulbbulb.color[HSBK_SATURATION]
370  return ColorMode.HS if has_sat else ColorMode.COLOR_TEMP
371 
372  @property
373  def hs_color(self) -> tuple[float, float] | None:
374  """Return the hs value."""
375  hue, sat, _, _ = self.bulbbulb.color
376  hue = hue / 65535 * 360
377  sat = sat / 65535 * 100
378  return (hue, sat) if sat else None
379 
380 
382  """Representation of a legacy LIFX multizone device."""
383 
384  _attr_effect_list = [
385  SERVICE_EFFECT_COLORLOOP,
386  SERVICE_EFFECT_PULSE,
387  SERVICE_EFFECT_MOVE,
388  SERVICE_EFFECT_STOP,
389  ]
390 
391  async def set_color(
392  self,
393  hsbk: list[float | int | None],
394  kwargs: dict[str, Any],
395  duration: int = 0,
396  ) -> None:
397  """Send a color change to the bulb."""
398  bulb = self.bulbbulb
399  color_zones = bulb.color_zones
400  num_zones = self.coordinator.get_number_of_zones()
401 
402  # Zone brightness is not reported when powered off
403  if not self.is_onis_onis_on and hsbk[HSBK_BRIGHTNESS] is None:
404  await self.set_powerset_power(True)
405  await asyncio.sleep(LIFX_STATE_SETTLE_DELAY)
406  await self.update_color_zonesupdate_color_zones()
407  await self.set_powerset_power(False)
408 
409  if (zones := kwargs.get(ATTR_ZONES)) is None:
410  # Fast track: setting all zones to the same brightness and color
411  # can be treated as a single-zone bulb.
412  first_zone = color_zones[0]
413  first_zone_brightness = first_zone[HSBK_BRIGHTNESS]
414  all_zones_have_same_brightness = all(
415  color_zones[zone][HSBK_BRIGHTNESS] == first_zone_brightness
416  for zone in range(num_zones)
417  )
418  all_zones_are_the_same = all(
419  color_zones[zone] == first_zone for zone in range(num_zones)
420  )
421  if (
422  all_zones_have_same_brightness or hsbk[HSBK_BRIGHTNESS] is not None
423  ) and (all_zones_are_the_same or hsbk[HSBK_KELVIN] is not None):
424  await super().set_color(hsbk, kwargs, duration)
425  return
426 
427  zones = list(range(num_zones))
428  else:
429  zones = [x for x in set(zones) if x < num_zones]
430 
431  # Send new color to each zone
432  for index, zone in enumerate(zones):
433  zone_hsbk = merge_hsbk(color_zones[zone], hsbk)
434  apply = 1 if (index == len(zones) - 1) else 0
435  try:
436  await self.coordinator.async_set_color_zones(
437  zone, zone, zone_hsbk, duration, apply
438  )
439  except TimeoutError as ex:
440  raise HomeAssistantError(
441  f"Timeout setting color zones for {self.name}"
442  ) from ex
443 
444  # set_color_zones does not update the
445  # state of the device, so we need to do that
446  await self.get_colorget_color()
447 
449  self,
450  ) -> None:
451  """Send a get color zones message to the device."""
452  try:
453  await self.coordinator.async_get_color_zones()
454  except TimeoutError as ex:
455  raise HomeAssistantError(
456  f"Timeout getting color zones from {self.name}"
457  ) from ex
458 
459 
461  """Representation of a LIFX device that supports extended multizone messages."""
462 
463  async def set_color(
464  self, hsbk: list[float | int | None], kwargs: dict[str, Any], duration: int = 0
465  ) -> None:
466  """Set colors on all zones of the device."""
467 
468  # trigger an update of all zone values before merging new values
469  await self.coordinator.async_get_extended_color_zones()
470 
471  color_zones = self.bulbbulb.color_zones
472  if (zones := kwargs.get(ATTR_ZONES)) is None:
473  # merge the incoming hsbk across all zones
474  for index, zone in enumerate(color_zones):
475  color_zones[index] = merge_hsbk(zone, hsbk)
476  else:
477  # merge the incoming HSBK with only the specified zones
478  for index, zone in enumerate(color_zones):
479  if index in zones:
480  color_zones[index] = merge_hsbk(zone, hsbk)
481 
482  # send the updated color zones list to the device
483  try:
484  await self.coordinator.async_set_extended_color_zones(
485  color_zones, duration=duration
486  )
487  except TimeoutError as ex:
488  raise HomeAssistantError(
489  f"Timeout setting color zones on {self.name}"
490  ) from ex
491 
492  # set_extended_color_zones does not update the
493  # state of the device, so we need to do that
494  await self.get_colorget_color()
495 
496 
498  """Representation of a LIFX matrix device."""
499 
500  _attr_effect_list = [
501  SERVICE_EFFECT_COLORLOOP,
502  SERVICE_EFFECT_FLAME,
503  SERVICE_EFFECT_PULSE,
504  SERVICE_EFFECT_MORPH,
505  SERVICE_EFFECT_STOP,
506  ]
507 
508 
510  """Representation of a LIFX Ceiling device."""
511 
512  _attr_effect_list = [
513  SERVICE_EFFECT_COLORLOOP,
514  SERVICE_EFFECT_FLAME,
515  SERVICE_EFFECT_PULSE,
516  SERVICE_EFFECT_MORPH,
517  SERVICE_EFFECT_SKY,
518  SERVICE_EFFECT_STOP,
519  ]
str|None async_get_entity_id(self, Platform platform, str key)
Definition: coordinator.py:218
None async_set_color_zones(self, int start_index, int end_index, list[float|int|None] hsbk, int|None duration, int apply)
Definition: coordinator.py:391
None async_set_extended_color_zones(self, list[tuple[int|float, int|float, int|float, int|float]] colors, int|None colors_count=None, int duration=0, int apply=1)
Definition: coordinator.py:410
None async_set_color(self, list[float|int|None] hsbk, int|None duration)
Definition: coordinator.py:378
None async_set_hev_cycle_state(self, bool enable, int duration=0)
Definition: coordinator.py:532
None async_set_power(self, bool state, int|None duration)
Definition: coordinator.py:370
set[ColorMode] supported_color_modes(self)
Definition: light.py:362
tuple[float, float]|None hs_color(self)
Definition: light.py:373
None set_color(self, list[float|int|None] hsbk, dict[str, Any] kwargs, int duration=0)
Definition: light.py:465
None set_power(self, bool pwr, int duration=0)
Definition: light.py:287
None default_effect(self, **Any kwargs)
Definition: light.py:318
None set_state(self, **Any kwargs)
Definition: light.py:210
None set_color(self, list[float|int|None] hsbk, dict[str, Any] kwargs, int duration=0)
Definition: light.py:299
None set_hev_cycle_state(self, bool power, int|None duration=None)
Definition: light.py:273
None __init__(self, LIFXUpdateCoordinator coordinator, LIFXManager manager, ConfigEntry entry)
Definition: light.py:128
None async_turn_on(self, **Any kwargs)
Definition: light.py:202
None async_turn_off(self, **Any kwargs)
Definition: light.py:206
None update_during_transition(self, int when)
Definition: light.py:177
None set_color(self, list[float|int|None] hsbk, dict[str, Any] kwargs, int duration=0)
Definition: light.py:396
None async_on_remove(self, CALLBACK_TYPE func)
Definition: entity.py:1331
None _async_refresh(self, bool log_failures=True, bool raise_on_auth_failed=False, bool scheduled=False, bool raise_on_entry_error=False)
None async_setup_entry(HomeAssistant hass, ConfigEntry entry, AddEntitiesCallback async_add_entities)
Definition: light.py:84
int convert_16_to_8(int value)
Definition: util.py:73
list[float|int|None] merge_hsbk(list[float|int|None] base, list[float|int|None] change)
Definition: util.py:147
list[float|int|None]|None find_hsbk(HomeAssistant hass, **Any kwargs)
Definition: util.py:86
int convert_8_to_16(int value)
Definition: util.py:68
dict[str, Any] lifx_features(Light bulb)
Definition: util.py:78
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