Home Assistant Unofficial Reference 2024.12.1
light.py
Go to the documentation of this file.
1 """Support for deCONZ lights."""
2 
3 from __future__ import annotations
4 
5 from typing import Any, TypedDict, cast
6 
7 from pydeconz.interfaces.groups import GroupHandler
8 from pydeconz.interfaces.lights import LightHandler
9 from pydeconz.models.event import EventType
10 from pydeconz.models.group import Group, TypedGroupAction
11 from pydeconz.models.light.light import Light, LightAlert, LightColorMode, LightEffect
12 
14  ATTR_BRIGHTNESS,
15  ATTR_COLOR_TEMP,
16  ATTR_EFFECT,
17  ATTR_FLASH,
18  ATTR_HS_COLOR,
19  ATTR_TRANSITION,
20  ATTR_XY_COLOR,
21  DOMAIN as LIGHT_DOMAIN,
22  EFFECT_COLORLOOP,
23  FLASH_LONG,
24  FLASH_SHORT,
25  ColorMode,
26  LightEntity,
27  LightEntityFeature,
28 )
29 from homeassistant.config_entries import ConfigEntry
30 from homeassistant.core import HomeAssistant, callback
31 from homeassistant.helpers.device_registry import DeviceInfo
32 from homeassistant.helpers.entity_platform import AddEntitiesCallback
33 from homeassistant.util.color import color_hs_to_xy
34 
35 from .const import DOMAIN as DECONZ_DOMAIN, POWER_PLUGS
36 from .entity import DeconzDevice
37 from .hub import DeconzHub
38 
39 DECONZ_GROUP = "is_deconz_group"
40 EFFECT_TO_DECONZ = {
41  EFFECT_COLORLOOP: LightEffect.COLOR_LOOP,
42  "none": LightEffect.NONE,
43  # Specific to Philips Hue
44  "candle": LightEffect.CANDLE,
45  "cosmos": LightEffect.COSMOS,
46  "enchant": LightEffect.ENCHANT,
47  "fire": LightEffect.FIRE,
48  "fireplace": LightEffect.FIREPLACE,
49  "glisten": LightEffect.GLISTEN,
50  "loop": LightEffect.LOOP,
51  "opal": LightEffect.OPAL,
52  "prism": LightEffect.PRISM,
53  "sparkle": LightEffect.SPARKLE,
54  "sunbeam": LightEffect.SUNBEAM,
55  "sunrise": LightEffect.SUNRISE,
56  "sunset": LightEffect.SUNSET,
57  "underwater": LightEffect.UNDERWATER,
58  # Specific to Lidl christmas light
59  "carnival": LightEffect.CARNIVAL,
60  "collide": LightEffect.COLLIDE,
61  "fading": LightEffect.FADING,
62  "fireworks": LightEffect.FIREWORKS,
63  "flag": LightEffect.FLAG,
64  "glow": LightEffect.GLOW,
65  "rainbow": LightEffect.RAINBOW,
66  "snake": LightEffect.SNAKE,
67  "snow": LightEffect.SNOW,
68  "sparkles": LightEffect.SPARKLES,
69  "steady": LightEffect.STEADY,
70  "strobe": LightEffect.STROBE,
71  "twinkle": LightEffect.TWINKLE,
72  "updown": LightEffect.UPDOWN,
73  "vintage": LightEffect.VINTAGE,
74  "waves": LightEffect.WAVES,
75 }
76 FLASH_TO_DECONZ = {FLASH_SHORT: LightAlert.SHORT, FLASH_LONG: LightAlert.LONG}
77 
78 DECONZ_TO_COLOR_MODE = {
79  LightColorMode.CT: ColorMode.COLOR_TEMP,
80  LightColorMode.GRADIENT: ColorMode.XY,
81  LightColorMode.HS: ColorMode.HS,
82  LightColorMode.XY: ColorMode.XY,
83 }
84 
85 XMAS_LIGHT_EFFECTS = [
86  "carnival",
87  "collide",
88  "fading",
89  "fireworks",
90  "flag",
91  "glow",
92  "rainbow",
93  "snake",
94  "snow",
95  "sparkles",
96  "steady",
97  "strobe",
98  "twinkle",
99  "updown",
100  "vintage",
101  "waves",
102 ]
103 
104 
105 class SetStateAttributes(TypedDict, total=False):
106  """Attributes available with set state call."""
107 
108  alert: LightAlert
109  brightness: int
110  color_temperature: int
111  effect: LightEffect
112  hue: int
113  on: bool
114  saturation: int
115  transition_time: int
116  xy: tuple[float, float]
117 
118 
120  group: Group, lights: list[Light], override: bool = False
121 ) -> None:
122  """Sync group color state with light."""
123  data = {
124  attribute: light_attribute
125  for light in lights
126  for attribute in ("bri", "ct", "hue", "sat", "xy", "colormode", "effect")
127  if (light_attribute := light.raw["state"].get(attribute)) is not None
128  }
129 
130  if override:
131  group.raw["action"] = cast(TypedGroupAction, data)
132  else:
133  group.update(cast(dict[str, dict[str, Any]], {"action": data}))
134 
135 
137  hass: HomeAssistant,
138  config_entry: ConfigEntry,
139  async_add_entities: AddEntitiesCallback,
140 ) -> None:
141  """Set up the deCONZ lights and groups from a config entry."""
142  hub = DeconzHub.get_hub(hass, config_entry)
143  hub.entities[LIGHT_DOMAIN] = set()
144 
145  @callback
146  def async_add_light(_: EventType, light_id: str) -> None:
147  """Add light from deCONZ."""
148  light = hub.api.lights.lights[light_id]
149  if light.type in POWER_PLUGS:
150  return
151 
152  async_add_entities([DeconzLight(light, hub)])
153 
154  hub.register_platform_add_device_callback(
155  async_add_light,
156  hub.api.lights.lights,
157  )
158 
159  @callback
160  def async_add_group(_: EventType, group_id: str) -> None:
161  """Add group from deCONZ.
162 
163  Update group states based on its sum of related lights.
164  """
165  if (group := hub.api.groups[group_id]) and not group.lights:
166  return
167 
168  lights = [
169  light
170  for light_id in group.lights
171  if (light := hub.api.lights.lights.get(light_id)) and light.reachable
172  ]
173  update_color_state(group, lights, True)
174 
175  async_add_entities([DeconzGroup(group, hub)])
176 
177  hub.register_platform_add_device_callback(
178  async_add_group,
179  hub.api.groups,
180  )
181 
182 
183 class DeconzBaseLight[_LightDeviceT: Group | Light](
184  DeconzDevice[_LightDeviceT], LightEntity
185 ):
186  """Representation of a deCONZ light."""
187 
188  TYPE = LIGHT_DOMAIN
189  _attr_color_mode = ColorMode.UNKNOWN
190 
191  def __init__(self, device: _LightDeviceT, hub: DeconzHub) -> None:
192  """Set up light."""
193  super().__init__(device, hub)
194 
195  self.api: GroupHandler | LightHandler
196  if isinstance(self._device, Light):
197  self.api = self.hub.api.lights.lights
198  elif isinstance(self._device, Group):
199  self.api = self.hub.api.groups
200 
201  self._attr_supported_color_modes: set[ColorMode] = set()
202 
203  if device.color_temp is not None:
204  self._attr_supported_color_modes.add(ColorMode.COLOR_TEMP)
205 
206  if device.hue is not None and device.saturation is not None:
207  self._attr_supported_color_modes.add(ColorMode.HS)
208 
209  if device.xy is not None:
210  self._attr_supported_color_modes.add(ColorMode.XY)
211 
212  if not self._attr_supported_color_modes and device.brightness is not None:
213  self._attr_supported_color_modes.add(ColorMode.BRIGHTNESS)
214 
215  if not self._attr_supported_color_modes:
216  self._attr_supported_color_modes.add(ColorMode.ONOFF)
217 
218  if device.brightness is not None:
219  self._attr_supported_features |= (
220  LightEntityFeature.FLASH | LightEntityFeature.TRANSITION
221  )
222 
223  if device.effect is not None:
224  self._attr_supported_features |= LightEntityFeature.EFFECT
225  self._attr_effect_list = [EFFECT_COLORLOOP]
226 
227  # For lights that report supported effects.
228  if isinstance(device, Light):
229  if device.supported_effects is not None:
230  self._attr_effect_list = [
231  EFFECT_TO_DECONZ[el]
232  for el in device.supported_effects
233  if el in EFFECT_TO_DECONZ
234  ]
235  if device.model_id in ("HG06467", "TS0601"):
236  self._attr_effect_list = XMAS_LIGHT_EFFECTS
237 
238  @property
239  def color_mode(self) -> str | None:
240  """Return the color mode of the light."""
241  if self._device.color_mode in DECONZ_TO_COLOR_MODE:
242  color_mode = DECONZ_TO_COLOR_MODE[self._device.color_mode]
243  elif self._device.brightness is not None:
244  color_mode = ColorMode.BRIGHTNESS
245  else:
246  color_mode = ColorMode.ONOFF
247  if color_mode not in self._attr_supported_color_modes:
248  # Some lights controlled by ZigBee scenes can get unsupported color mode
249  return self._attr_color_mode
250  self._attr_color_mode = color_mode
251  return color_mode
252 
253  @property
254  def brightness(self) -> int | None:
255  """Return the brightness of this light between 0..255."""
256  return self._device.brightness
257 
258  @property
259  def color_temp(self) -> int | None:
260  """Return the CT color value."""
261  return self._device.color_temp
262 
263  @property
264  def hs_color(self) -> tuple[float, float] | None:
265  """Return the hs color value."""
266  if (hue := self._device.hue) and (sat := self._device.saturation):
267  return (hue / 65535 * 360, sat / 255 * 100)
268  return None
269 
270  @property
271  def xy_color(self) -> tuple[float, float] | None:
272  """Return the XY color value."""
273  return self._device.xy
274 
275  @property
276  def is_on(self) -> bool | None:
277  """Return true if light is on."""
278  return self._device.state
279 
280  async def async_turn_on(self, **kwargs: Any) -> None:
281  """Turn on light."""
282  data: SetStateAttributes = {"on": True}
283 
284  if ATTR_BRIGHTNESS in kwargs:
285  data["brightness"] = kwargs[ATTR_BRIGHTNESS]
286 
287  if ATTR_COLOR_TEMP in kwargs:
288  data["color_temperature"] = kwargs[ATTR_COLOR_TEMP]
289 
290  if ATTR_HS_COLOR in kwargs:
291  if ColorMode.XY in self._attr_supported_color_modes:
292  data["xy"] = color_hs_to_xy(*kwargs[ATTR_HS_COLOR])
293  else:
294  data["hue"] = int(kwargs[ATTR_HS_COLOR][0] / 360 * 65535)
295  data["saturation"] = int(kwargs[ATTR_HS_COLOR][1] / 100 * 255)
296 
297  if ATTR_XY_COLOR in kwargs:
298  data["xy"] = kwargs[ATTR_XY_COLOR]
299 
300  if ATTR_TRANSITION in kwargs:
301  data["transition_time"] = int(kwargs[ATTR_TRANSITION] * 10)
302  elif "IKEA" in self._device.manufacturer:
303  data["transition_time"] = 0
304 
305  if ATTR_FLASH in kwargs and kwargs[ATTR_FLASH] in FLASH_TO_DECONZ:
306  data["alert"] = FLASH_TO_DECONZ[kwargs[ATTR_FLASH]]
307  del data["on"]
308 
309  if ATTR_EFFECT in kwargs and kwargs[ATTR_EFFECT] in EFFECT_TO_DECONZ:
310  data["effect"] = EFFECT_TO_DECONZ[kwargs[ATTR_EFFECT]]
311 
312  await self.api.set_state(id=self._device.resource_id, **data)
313 
314  async def async_turn_off(self, **kwargs: Any) -> None:
315  """Turn off light."""
316  if not self._device.state:
317  return
318 
319  data: SetStateAttributes = {"on": False}
320 
321  if ATTR_TRANSITION in kwargs:
322  data["brightness"] = 0
323  data["transition_time"] = int(kwargs[ATTR_TRANSITION] * 10)
324 
325  if ATTR_FLASH in kwargs and kwargs[ATTR_FLASH] in FLASH_TO_DECONZ:
326  data["alert"] = FLASH_TO_DECONZ[kwargs[ATTR_FLASH]]
327  del data["on"]
328 
329  await self.api.set_state(id=self._device.resource_id, **data)
330 
331  @property
332  def extra_state_attributes(self) -> dict[str, bool]:
333  """Return the device state attributes."""
334  return {DECONZ_GROUP: isinstance(self._device, Group)}
335 
336 
338  """Representation of a deCONZ light."""
339 
340  @property
341  def max_mireds(self) -> int:
342  """Return the warmest color_temp that this light supports."""
343  return self._device.max_color_temp or super().max_mireds
344 
345  @property
346  def min_mireds(self) -> int:
347  """Return the coldest color_temp that this light supports."""
348  return self._device.min_color_temp or super().min_mireds
349 
350  @callback
351  def async_update_callback(self) -> None:
352  """Light state will also reflect in relevant groups."""
353  super().async_update_callback()
354 
355  if self._device.reachable and "attr" not in self._device.changed_keys:
356  for group in self.hub.api.groups.values():
357  if self._device.resource_id in group.lights:
358  update_color_state(group, [self._device])
359 
360 
362  """Representation of a deCONZ group."""
363 
364  _attr_has_entity_name = True
365 
366  def __init__(self, device: Group, hub: DeconzHub) -> None:
367  """Set up group and create an unique id."""
368  self._unique_id_unique_id = f"{hub.bridgeid}-{device.deconz_id}"
369  super().__init__(device, hub)
370 
371  self._attr_name_attr_name = None
372 
373  @property
374  def unique_id(self) -> str:
375  """Return a unique identifier for this device."""
376  return self._unique_id_unique_id
377 
378  @property
379  def device_info(self) -> DeviceInfo:
380  """Return a device description for device registry."""
381  return DeviceInfo(
382  identifiers={(DECONZ_DOMAIN, self.unique_idunique_id)},
383  manufacturer="Dresden Elektronik",
384  model="deCONZ group",
385  name=self._device.name,
386  via_device=(DECONZ_DOMAIN, self.hub.api.config.bridge_id),
387  )
388 
389  @property
390  def extra_state_attributes(self) -> dict[str, bool]:
391  """Return the device state attributes."""
392  attributes = dict(super().extra_state_attributes)
393  attributes["all_on"] = self._device.all_on
394 
395  return attributes
None __init__(self, Group device, DeconzHub hub)
Definition: light.py:366
dict[str, bool] extra_state_attributes(self)
Definition: light.py:390
bool add(self, _T matcher)
Definition: match.py:185
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
None async_turn_on(self, **Any kwargs)
Definition: light.py:280
None update_color_state(Group group, list[Light] lights, bool override=False)
Definition: light.py:121
tuple[float, float]|None hs_color(self)
Definition: light.py:264
None async_turn_off(self, **Any kwargs)
Definition: light.py:314
None async_setup_entry(HomeAssistant hass, ConfigEntry config_entry, AddEntitiesCallback async_add_entities)
Definition: light.py:140
None __init__(self, _LightDeviceT device, DeconzHub hub)
Definition: light.py:191
dict[str, bool] extra_state_attributes(self)
Definition: light.py:332
tuple[float, float]|None xy_color(self)
Definition: light.py:271
tuple[float, float] color_hs_to_xy(float iH, float iS, GamutType|None Gamut=None)
Definition: color.py:398