Home Assistant Unofficial Reference 2024.12.1
light.py
Go to the documentation of this file.
1 """Support for Osram Lightify."""
2 
3 from __future__ import annotations
4 
5 import logging
6 import random
7 from typing import Any
8 
9 from lightify import Lightify
10 import voluptuous as vol
11 
13  ATTR_BRIGHTNESS,
14  ATTR_COLOR_TEMP,
15  ATTR_EFFECT,
16  ATTR_HS_COLOR,
17  ATTR_TRANSITION,
18  EFFECT_RANDOM,
19  PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA,
20  ColorMode,
21  LightEntity,
22  LightEntityFeature,
23  brightness_supported,
24 )
25 from homeassistant.const import CONF_HOST
26 from homeassistant.core import HomeAssistant
28 from homeassistant.helpers.entity_platform import AddEntitiesCallback
29 from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
30 import homeassistant.util.color as color_util
31 
32 _LOGGER = logging.getLogger(__name__)
33 
34 CONF_ALLOW_LIGHTIFY_NODES = "allow_lightify_nodes"
35 CONF_ALLOW_LIGHTIFY_GROUPS = "allow_lightify_groups"
36 CONF_ALLOW_LIGHTIFY_SENSORS = "allow_lightify_sensors"
37 CONF_ALLOW_LIGHTIFY_SWITCHES = "allow_lightify_switches"
38 CONF_INTERVAL_LIGHTIFY_STATUS = "interval_lightify_status"
39 CONF_INTERVAL_LIGHTIFY_CONF = "interval_lightify_conf"
40 
41 DEFAULT_ALLOW_LIGHTIFY_NODES = True
42 DEFAULT_ALLOW_LIGHTIFY_GROUPS = True
43 DEFAULT_ALLOW_LIGHTIFY_SENSORS = True
44 DEFAULT_ALLOW_LIGHTIFY_SWITCHES = True
45 DEFAULT_INTERVAL_LIGHTIFY_STATUS = 5
46 DEFAULT_INTERVAL_LIGHTIFY_CONF = 3600
47 
48 PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA.extend(
49  {
50  vol.Required(CONF_HOST): cv.string,
51  vol.Optional(
52  CONF_ALLOW_LIGHTIFY_NODES, default=DEFAULT_ALLOW_LIGHTIFY_NODES
53  ): cv.boolean,
54  vol.Optional(
55  CONF_ALLOW_LIGHTIFY_GROUPS, default=DEFAULT_ALLOW_LIGHTIFY_GROUPS
56  ): cv.boolean,
57  vol.Optional(
58  CONF_ALLOW_LIGHTIFY_SENSORS, default=DEFAULT_ALLOW_LIGHTIFY_SENSORS
59  ): cv.boolean,
60  vol.Optional(
61  CONF_ALLOW_LIGHTIFY_SWITCHES, default=DEFAULT_ALLOW_LIGHTIFY_SWITCHES
62  ): cv.boolean,
63  vol.Optional(
64  CONF_INTERVAL_LIGHTIFY_STATUS, default=DEFAULT_INTERVAL_LIGHTIFY_STATUS
65  ): cv.positive_int,
66  vol.Optional(
67  CONF_INTERVAL_LIGHTIFY_CONF, default=DEFAULT_INTERVAL_LIGHTIFY_CONF
68  ): cv.positive_int,
69  }
70 )
71 
72 DEFAULT_BRIGHTNESS = 2
73 DEFAULT_KELVIN = 2700
74 
75 
77  hass: HomeAssistant,
78  config: ConfigType,
79  add_entities: AddEntitiesCallback,
80  discovery_info: DiscoveryInfoType | None = None,
81 ) -> None:
82  """Set up the Osram Lightify lights."""
83  host = config[CONF_HOST]
84  try:
85  bridge = Lightify(host, log_level=logging.NOTSET)
86  except OSError:
87  _LOGGER.exception("Error connecting to bridge %s", host)
88  return
89 
90  setup_bridge(bridge, add_entities, config)
91 
92 
93 def setup_bridge(bridge, add_entities, config):
94  """Set up the Lightify bridge."""
95  lights = {}
96  groups = {}
97  groups_last_updated = [0]
98 
99  def update_lights():
100  """Update the lights objects with the latest info from the bridge."""
101  try:
102  new_lights = bridge.update_all_light_status(
103  config[CONF_INTERVAL_LIGHTIFY_STATUS]
104  )
105  lights_changed = bridge.lights_changed()
106  except TimeoutError:
107  _LOGGER.error("Timeout during updating of lights")
108  return 0
109  except OSError:
110  _LOGGER.error("OSError during updating of lights")
111  return 0
112 
113  if new_lights and config[CONF_ALLOW_LIGHTIFY_NODES]:
114  new_entities = []
115  for addr, light in new_lights.items():
116  if (
117  light.devicetype().name == "SENSOR"
118  and not config[CONF_ALLOW_LIGHTIFY_SENSORS]
119  ) or (
120  light.devicetype().name == "SWITCH"
121  and not config[CONF_ALLOW_LIGHTIFY_SWITCHES]
122  ):
123  continue
124 
125  if addr not in lights:
126  osram_light = OsramLightifyLight(
127  light, update_lights, lights_changed
128  )
129  lights[addr] = osram_light
130  new_entities.append(osram_light)
131  else:
132  lights[addr].update_luminary(light)
133 
134  add_entities(new_entities)
135 
136  return lights_changed
137 
138  def update_groups():
139  """Update the groups objects with the latest info from the bridge."""
140  lights_changed = update_lights()
141 
142  try:
143  bridge.update_scene_list(config[CONF_INTERVAL_LIGHTIFY_CONF])
144  new_groups = bridge.update_group_list(config[CONF_INTERVAL_LIGHTIFY_CONF])
145  groups_updated = bridge.groups_updated()
146  except TimeoutError:
147  _LOGGER.error("Timeout during updating of scenes/groups")
148  return 0
149  except OSError:
150  _LOGGER.error("OSError during updating of scenes/groups")
151  return 0
152 
153  if new_groups:
154  new_groups = {group.idx(): group for group in new_groups.values()}
155  new_entities = []
156  for idx, group in new_groups.items():
157  if idx not in groups:
158  osram_group = OsramLightifyGroup(
159  group, update_groups, groups_updated
160  )
161  groups[idx] = osram_group
162  new_entities.append(osram_group)
163  else:
164  groups[idx].update_luminary(group)
165 
166  add_entities(new_entities)
167 
168  if groups_updated > groups_last_updated[0]:
169  groups_last_updated[0] = groups_updated
170  for idx, osram_group in groups.items():
171  if idx not in new_groups:
172  osram_group.update_static_attributes()
173 
174  return max(lights_changed, groups_updated)
175 
176  update_lights()
177  if config[CONF_ALLOW_LIGHTIFY_GROUPS]:
178  update_groups()
179 
180 
182  """Representation of Luminary Lights and Groups."""
183 
184  def __init__(self, luminary, update_func, changed):
185  """Initialize a Luminary Light."""
186  self.update_funcupdate_func = update_func
187  self._luminary_luminary = luminary
188  self._changed_changed = changed
189 
190  self._unique_id_unique_id = None
191  self._effect_list_effect_list = []
192  self._is_on_is_on = False
193  self._available_available = True
194  self._min_mireds_min_mireds = None
195  self._max_mireds_max_mireds = None
196  self._brightness_brightness = None
197  self._color_temp_color_temp = None
198  self._rgb_color_rgb_color = None
199  self._device_attributes_device_attributes = None
200 
201  self.update_static_attributesupdate_static_attributes()
202  self.update_dynamic_attributesupdate_dynamic_attributes()
203 
204  def _get_unique_id(self):
205  """Get a unique ID (not implemented)."""
206  raise NotImplementedError
207 
208  def _get_supported_color_modes(self) -> set[ColorMode]:
209  """Get supported color modes."""
210  color_modes: set[ColorMode] = set()
211  if "temp" in self._luminary_luminary.supported_features():
212  color_modes.add(ColorMode.COLOR_TEMP)
213 
214  if "rgb" in self._luminary_luminary.supported_features():
215  color_modes.add(ColorMode.HS)
216 
217  if not color_modes and "lum" in self._luminary_luminary.supported_features():
218  color_modes.add(ColorMode.BRIGHTNESS)
219 
220  if not color_modes:
221  color_modes.add(ColorMode.ONOFF)
222 
223  return color_modes
224 
225  def _get_supported_features(self) -> LightEntityFeature:
226  """Get list of supported features."""
227  features = LightEntityFeature(0)
228  if "lum" in self._luminary_luminary.supported_features():
229  features = features | LightEntityFeature.TRANSITION
230 
231  if "temp" in self._luminary_luminary.supported_features():
232  features = features | LightEntityFeature.TRANSITION
233 
234  if "rgb" in self._luminary_luminary.supported_features():
235  features = (
236  features | LightEntityFeature.TRANSITION | LightEntityFeature.EFFECT
237  )
238 
239  return features
240 
241  def _get_effect_list(self):
242  """Get list of supported effects."""
243  effects = []
244  if "rgb" in self._luminary_luminary.supported_features():
245  effects.append(EFFECT_RANDOM)
246 
247  return effects
248 
249  @property
250  def name(self):
251  """Return the name of the luminary."""
252  return self._luminary_luminary.name()
253 
254  @property
255  def hs_color(self):
256  """Return last hs color value set."""
257  return color_util.color_RGB_to_hs(*self._rgb_color_rgb_color)
258 
259  @property
260  def color_temp(self):
261  """Return the color temperature."""
262  return self._color_temp_color_temp
263 
264  @property
265  def brightness(self):
266  """Return brightness of the luminary (0..255)."""
267  return self._brightness_brightness
268 
269  @property
270  def is_on(self):
271  """Return True if the device is on."""
272  return self._is_on_is_on
273 
274  @property
275  def effect_list(self):
276  """List of supported effects."""
277  return self._effect_list_effect_list
278 
279  @property
280  def min_mireds(self):
281  """Return the coldest color_temp that this light supports."""
282  return self._min_mireds_min_mireds
283 
284  @property
285  def max_mireds(self):
286  """Return the warmest color_temp that this light supports."""
287  return self._max_mireds_max_mireds
288 
289  @property
290  def unique_id(self):
291  """Return a unique ID."""
292  return self._unique_id_unique_id
293 
294  @property
296  """Return device specific state attributes."""
297  return self._device_attributes_device_attributes
298 
299  @property
300  def available(self):
301  """Return True if entity is available."""
302  return self._available_available
303 
304  def play_effect(self, effect, transition):
305  """Play selected effect."""
306  if effect == EFFECT_RANDOM:
307  self._rgb_color_rgb_color = (
308  random.randrange(0, 256),
309  random.randrange(0, 256),
310  random.randrange(0, 256),
311  )
312  self._luminary_luminary.set_rgb(*self._rgb_color_rgb_color, transition)
313  self._luminary_luminary.set_onoff(True)
314  return True
315 
316  return False
317 
318  def turn_on(self, **kwargs: Any) -> None:
319  """Turn the device on."""
320  transition = int(kwargs.get(ATTR_TRANSITION, 0) * 10)
321  if ATTR_EFFECT in kwargs:
322  self.play_effectplay_effect(kwargs[ATTR_EFFECT], transition)
323  return
324 
325  if ATTR_HS_COLOR in kwargs:
326  self._rgb_color_rgb_color = color_util.color_hs_to_RGB(*kwargs[ATTR_HS_COLOR])
327  self._luminary_luminary.set_rgb(*self._rgb_color_rgb_color, transition)
328 
329  if ATTR_COLOR_TEMP in kwargs:
330  self._color_temp_color_temp = kwargs[ATTR_COLOR_TEMP]
331  self._luminary_luminary.set_temperature(
332  int(color_util.color_temperature_mired_to_kelvin(self._color_temp_color_temp)),
333  transition,
334  )
335 
336  self._is_on_is_on = True
337  if ATTR_BRIGHTNESS in kwargs:
338  self._brightness_brightness = kwargs[ATTR_BRIGHTNESS]
339  self._luminary_luminary.set_luminance(int(self._brightness_brightness / 2.55), transition)
340  else:
341  self._luminary_luminary.set_onoff(True)
342 
343  def turn_off(self, **kwargs: Any) -> None:
344  """Turn the device off."""
345  self._is_on_is_on = False
346  if ATTR_TRANSITION in kwargs:
347  transition = int(kwargs[ATTR_TRANSITION] * 10)
348  self._brightness_brightness = DEFAULT_BRIGHTNESS
349  self._luminary_luminary.set_luminance(0, transition)
350  else:
351  self._luminary_luminary.set_onoff(False)
352 
353  def update_luminary(self, luminary):
354  """Update internal luminary object."""
355  self._luminary_luminary = luminary
356  self.update_static_attributesupdate_static_attributes()
357 
358  def update_static_attributes(self) -> None:
359  """Update static attributes of the luminary."""
360  self._unique_id_unique_id = self._get_unique_id_get_unique_id()
361  self._attr_supported_color_modes_attr_supported_color_modes = self._get_supported_color_modes_get_supported_color_modes()
362  self._attr_supported_features_attr_supported_features = self._get_supported_features_get_supported_features()
363  self._effect_list_effect_list = self._get_effect_list_get_effect_list()
364  if ColorMode.COLOR_TEMP in self._attr_supported_color_modes_attr_supported_color_modes:
365  self._min_mireds_min_mireds = color_util.color_temperature_kelvin_to_mired(
366  self._luminary_luminary.max_temp() or DEFAULT_KELVIN
367  )
368  self._max_mireds_max_mireds = color_util.color_temperature_kelvin_to_mired(
369  self._luminary_luminary.min_temp() or DEFAULT_KELVIN
370  )
371  if len(self._attr_supported_color_modes_attr_supported_color_modes) == 1:
372  # The light supports only a single color mode
373  self._attr_color_mode_attr_color_mode = list(self._attr_supported_color_modes_attr_supported_color_modes)[0]
374 
376  """Update dynamic attributes of the luminary."""
377  self._is_on_is_on = self._luminary_luminary.on()
378  self._available_available = self._luminary_luminary.reachable() and not self._luminary_luminary.deleted()
379  if brightness_supported(self._attr_supported_color_modes_attr_supported_color_modes):
380  self._brightness_brightness = int(self._luminary_luminary.lum() * 2.55)
381 
382  if ColorMode.COLOR_TEMP in self._attr_supported_color_modes_attr_supported_color_modes:
383  self._color_temp_color_temp = color_util.color_temperature_kelvin_to_mired(
384  self._luminary_luminary.temp() or DEFAULT_KELVIN
385  )
386 
387  if ColorMode.HS in self._attr_supported_color_modes_attr_supported_color_modes:
388  self._rgb_color_rgb_color = self._luminary_luminary.rgb()
389 
390  if len(self._attr_supported_color_modes_attr_supported_color_modes) > 1:
391  # The light supports hs + color temp, determine which one it is
392  if self._rgb_color_rgb_color == (0, 0, 0):
393  self._attr_color_mode_attr_color_mode = ColorMode.COLOR_TEMP
394  else:
395  self._attr_color_mode_attr_color_mode = ColorMode.HS
396 
397  def update(self) -> None:
398  """Synchronize state with bridge."""
399  changed = self.update_funcupdate_func()
400  if changed > self._changed_changed:
401  self._changed_changed = changed
402  self.update_dynamic_attributesupdate_dynamic_attributes()
403 
404 
406  """Representation of an Osram Lightify Light."""
407 
408  def _get_unique_id(self):
409  """Get a unique ID."""
410  return self._luminary_luminary.addr()
411 
413  """Update static attributes of the luminary."""
414  super().update_static_attributes()
415  attrs = {
416  "device_type": (
417  f"{self._luminary.type_id()} ({self._luminary.devicename()})"
418  ),
419  "firmware_version": self._luminary_luminary.version(),
420  }
421  if self._luminary_luminary.devicetype().name == "SENSOR":
422  attrs["sensor_values"] = self._luminary_luminary.raw_values()
423 
424  self._device_attributes_device_attributes_device_attributes = attrs
425 
426 
428  """Representation of an Osram Lightify Group."""
429 
430  def _get_unique_id(self):
431  """Get a unique ID for the group."""
432  # Actually, it's a wrong choice for a unique ID, because a combination of
433  # lights is NOT unique (Osram Lightify allows to create different groups
434  # with the same lights). Also a combination of lights may easily change,
435  # but the group remains the same from the user's perspective.
436  # It should be something like "<gateway host>-<group.idx()>"
437  # For now keeping it as is for backward compatibility with existing
438  # users.
439  return f"{self._luminary.lights()}"
440 
441  def _get_supported_features(self) -> LightEntityFeature:
442  """Get list of supported features."""
443  features = super()._get_supported_features()
444  if self._luminary_luminary.scenes():
445  features |= LightEntityFeature.EFFECT
446 
447  return features
448 
449  def _get_effect_list(self):
450  """Get list of supported effects."""
451  effects = super()._get_effect_list()
452  effects.extend(self._luminary_luminary.scenes())
453  return sorted(effects)
454 
455  def play_effect(self, effect, transition):
456  """Play selected effect."""
457  if super().play_effect(effect, transition):
458  return True
459 
460  if effect in self._luminary_luminary.scenes():
461  self._luminary_luminary.activate_scene(effect)
462  return True
463 
464  return False
465 
467  """Update static attributes of the luminary."""
468  super().update_static_attributes()
469  self._device_attributes_device_attributes_device_attributes = {"lights": self._luminary_luminary.light_names()}
LightEntityFeature supported_features(self)
Definition: __init__.py:1307
LightEntityFeature _get_supported_features(self)
Definition: light.py:225
def __init__(self, luminary, update_func, changed)
Definition: light.py:184
def play_effect(self, effect, transition)
Definition: light.py:304
None add_entities(AsusWrtRouter router, AddEntitiesCallback async_add_entities, set[str] tracked)
bool brightness_supported(Iterable[ColorMode|str]|None color_modes)
Definition: __init__.py:155
None setup_platform(HomeAssistant hass, ConfigType config, AddEntitiesCallback add_entities, DiscoveryInfoType|None discovery_info=None)
Definition: light.py:81
def setup_bridge(bridge, add_entities, config)
Definition: light.py:93