Home Assistant Unofficial Reference 2024.12.1
light.py
Go to the documentation of this file.
1 """Support for the Philips Hue lights."""
2 
3 from __future__ import annotations
4 
5 import asyncio
6 from datetime import timedelta
7 from functools import partial
8 import logging
9 import random
10 
11 import aiohue
12 
14  ATTR_BRIGHTNESS,
15  ATTR_COLOR_TEMP,
16  ATTR_EFFECT,
17  ATTR_FLASH,
18  ATTR_HS_COLOR,
19  ATTR_TRANSITION,
20  EFFECT_COLORLOOP,
21  EFFECT_RANDOM,
22  FLASH_LONG,
23  FLASH_SHORT,
24  ColorMode,
25  LightEntity,
26  LightEntityFeature,
27  filter_supported_color_modes,
28 )
29 from homeassistant.core import callback
30 from homeassistant.exceptions import PlatformNotReady
31 from homeassistant.helpers.debounce import Debouncer
32 from homeassistant.helpers.device_registry import DeviceInfo
34  CoordinatorEntity,
35  DataUpdateCoordinator,
36  UpdateFailed,
37 )
38 from homeassistant.util import color
39 
40 from ..bridge import HueBridge
41 from ..const import (
42  CONF_ALLOW_HUE_GROUPS,
43  CONF_ALLOW_UNREACHABLE,
44  DEFAULT_ALLOW_HUE_GROUPS,
45  DEFAULT_ALLOW_UNREACHABLE,
46  DOMAIN as HUE_DOMAIN,
47  GROUP_TYPE_ENTERTAINMENT,
48  GROUP_TYPE_LIGHT_GROUP,
49  GROUP_TYPE_LIGHT_SOURCE,
50  GROUP_TYPE_LUMINAIRE,
51  GROUP_TYPE_ROOM,
52  GROUP_TYPE_ZONE,
53  REQUEST_REFRESH_DELAY,
54 )
55 from .helpers import remove_devices
56 
57 SCAN_INTERVAL = timedelta(seconds=5)
58 
59 LOGGER = logging.getLogger(__name__)
60 
61 COLOR_MODES_HUE_ON_OFF = {ColorMode.ONOFF}
62 COLOR_MODES_HUE_DIMMABLE = {ColorMode.BRIGHTNESS}
63 COLOR_MODES_HUE_COLOR_TEMP = {ColorMode.COLOR_TEMP}
64 COLOR_MODES_HUE_COLOR = {ColorMode.HS}
65 COLOR_MODES_HUE_EXTENDED = {ColorMode.COLOR_TEMP, ColorMode.HS}
66 
67 COLOR_MODES_HUE = {
68  "Extended color light": COLOR_MODES_HUE_EXTENDED,
69  "Color light": COLOR_MODES_HUE_COLOR,
70  "Dimmable light": COLOR_MODES_HUE_DIMMABLE,
71  "On/Off plug-in unit": COLOR_MODES_HUE_ON_OFF,
72  "Color temperature light": COLOR_MODES_HUE_COLOR_TEMP,
73 }
74 
75 SUPPORT_HUE_ON_OFF = LightEntityFeature.FLASH | LightEntityFeature.TRANSITION
76 SUPPORT_HUE_DIMMABLE = SUPPORT_HUE_ON_OFF
77 SUPPORT_HUE_COLOR_TEMP = SUPPORT_HUE_DIMMABLE
78 SUPPORT_HUE_COLOR = SUPPORT_HUE_DIMMABLE | LightEntityFeature.EFFECT
79 SUPPORT_HUE_EXTENDED = SUPPORT_HUE_COLOR_TEMP | SUPPORT_HUE_COLOR
80 
81 SUPPORT_HUE = {
82  "Extended color light": SUPPORT_HUE_EXTENDED,
83  "Color light": SUPPORT_HUE_COLOR,
84  "Dimmable light": SUPPORT_HUE_DIMMABLE,
85  "On/Off plug-in unit": SUPPORT_HUE_ON_OFF,
86  "Color temperature light": SUPPORT_HUE_COLOR_TEMP,
87 }
88 
89 ATTR_IS_HUE_GROUP = "is_hue_group"
90 GAMUT_TYPE_UNAVAILABLE = "None"
91 # Minimum Hue Bridge API version to support groups
92 # 1.4.0 introduced extended group info
93 # 1.12 introduced the state object for groups
94 # 1.13 introduced "any_on" to group state objects
95 GROUP_MIN_API_VERSION = (1, 13, 0)
96 
97 
98 async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
99  """Old way of setting up Hue lights.
100 
101  Can only be called when a user accidentally mentions hue platform in their
102  config. But even in that case it would have been ignored.
103  """
104 
105 
106 def create_light(item_class, coordinator, bridge, is_group, rooms, api, item_id):
107  """Create the light."""
108  api_item = api[item_id]
109 
110  if is_group:
111  supported_color_modes = set()
112  supported_features = LightEntityFeature(0)
113  for light_id in api_item.lights:
114  if light_id not in bridge.api.lights:
115  continue
116  light = bridge.api.lights[light_id]
117  supported_features |= SUPPORT_HUE.get(light.type, SUPPORT_HUE_EXTENDED)
118  supported_color_modes.update(
119  COLOR_MODES_HUE.get(light.type, COLOR_MODES_HUE_EXTENDED)
120  )
121  supported_features = supported_features or SUPPORT_HUE_EXTENDED
122  supported_color_modes = supported_color_modes or COLOR_MODES_HUE_EXTENDED
123  supported_color_modes = filter_supported_color_modes(supported_color_modes)
124  else:
125  supported_color_modes = COLOR_MODES_HUE.get(
126  api_item.type, COLOR_MODES_HUE_EXTENDED
127  )
128  supported_features = SUPPORT_HUE.get(api_item.type, SUPPORT_HUE_EXTENDED)
129  return item_class(
130  coordinator,
131  bridge,
132  is_group,
133  api_item,
134  supported_color_modes,
135  supported_features,
136  rooms,
137  )
138 
139 
140 async def async_setup_entry(hass, config_entry, async_add_entities):
141  """Set up the Hue lights from a config entry."""
142  bridge: HueBridge = hass.data[HUE_DOMAIN][config_entry.entry_id]
143  api_version = tuple(int(v) for v in bridge.api.config.apiversion.split("."))
144  rooms = {}
145 
146  allow_groups = config_entry.options.get(
147  CONF_ALLOW_HUE_GROUPS, DEFAULT_ALLOW_HUE_GROUPS
148  )
149  supports_groups = api_version >= GROUP_MIN_API_VERSION
150  if allow_groups and not supports_groups:
151  LOGGER.warning("Please update your Hue bridge to support groups")
152 
153  light_coordinator = DataUpdateCoordinator(
154  hass,
155  LOGGER,
156  name="light",
157  update_method=partial(async_safe_fetch, bridge, bridge.api.lights.update),
158  update_interval=SCAN_INTERVAL,
159  request_refresh_debouncer=Debouncer(
160  bridge.hass, LOGGER, cooldown=REQUEST_REFRESH_DELAY, immediate=True
161  ),
162  )
163 
164  # First do a refresh to see if we can reach the hub.
165  # Otherwise we will declare not ready.
166  await light_coordinator.async_refresh()
167 
168  if not light_coordinator.last_update_success:
169  raise PlatformNotReady
170 
171  if not supports_groups:
172  update_lights_without_group_support = partial(
173  async_update_items,
174  bridge,
175  bridge.api.lights,
176  {},
177  async_add_entities,
178  partial(create_light, HueLight, light_coordinator, bridge, False, rooms),
179  None,
180  )
181  # We add a listener after fetching the data, so manually trigger listener
182  bridge.reset_jobs.append(
183  light_coordinator.async_add_listener(update_lights_without_group_support)
184  )
185  return
186 
187  group_coordinator = DataUpdateCoordinator(
188  hass,
189  LOGGER,
190  name="group",
191  update_method=partial(async_safe_fetch, bridge, bridge.api.groups.update),
192  update_interval=SCAN_INTERVAL,
193  request_refresh_debouncer=Debouncer(
194  bridge.hass, LOGGER, cooldown=REQUEST_REFRESH_DELAY, immediate=True
195  ),
196  )
197 
198  if allow_groups:
199  update_groups = partial(
200  async_update_items,
201  bridge,
202  bridge.api.groups,
203  {},
204  async_add_entities,
205  partial(create_light, HueLight, group_coordinator, bridge, True, None),
206  None,
207  )
208 
209  bridge.reset_jobs.append(group_coordinator.async_add_listener(update_groups))
210 
211  cancel_update_rooms_listener = None
212 
213  @callback
214  def _async_update_rooms():
215  """Update rooms."""
216  nonlocal cancel_update_rooms_listener
217  rooms.clear()
218  for item_id in bridge.api.groups:
219  group = bridge.api.groups[item_id]
220  if group.type not in [GROUP_TYPE_ROOM, GROUP_TYPE_ZONE]:
221  continue
222  for light_id in group.lights:
223  rooms[light_id] = group.name
224 
225  # Once we do a rooms update, we cancel the listener
226  # until the next time lights are added
227  bridge.reset_jobs.remove(cancel_update_rooms_listener)
228  cancel_update_rooms_listener()
229  cancel_update_rooms_listener = None
230 
231  @callback
232  def _setup_rooms_listener():
233  nonlocal cancel_update_rooms_listener
234  if cancel_update_rooms_listener is not None:
235  # If there are new lights added before _async_update_rooms
236  # is called we should not add another listener
237  return
238 
239  cancel_update_rooms_listener = group_coordinator.async_add_listener(
240  _async_update_rooms
241  )
242  bridge.reset_jobs.append(cancel_update_rooms_listener)
243 
244  _setup_rooms_listener()
245  await group_coordinator.async_refresh()
246 
247  update_lights_with_group_support = partial(
248  async_update_items,
249  bridge,
250  bridge.api.lights,
251  {},
252  async_add_entities,
253  partial(create_light, HueLight, light_coordinator, bridge, False, rooms),
254  _setup_rooms_listener,
255  )
256  # We add a listener after fetching the data, so manually trigger listener
257  bridge.reset_jobs.append(
258  light_coordinator.async_add_listener(update_lights_with_group_support)
259  )
260  update_lights_with_group_support()
261 
262 
263 async def async_safe_fetch(bridge, fetch_method):
264  """Safely fetch data."""
265  try:
266  async with asyncio.timeout(4):
267  return await bridge.async_request_call(fetch_method)
268  except aiohue.Unauthorized as err:
269  await bridge.handle_unauthorized_error()
270  raise UpdateFailed("Unauthorized") from err
271  except aiohue.AiohueException as err:
272  raise UpdateFailed(f"Hue error: {err}") from err
273 
274 
275 @callback
277  bridge, api, current, async_add_entities, create_item, new_items_callback
278 ):
279  """Update items."""
280  new_items = []
281 
282  for item_id in api:
283  if item_id in current:
284  continue
285 
286  current[item_id] = create_item(api, item_id)
287  new_items.append(current[item_id])
288 
289  bridge.hass.async_create_task(remove_devices(bridge, api, current))
290 
291  if new_items:
292  # This is currently used to setup the listener to update rooms
293  if new_items_callback:
294  new_items_callback()
295  async_add_entities(new_items)
296 
297 
299  """Convert hue brightness 1..254 to hass format 0..255."""
300  return min(255, round((value / 254) * 255))
301 
302 
304  """Convert hass brightness 0..255 to hue 1..254 scale."""
305  return max(1, round((value / 255) * 254))
306 
307 
308 # pylint: disable-next=hass-enforce-class-module
310  """Representation of a Hue light."""
311 
312  def __init__(
313  self,
314  coordinator,
315  bridge,
316  is_group,
317  light,
318  supported_color_modes,
319  supported_features,
320  rooms,
321  ):
322  """Initialize the light."""
323  super().__init__(coordinator)
324  self._attr_supported_color_modes_attr_supported_color_modes = supported_color_modes
325  self._attr_supported_features_attr_supported_features = supported_features
326  self.lightlight = light
327  self.bridgebridge = bridge
328  self.is_groupis_group = is_group
329  self._rooms_rooms = rooms
330  self.allow_unreachableallow_unreachable = self.bridgebridge.config_entry.options.get(
331  CONF_ALLOW_UNREACHABLE, DEFAULT_ALLOW_UNREACHABLE
332  )
333 
334  self._fixed_color_mode_fixed_color_mode = None
335  if len(supported_color_modes) == 1:
336  self._fixed_color_mode_fixed_color_mode = next(iter(supported_color_modes))
337  else:
338  assert supported_color_modes == {ColorMode.COLOR_TEMP, ColorMode.HS}
339 
340  if is_group:
341  self.is_osramis_osram = False
342  self.is_philipsis_philips = False
343  self.is_innris_innr = False
344  self.is_ewelinkis_ewelink = False
345  self.is_livarnois_livarno = False
346  self.is_s31litezbis_s31litezb = False
347  self.gamut_typgamut_typ = GAMUT_TYPE_UNAVAILABLE
348  self.gamutgamut = None
349  else:
350  self.is_osramis_osram = light.manufacturername == "OSRAM"
351  self.is_philipsis_philips = light.manufacturername == "Philips"
352  self.is_innris_innr = light.manufacturername == "innr"
353  self.is_ewelinkis_ewelink = light.manufacturername == "eWeLink"
354  self.is_livarnois_livarno = light.manufacturername.startswith("_TZ3000_")
355  self.is_s31litezbis_s31litezb = light.modelid == "S31 Lite zb"
356  self.gamut_typgamut_typ = self.lightlight.colorgamuttype
357  self.gamutgamut = self.lightlight.colorgamut
358  LOGGER.debug("Color gamut of %s: %s", self.namenamename, str(self.gamutgamut))
359  if self.lightlight.swupdatestate == "readytoinstall":
360  err = (
361  "Please check for software updates of the %s "
362  "bulb in the Philips Hue App."
363  )
364  LOGGER.warning(err, self.namenamename)
365  if self.gamutgamut and not color.check_valid_gamut(self.gamutgamut):
366  err = "Color gamut of %s: %s, not valid, setting gamut to None."
367  LOGGER.debug(err, self.namenamename, str(self.gamutgamut))
368  self.gamut_typgamut_typ = GAMUT_TYPE_UNAVAILABLE
369  self.gamutgamut = None
370 
371  @property
372  def unique_id(self):
373  """Return the unique ID of this Hue light."""
374  unique_id = self.lightlight.uniqueid
375  if not unique_id and self.is_groupis_group:
376  unique_id = self.lightlight.id
377 
378  return unique_id
379 
380  @property
381  def device_id(self):
382  """Return the ID of this Hue light."""
383  return self.unique_idunique_idunique_id
384 
385  @property
386  def name(self):
387  """Return the name of the Hue light."""
388  return self.lightlight.name
389 
390  @property
391  def brightness(self):
392  """Return the brightness of this light between 0..255."""
393  if self.is_groupis_group:
394  bri = self.lightlight.action.get("bri")
395  else:
396  bri = self.lightlight.state.get("bri")
397 
398  if bri is None:
399  return bri
400 
401  return hue_brightness_to_hass(bri)
402 
403  @property
404  def color_mode(self) -> str:
405  """Return the color mode of the light."""
406  if self._fixed_color_mode_fixed_color_mode:
407  return self._fixed_color_mode_fixed_color_mode
408 
409  # The light supports both hs/xy and white with adjustabe color_temperature
410  mode = self._color_mode_color_mode
411  if mode in ("xy", "hs"):
412  return ColorMode.HS
413 
414  return ColorMode.COLOR_TEMP
415 
416  @property
417  def _color_mode(self):
418  """Return the hue color mode."""
419  if self.is_groupis_group:
420  return self.lightlight.action.get("colormode")
421  return self.lightlight.state.get("colormode")
422 
423  @property
424  def hs_color(self):
425  """Return the hs color value."""
426  mode = self._color_mode_color_mode
427  source = self.lightlight.action if self.is_groupis_group else self.lightlight.state
428 
429  if mode in ("xy", "hs") and "xy" in source:
430  return color.color_xy_to_hs(*source["xy"], self.gamutgamut)
431 
432  return None
433 
434  @property
435  def color_temp(self):
436  """Return the CT color value."""
437  # Don't return color temperature unless in color temperature mode
438  if self._color_mode_color_mode != "ct":
439  return None
440 
441  if self.is_groupis_group:
442  return self.lightlight.action.get("ct")
443  return self.lightlight.state.get("ct")
444 
445  @property
446  def min_mireds(self):
447  """Return the coldest color_temp that this light supports."""
448  if self.is_groupis_group:
449  return super().min_mireds
450 
451  min_mireds = self.lightlight.controlcapabilities.get("ct", {}).get("min")
452 
453  # We filter out '0' too, which can be incorrectly reported by 3rd party buls
454  if not min_mireds:
455  return super().min_mireds
456 
457  return min_mireds
458 
459  @property
460  def max_mireds(self):
461  """Return the warmest color_temp that this light supports."""
462  if self.is_groupis_group:
463  return super().max_mireds
464  if self.is_livarnois_livarno:
465  return 500
466 
467  max_mireds = self.lightlight.controlcapabilities.get("ct", {}).get("max")
468 
469  if not max_mireds:
470  return super().max_mireds
471 
472  return max_mireds
473 
474  @property
475  def is_on(self):
476  """Return true if device is on."""
477  if self.is_groupis_group:
478  return self.lightlight.state["any_on"]
479  return self.lightlight.state["on"]
480 
481  @property
482  def available(self):
483  """Return if light is available."""
484  return self.coordinator.last_update_success and (
485  self.is_groupis_group or self.allow_unreachableallow_unreachable or self.lightlight.state["reachable"]
486  )
487 
488  @property
489  def effect(self):
490  """Return the current effect."""
491  return self.lightlight.state.get("effect", None)
492 
493  @property
494  def effect_list(self):
495  """Return the list of supported effects."""
496  if self.is_osramis_osram:
497  return [EFFECT_RANDOM]
498  return [EFFECT_COLORLOOP, EFFECT_RANDOM]
499 
500  @property
501  def device_info(self) -> DeviceInfo | None:
502  """Return the device info."""
503  if self.lightlight.type in (
504  GROUP_TYPE_ENTERTAINMENT,
505  GROUP_TYPE_LIGHT_GROUP,
506  GROUP_TYPE_ROOM,
507  GROUP_TYPE_LUMINAIRE,
508  GROUP_TYPE_LIGHT_SOURCE,
509  GROUP_TYPE_ZONE,
510  ):
511  return None
512 
513  suggested_area = None
514  if self._rooms_rooms and self.lightlight.id in self._rooms_rooms:
515  suggested_area = self._rooms_rooms[self.lightlight.id]
516 
517  return DeviceInfo(
518  identifiers={(HUE_DOMAIN, self.device_iddevice_id)},
519  manufacturer=self.lightlight.manufacturername,
520  # productname added in Hue Bridge API 1.24
521  # (published 03/05/2018)
522  model=self.lightlight.productname or self.lightlight.modelid,
523  name=self.namenamename,
524  sw_version=self.lightlight.swversion,
525  suggested_area=suggested_area,
526  via_device=(HUE_DOMAIN, self.bridgebridge.api.config.bridgeid),
527  )
528 
529  async def async_turn_on(self, **kwargs):
530  """Turn the specified or all lights on."""
531  command = {"on": True}
532 
533  if ATTR_TRANSITION in kwargs:
534  command["transitiontime"] = int(kwargs[ATTR_TRANSITION] * 10)
535 
536  if ATTR_HS_COLOR in kwargs:
537  if self.is_osramis_osram:
538  command["hue"] = int(kwargs[ATTR_HS_COLOR][0] / 360 * 65535)
539  command["sat"] = int(kwargs[ATTR_HS_COLOR][1] / 100 * 255)
540  else:
541  # Philips hue bulb models respond differently to hue/sat
542  # requests, so we convert to XY first to ensure a consistent
543  # color.
544  xy_color = color.color_hs_to_xy(*kwargs[ATTR_HS_COLOR], self.gamutgamut)
545  command["xy"] = xy_color
546  elif ATTR_COLOR_TEMP in kwargs:
547  temp = kwargs[ATTR_COLOR_TEMP]
548  command["ct"] = max(self.min_miredsmin_miredsmin_mireds, min(temp, self.max_miredsmax_miredsmax_mireds))
549 
550  if ATTR_BRIGHTNESS in kwargs:
551  command["bri"] = hass_to_hue_brightness(kwargs[ATTR_BRIGHTNESS])
552 
553  flash = kwargs.get(ATTR_FLASH)
554 
555  if flash == FLASH_LONG:
556  command["alert"] = "lselect"
557  del command["on"]
558  elif flash == FLASH_SHORT:
559  command["alert"] = "select"
560  del command["on"]
561  elif (
562  not self.is_innris_innr
563  and not self.is_ewelinkis_ewelink
564  and not self.is_livarnois_livarno
565  and not self.is_s31litezbis_s31litezb
566  ):
567  command["alert"] = "none"
568 
569  if ATTR_EFFECT in kwargs:
570  effect = kwargs[ATTR_EFFECT]
571  if effect == EFFECT_COLORLOOP:
572  command["effect"] = "colorloop"
573  elif effect == EFFECT_RANDOM:
574  command["hue"] = random.randrange(0, 65535)
575  command["sat"] = random.randrange(150, 254)
576  else:
577  command["effect"] = "none"
578 
579  if self.is_groupis_group:
580  await self.bridgebridge.async_request_call(self.lightlight.set_action, **command)
581  else:
582  await self.bridgebridge.async_request_call(self.lightlight.set_state, **command)
583 
584  await self.coordinator.async_request_refresh()
585 
586  async def async_turn_off(self, **kwargs):
587  """Turn the specified or all lights off."""
588  command = {"on": False}
589 
590  if ATTR_TRANSITION in kwargs:
591  command["transitiontime"] = int(kwargs[ATTR_TRANSITION] * 10)
592 
593  flash = kwargs.get(ATTR_FLASH)
594 
595  if flash == FLASH_LONG:
596  command["alert"] = "lselect"
597  del command["on"]
598  elif flash == FLASH_SHORT:
599  command["alert"] = "select"
600  del command["on"]
601  elif not self.is_innris_innr and not self.is_livarnois_livarno:
602  command["alert"] = "none"
603 
604  if self.is_groupis_group:
605  await self.bridgebridge.async_request_call(self.lightlight.set_action, **command)
606  else:
607  await self.bridgebridge.async_request_call(self.lightlight.set_state, **command)
608 
609  await self.coordinator.async_request_refresh()
610 
611  @property
613  """Return the device state attributes."""
614  if not self.is_groupis_group:
615  return {}
616  return {ATTR_IS_HUE_GROUP: self.is_groupis_group}
def __init__(self, coordinator, bridge, is_group, light, supported_color_modes, supported_features, rooms)
Definition: light.py:321
str|UndefinedType|None name(self)
Definition: entity.py:738
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
def remove_devices(bridge, api_ids, current)
Definition: helpers.py:8
def async_setup_entry(hass, config_entry, async_add_entities)
Definition: light.py:140
def create_light(item_class, coordinator, bridge, is_group, rooms, api, item_id)
Definition: light.py:106
def async_update_items(bridge, api, current, async_add_entities, create_item, new_items_callback)
Definition: light.py:278
def async_safe_fetch(bridge, fetch_method)
Definition: light.py:263
def async_setup_platform(hass, config, async_add_entities, discovery_info=None)
Definition: light.py:98
set[ColorMode] filter_supported_color_modes(Iterable[ColorMode] color_modes)
Definition: __init__.py:122