Home Assistant Unofficial Reference 2024.12.1
light.py
Go to the documentation of this file.
1 """Support for LimitlessLED bulbs."""
2 
3 from __future__ import annotations
4 
5 from collections.abc import Callable
6 import logging
7 from typing import Any, Concatenate, cast
8 
9 from limitlessled import Color
10 from limitlessled.bridge import Bridge
11 from limitlessled.group import Group
12 from limitlessled.group.dimmer import DimmerGroup
13 from limitlessled.group.rgbw import RgbwGroup
14 from limitlessled.group.rgbww import RgbwwGroup
15 from limitlessled.group.white import WhiteGroup
16 from limitlessled.pipeline import Pipeline
17 from limitlessled.presets import COLORLOOP
18 import voluptuous as vol
19 
21  ATTR_BRIGHTNESS,
22  ATTR_COLOR_TEMP,
23  ATTR_EFFECT,
24  ATTR_FLASH,
25  ATTR_HS_COLOR,
26  ATTR_TRANSITION,
27  EFFECT_COLORLOOP,
28  EFFECT_WHITE,
29  FLASH_LONG,
30  PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA,
31  ColorMode,
32  LightEntity,
33  LightEntityFeature,
34 )
35 from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, CONF_TYPE, STATE_ON
36 from homeassistant.core import HomeAssistant
38 from homeassistant.helpers.entity_platform import AddEntitiesCallback
39 from homeassistant.helpers.restore_state import RestoreEntity
40 from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
41 from homeassistant.util.color import color_hs_to_RGB, color_temperature_mired_to_kelvin
42 
43 _LOGGER = logging.getLogger(__name__)
44 
45 CONF_BRIDGES = "bridges"
46 CONF_GROUPS = "groups"
47 CONF_NUMBER = "number"
48 CONF_VERSION = "version"
49 CONF_FADE = "fade"
50 
51 DEFAULT_LED_TYPE = "rgbw"
52 DEFAULT_PORT = 5987
53 DEFAULT_TRANSITION = 0
54 DEFAULT_VERSION = 6
55 DEFAULT_FADE = False
56 
57 LED_TYPE = ["rgbw", "rgbww", "white", "bridge-led", "dimmer"]
58 
59 EFFECT_NIGHT = "night"
60 
61 MIN_SATURATION = 10
62 
63 WHITE = (0, 0)
64 
65 COLOR_MODES_LIMITLESS_WHITE = {ColorMode.COLOR_TEMP}
66 SUPPORT_LIMITLESSLED_WHITE = LightEntityFeature.EFFECT | LightEntityFeature.TRANSITION
67 COLOR_MODES_LIMITLESS_DIMMER = {ColorMode.BRIGHTNESS}
68 SUPPORT_LIMITLESSLED_DIMMER = LightEntityFeature.TRANSITION
69 COLOR_MODES_LIMITLESS_RGB = {ColorMode.HS}
70 SUPPORT_LIMITLESSLED_RGB = (
71  LightEntityFeature.EFFECT | LightEntityFeature.FLASH | LightEntityFeature.TRANSITION
72 )
73 COLOR_MODES_LIMITLESS_RGBWW = {ColorMode.COLOR_TEMP, ColorMode.HS}
74 SUPPORT_LIMITLESSLED_RGBWW = (
75  LightEntityFeature.EFFECT | LightEntityFeature.FLASH | LightEntityFeature.TRANSITION
76 )
77 
78 PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA.extend(
79  {
80  vol.Required(CONF_BRIDGES): vol.All(
81  cv.ensure_list,
82  [
83  {
84  vol.Required(CONF_HOST): cv.string,
85  vol.Optional(
86  CONF_VERSION, default=DEFAULT_VERSION
87  ): cv.positive_int,
88  vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
89  vol.Required(CONF_GROUPS): vol.All(
90  cv.ensure_list,
91  [
92  {
93  vol.Required(CONF_NAME): cv.string,
94  vol.Optional(
95  CONF_TYPE, default=DEFAULT_LED_TYPE
96  ): vol.In(LED_TYPE),
97  vol.Required(CONF_NUMBER): cv.positive_int,
98  vol.Optional(
99  CONF_FADE, default=DEFAULT_FADE
100  ): cv.boolean,
101  }
102  ],
103  ),
104  }
105  ],
106  )
107  }
108 )
109 
110 
111 def rewrite_legacy(config: ConfigType) -> ConfigType:
112  """Rewrite legacy configuration to new format."""
113  bridges = config.get(CONF_BRIDGES, [config])
114  new_bridges = []
115  for bridge_conf in bridges:
116  groups = []
117  if "groups" in bridge_conf:
118  groups = bridge_conf["groups"]
119  else:
120  _LOGGER.warning("Legacy configuration format detected")
121  for i in range(1, 5):
122  name_key = f"group_{i}_name"
123  if name_key in bridge_conf:
124  groups.append(
125  {
126  "number": i,
127  "type": bridge_conf.get(
128  f"group_{i}_type", DEFAULT_LED_TYPE
129  ),
130  "name": bridge_conf.get(name_key),
131  }
132  )
133  new_bridges.append(
134  {
135  "host": bridge_conf.get(CONF_HOST),
136  "version": bridge_conf.get(CONF_VERSION),
137  "port": bridge_conf.get(CONF_PORT),
138  "groups": groups,
139  }
140  )
141  return {"bridges": new_bridges}
142 
143 
145  hass: HomeAssistant,
146  config: ConfigType,
147  add_entities: AddEntitiesCallback,
148  discovery_info: DiscoveryInfoType | None = None,
149 ) -> None:
150  """Set up the LimitlessLED lights."""
151 
152  # Two legacy configuration formats are supported to maintain backwards
153  # compatibility.
154  config = rewrite_legacy(config)
155 
156  # Use the expanded configuration format.
157  lights = []
158  bridge_conf: dict[str, Any]
159  group_conf: dict[str, Any]
160  for bridge_conf in config[CONF_BRIDGES]:
161  bridge = Bridge(
162  bridge_conf.get(CONF_HOST),
163  port=bridge_conf.get(CONF_PORT, DEFAULT_PORT),
164  version=bridge_conf.get(CONF_VERSION, DEFAULT_VERSION),
165  )
166  for group_conf in bridge_conf[CONF_GROUPS]:
167  group = bridge.add_group(
168  group_conf.get(CONF_NUMBER),
169  group_conf.get(CONF_NAME),
170  group_conf.get(CONF_TYPE, DEFAULT_LED_TYPE),
171  )
172  lights.append(LimitlessLEDGroup(group, {"fade": group_conf[CONF_FADE]}))
173  add_entities(lights)
174 
175 
176 def state[_LimitlessLEDGroupT: LimitlessLEDGroup, **_P](
177  new_state: bool,
178 ) -> Callable[
179  [Callable[Concatenate[_LimitlessLEDGroupT, int, Pipeline, _P], Any]],
180  Callable[Concatenate[_LimitlessLEDGroupT, _P], None],
181 ]:
182  """State decorator.
183 
184  Specify True (turn on) or False (turn off).
185  """
186 
187  def decorator(
188  function: Callable[Concatenate[_LimitlessLEDGroupT, int, Pipeline, _P], Any],
189  ) -> Callable[Concatenate[_LimitlessLEDGroupT, _P], None]:
190  """Set up the decorator function."""
191 
192  def wrapper(
193  self: _LimitlessLEDGroupT, *args: _P.args, **kwargs: _P.kwargs
194  ) -> None:
195  """Wrap a group state change."""
196  pipeline = Pipeline()
197  transition_time = DEFAULT_TRANSITION
198  if self.effect == EFFECT_COLORLOOP:
199  self.group.stop()
200  self._attr_effect = None
201  # Set transition time.
202  if ATTR_TRANSITION in kwargs:
203  transition_time = int(cast(float, kwargs[ATTR_TRANSITION]))
204  # Do group type-specific work.
205  function(self, transition_time, pipeline, *args, **kwargs)
206  # Update state.
207  self._attr_is_on = new_state
208  self.group.enqueue(pipeline)
209  self.schedule_update_ha_state()
210 
211  return wrapper
212 
213  return decorator
214 
215 
217  """Representation of a LimitessLED group."""
218 
219  _attr_assumed_state = True
220  _attr_max_mireds = 370
221  _attr_min_mireds = 154
222  _attr_should_poll = False
223 
224  def __init__(self, group: Group, config: dict[str, Any]) -> None:
225  """Initialize a group."""
226 
227  if isinstance(group, WhiteGroup):
228  self._attr_supported_color_modes_attr_supported_color_modes = COLOR_MODES_LIMITLESS_WHITE
229  self._attr_supported_features_attr_supported_features = SUPPORT_LIMITLESSLED_WHITE
230  self._attr_effect_list_attr_effect_list = [EFFECT_NIGHT]
231  elif isinstance(group, DimmerGroup):
232  self._attr_supported_color_modes_attr_supported_color_modes = COLOR_MODES_LIMITLESS_DIMMER
233  self._attr_supported_features_attr_supported_features = SUPPORT_LIMITLESSLED_DIMMER
234  self._attr_effect_list_attr_effect_list = []
235  elif isinstance(group, RgbwGroup):
236  self._attr_supported_color_modes_attr_supported_color_modes = COLOR_MODES_LIMITLESS_RGB
237  self._attr_supported_features_attr_supported_features = SUPPORT_LIMITLESSLED_RGB
238  self._attr_effect_list_attr_effect_list = [EFFECT_COLORLOOP, EFFECT_NIGHT, EFFECT_WHITE]
239  elif isinstance(group, RgbwwGroup):
240  self._attr_supported_color_modes_attr_supported_color_modes = COLOR_MODES_LIMITLESS_RGBWW
241  self._attr_supported_features_attr_supported_features = SUPPORT_LIMITLESSLED_RGBWW
242  self._attr_effect_list_attr_effect_list = [EFFECT_COLORLOOP, EFFECT_NIGHT, EFFECT_WHITE]
243 
244  self._fixed_color_mode_fixed_color_mode = None
245  if self.supported_color_modessupported_color_modes and len(self.supported_color_modessupported_color_modes) == 1:
246  self._fixed_color_mode_fixed_color_mode = next(iter(self.supported_color_modessupported_color_modes))
247  else:
248  assert self._attr_supported_color_modes_attr_supported_color_modes == {
249  ColorMode.COLOR_TEMP,
250  ColorMode.HS,
251  }
252 
253  self.groupgroup = group
254  self._attr_name_attr_name = group.name
255  self.configconfig = config
256  self._attr_is_on_attr_is_on = False
257 
258  async def async_added_to_hass(self) -> None:
259  """Handle entity about to be added to hass event."""
260  await super().async_added_to_hass()
261  if last_state := await self.async_get_last_stateasync_get_last_state():
262  self._attr_is_on_attr_is_on = last_state.state == STATE_ON
263  self._attr_brightness_attr_brightness = last_state.attributes.get("brightness")
264  self._attr_color_temp_attr_color_temp = last_state.attributes.get("color_temp")
265  self._attr_hs_color_attr_hs_color = last_state.attributes.get("hs_color")
266 
267  @property
268  def brightness(self) -> int | None:
269  """Return the brightness property."""
270  if self.effecteffecteffect == EFFECT_NIGHT:
271  return 1
272 
273  return self._attr_brightness_attr_brightness
274 
275  @property
276  def color_mode(self) -> str | None:
277  """Return the color mode of the light."""
278  if self._fixed_color_mode_fixed_color_mode:
279  return self._fixed_color_mode_fixed_color_mode
280 
281  # The light supports both hs and white with adjustable color temperature
282  if (
283  self.effecteffecteffect == EFFECT_NIGHT
284  or self.hs_colorhs_color is None
285  or self.hs_colorhs_color[1] == 0
286  ):
287  return ColorMode.COLOR_TEMP
288  return ColorMode.HS
289 
290  @state(False)
291  def turn_off(self, transition_time: int, pipeline: Pipeline, **kwargs: Any) -> None:
292  """Turn off a group."""
293  if self.configconfig[CONF_FADE]:
294  pipeline.transition(transition_time, brightness=0.0)
295  pipeline.off()
296 
297  @state(True)
298  def turn_on(self, transition_time: int, pipeline: Pipeline, **kwargs: Any) -> None:
299  """Turn on (or adjust property of) a group."""
300  # The night effect does not need a turned on light
301  if kwargs.get(ATTR_EFFECT) == EFFECT_NIGHT:
302  if self.effect_listeffect_list and EFFECT_NIGHT in self.effect_listeffect_list:
303  pipeline.night_light()
304  self._attr_effect_attr_effect = EFFECT_NIGHT
305  return
306 
307  pipeline.on()
308 
309  # Set up transition.
310  args = {}
311  if self.configconfig[CONF_FADE] and not self.is_onis_on and self.brightnessbrightnessbrightness:
312  args["brightness"] = self.limitlessled_brightnesslimitlessled_brightness()
313 
314  if ATTR_BRIGHTNESS in kwargs:
315  self._attr_brightness_attr_brightness = kwargs[ATTR_BRIGHTNESS]
316  args["brightness"] = self.limitlessled_brightnesslimitlessled_brightness()
317 
318  if ATTR_HS_COLOR in kwargs:
319  self._attr_hs_color_attr_hs_color = kwargs[ATTR_HS_COLOR]
320  # White is a special case.
321  assert self.hs_colorhs_color is not None
322  if self.hs_colorhs_color[1] < MIN_SATURATION:
323  pipeline.white()
324  self._attr_hs_color_attr_hs_color = WHITE
325  else:
326  args["color"] = self.limitlessled_colorlimitlessled_color()
327 
328  if ATTR_COLOR_TEMP in kwargs:
329  assert self.supported_color_modessupported_color_modes
330  if ColorMode.HS in self.supported_color_modessupported_color_modes:
331  pipeline.white()
332  self._attr_hs_color_attr_hs_color = WHITE
333  self._attr_color_temp_attr_color_temp = kwargs[ATTR_COLOR_TEMP]
334  args["temperature"] = self.limitlessled_temperaturelimitlessled_temperature()
335 
336  if args:
337  pipeline.transition(transition_time, **args)
338 
339  # Flash.
340  if ATTR_FLASH in kwargs and self.supported_featuressupported_featuressupported_features & LightEntityFeature.FLASH:
341  duration = 0
342  if kwargs[ATTR_FLASH] == FLASH_LONG:
343  duration = 1
344  pipeline.flash(duration=duration)
345 
346  # Add effects.
347  if ATTR_EFFECT in kwargs and self.effect_listeffect_list:
348  if kwargs[ATTR_EFFECT] == EFFECT_COLORLOOP:
349  self._attr_effect_attr_effect = EFFECT_COLORLOOP
350  pipeline.append(COLORLOOP)
351  if kwargs[ATTR_EFFECT] == EFFECT_WHITE:
352  pipeline.white()
353  self._attr_hs_color_attr_hs_color = WHITE
354 
355  def limitlessled_temperature(self) -> float:
356  """Convert Home Assistant color temperature units to percentage."""
357  max_kelvin = color_temperature_mired_to_kelvin(self.min_miredsmin_mireds)
358  min_kelvin = color_temperature_mired_to_kelvin(self.max_miredsmax_mireds)
359  width = max_kelvin - min_kelvin
360  assert self.color_tempcolor_temp is not None
361  kelvin = color_temperature_mired_to_kelvin(self.color_tempcolor_temp)
362  temperature = (kelvin - min_kelvin) / width
363  return max(0, min(1, temperature))
364 
365  def limitlessled_brightness(self) -> float:
366  """Convert Home Assistant brightness units to percentage."""
367  assert self.brightnessbrightnessbrightness is not None
368  return self.brightnessbrightnessbrightness / 255
369 
370  def limitlessled_color(self) -> Color:
371  """Convert Home Assistant HS list to RGB Color tuple."""
372  assert self.hs_colorhs_color is not None
373  return Color(*color_hs_to_RGB(*self.hs_colorhs_color))
set[ColorMode]|set[str]|None supported_color_modes(self)
Definition: __init__.py:1302
tuple[float, float]|None hs_color(self)
Definition: __init__.py:947
LightEntityFeature supported_features(self)
Definition: __init__.py:1307
None __init__(self, Group group, dict[str, Any] config)
Definition: light.py:224
None turn_off(self, int transition_time, Pipeline pipeline, **Any kwargs)
Definition: light.py:291
None turn_on(self, int transition_time, Pipeline pipeline, **Any kwargs)
Definition: light.py:298
int|None supported_features(self)
Definition: entity.py:861
None add_entities(AsusWrtRouter router, AddEntitiesCallback async_add_entities, set[str] tracked)
None setup_platform(HomeAssistant hass, ConfigType config, AddEntitiesCallback add_entities, DiscoveryInfoType|None discovery_info=None)
Definition: light.py:149
ConfigType rewrite_legacy(ConfigType config)
Definition: light.py:111
bool state(HomeAssistant hass, str|State|None entity, Any req_state, timedelta|None for_period=None, str|None attribute=None, TemplateVarsType variables=None)
Definition: condition.py:551
int color_temperature_mired_to_kelvin(float mired_temperature)
Definition: color.py:631
tuple[int, int, int] color_hs_to_RGB(float iH, float iS)
Definition: color.py:383