Home Assistant Unofficial Reference 2024.12.1
light.py
Go to the documentation of this file.
1 """Support for Magic Home lights."""
2 
3 from __future__ import annotations
4 
5 import ast
6 import logging
7 from typing import Any, Final
8 
9 from flux_led.const import MultiColorEffects
10 from flux_led.protocol import MusicMode
11 from flux_led.utils import rgbcw_brightness, rgbcw_to_rgbwc, rgbw_brightness
12 import voluptuous as vol
13 
14 from homeassistant import config_entries
16  ATTR_BRIGHTNESS,
17  ATTR_COLOR_TEMP,
18  ATTR_EFFECT,
19  ATTR_RGB_COLOR,
20  ATTR_RGBW_COLOR,
21  ATTR_RGBWW_COLOR,
22  ATTR_WHITE,
23  LightEntity,
24  LightEntityFeature,
25 )
26 from homeassistant.const import CONF_EFFECT
27 from homeassistant.core import HomeAssistant, callback
28 from homeassistant.helpers import entity_platform
30 from homeassistant.helpers.entity_platform import AddEntitiesCallback
31 from homeassistant.helpers.typing import VolDictType
32 from homeassistant.helpers.update_coordinator import CoordinatorEntity
33 from homeassistant.util.color import (
34  color_temperature_kelvin_to_mired,
35  color_temperature_mired_to_kelvin,
36 )
37 
38 from .const import (
39  CONF_COLORS,
40  CONF_CUSTOM_EFFECT_COLORS,
41  CONF_CUSTOM_EFFECT_SPEED_PCT,
42  CONF_CUSTOM_EFFECT_TRANSITION,
43  CONF_SPEED_PCT,
44  CONF_TRANSITION,
45  DEFAULT_EFFECT_SPEED,
46  DOMAIN,
47  MIN_CCT_BRIGHTNESS,
48  MIN_RGB_BRIGHTNESS,
49  MULTI_BRIGHTNESS_COLOR_MODES,
50  TRANSITION_GRADUAL,
51  TRANSITION_JUMP,
52  TRANSITION_STROBE,
53 )
54 from .coordinator import FluxLedUpdateCoordinator
55 from .entity import FluxOnOffEntity
56 from .util import (
57  _effect_brightness,
58  _flux_color_mode_to_hass,
59  _hass_color_modes,
60  _min_rgb_brightness,
61  _min_rgbw_brightness,
62  _min_rgbwc_brightness,
63  _str_to_multi_color_effect,
64 )
65 
66 _LOGGER = logging.getLogger(__name__)
67 
68 MODE_ATTRS = {
69  ATTR_EFFECT,
70  ATTR_COLOR_TEMP,
71  ATTR_RGB_COLOR,
72  ATTR_RGBW_COLOR,
73  ATTR_RGBWW_COLOR,
74  ATTR_WHITE,
75 }
76 
77 ATTR_FOREGROUND_COLOR: Final = "foreground_color"
78 ATTR_BACKGROUND_COLOR: Final = "background_color"
79 ATTR_SENSITIVITY: Final = "sensitivity"
80 ATTR_LIGHT_SCREEN: Final = "light_screen"
81 
82 # Constant color temp values for 2 flux_led special modes
83 # Warm-white and Cool-white modes
84 COLOR_TEMP_WARM_VS_COLD_WHITE_CUT_OFF: Final = 285
85 
86 EFFECT_CUSTOM: Final = "custom"
87 
88 SERVICE_CUSTOM_EFFECT: Final = "set_custom_effect"
89 SERVICE_SET_ZONES: Final = "set_zones"
90 SERVICE_SET_MUSIC_MODE: Final = "set_music_mode"
91 
92 CUSTOM_EFFECT_DICT: VolDictType = {
93  vol.Required(CONF_COLORS): vol.All(
94  cv.ensure_list,
95  vol.Length(min=1, max=16),
96  [vol.All(vol.Coerce(tuple), vol.ExactSequence((cv.byte, cv.byte, cv.byte)))],
97  ),
98  vol.Optional(CONF_SPEED_PCT, default=50): vol.All(
99  vol.Coerce(int), vol.Range(min=0, max=100)
100  ),
101  vol.Optional(CONF_TRANSITION, default=TRANSITION_GRADUAL): vol.All(
102  cv.string, vol.In([TRANSITION_GRADUAL, TRANSITION_JUMP, TRANSITION_STROBE])
103  ),
104 }
105 
106 SET_MUSIC_MODE_DICT: VolDictType = {
107  vol.Optional(ATTR_SENSITIVITY, default=100): vol.All(
108  vol.Coerce(int), vol.Range(min=0, max=100)
109  ),
110  vol.Optional(ATTR_BRIGHTNESS, default=100): vol.All(
111  vol.Coerce(int), vol.Range(min=0, max=100)
112  ),
113  vol.Optional(ATTR_EFFECT, default=1): vol.All(
114  vol.Coerce(int), vol.Range(min=0, max=16)
115  ),
116  vol.Optional(ATTR_LIGHT_SCREEN, default=False): bool,
117  vol.Optional(ATTR_FOREGROUND_COLOR): vol.All(
118  vol.Coerce(tuple), vol.ExactSequence((cv.byte,) * 3)
119  ),
120  vol.Optional(ATTR_BACKGROUND_COLOR): vol.All(
121  vol.Coerce(tuple), vol.ExactSequence((cv.byte,) * 3)
122  ),
123 }
124 
125 SET_ZONES_DICT: VolDictType = {
126  vol.Required(CONF_COLORS): vol.All(
127  cv.ensure_list,
128  vol.Length(min=1, max=2048),
129  [vol.All(vol.Coerce(tuple), vol.ExactSequence((cv.byte, cv.byte, cv.byte)))],
130  ),
131  vol.Optional(CONF_SPEED_PCT, default=50): vol.All(
132  vol.Coerce(int), vol.Range(min=0, max=100)
133  ),
134  vol.Optional(CONF_EFFECT, default=MultiColorEffects.STATIC.name.lower()): vol.All(
135  cv.string, vol.In([effect.name.lower() for effect in MultiColorEffects])
136  ),
137 }
138 
139 
141  hass: HomeAssistant,
143  async_add_entities: AddEntitiesCallback,
144 ) -> None:
145  """Set up the Flux lights."""
146  coordinator: FluxLedUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
147 
148  platform = entity_platform.async_get_current_platform()
149  platform.async_register_entity_service(
150  SERVICE_CUSTOM_EFFECT,
151  CUSTOM_EFFECT_DICT,
152  "async_set_custom_effect",
153  )
154  platform.async_register_entity_service(
155  SERVICE_SET_ZONES,
156  SET_ZONES_DICT,
157  "async_set_zones",
158  )
159  platform.async_register_entity_service(
160  SERVICE_SET_MUSIC_MODE,
161  SET_MUSIC_MODE_DICT,
162  "async_set_music_mode",
163  )
164  options = entry.options
165 
166  try:
167  custom_effect_colors = ast.literal_eval(
168  options.get(CONF_CUSTOM_EFFECT_COLORS) or "[]"
169  )
170  except (ValueError, TypeError, SyntaxError, MemoryError) as ex:
171  _LOGGER.warning(
172  "Could not parse custom effect colors for %s: %s", entry.unique_id, ex
173  )
174  custom_effect_colors = []
175 
177  [
178  FluxLight(
179  coordinator,
180  entry.unique_id or entry.entry_id,
181  list(custom_effect_colors),
182  options.get(CONF_CUSTOM_EFFECT_SPEED_PCT, DEFAULT_EFFECT_SPEED),
183  options.get(CONF_CUSTOM_EFFECT_TRANSITION, TRANSITION_GRADUAL),
184  )
185  ]
186  )
187 
188 
189 class FluxLight(
190  FluxOnOffEntity, CoordinatorEntity[FluxLedUpdateCoordinator], LightEntity
191 ):
192  """Representation of a Flux light."""
193 
194  _attr_name = None
195 
196  _attr_supported_features = LightEntityFeature.TRANSITION | LightEntityFeature.EFFECT
197 
198  def __init__(
199  self,
200  coordinator: FluxLedUpdateCoordinator,
201  base_unique_id: str,
202  custom_effect_colors: list[tuple[int, int, int]],
203  custom_effect_speed_pct: int,
204  custom_effect_transition: str,
205  ) -> None:
206  """Initialize the light."""
207  super().__init__(coordinator, base_unique_id, None)
208  self._attr_min_mireds_attr_min_mireds = color_temperature_kelvin_to_mired(self._device.max_temp)
209  self._attr_max_mireds_attr_max_mireds = color_temperature_kelvin_to_mired(self._device.min_temp)
210  self._attr_supported_color_modes_attr_supported_color_modes = _hass_color_modes(self._device)
211  custom_effects: list[str] = []
212  if custom_effect_colors:
213  custom_effects.append(EFFECT_CUSTOM)
214  self._attr_effect_list_attr_effect_list = [*self._device.effect_list, *custom_effects]
215  self._custom_effect_colors_custom_effect_colors = custom_effect_colors
216  self._custom_effect_speed_pct_custom_effect_speed_pct = custom_effect_speed_pct
217  self._custom_effect_transition_custom_effect_transition = custom_effect_transition
218 
219  @property
220  def brightness(self) -> int:
221  """Return the brightness of this light between 0..255."""
222  return self._device.brightness
223 
224  @property
225  def color_temp(self) -> int:
226  """Return the kelvin value of this light in mired."""
227  return color_temperature_kelvin_to_mired(self._device.color_temp)
228 
229  @property
230  def rgb_color(self) -> tuple[int, int, int]:
231  """Return the rgb color value."""
232  return self._device.rgb_unscaled
233 
234  @property
235  def rgbw_color(self) -> tuple[int, int, int, int]:
236  """Return the rgbw color value."""
237  return self._device.rgbw
238 
239  @property
240  def rgbww_color(self) -> tuple[int, int, int, int, int]:
241  """Return the rgbww aka rgbcw color value."""
242  return self._device.rgbcw
243 
244  @property
245  def color_mode(self) -> str:
246  """Return the color mode of the light."""
248  self._device.color_mode, self._device.color_modes
249  )
250 
251  @property
252  def effect(self) -> str | None:
253  """Return the current effect."""
254  return self._device.effect
255 
256  async def _async_turn_on(self, **kwargs: Any) -> None:
257  """Turn the specified or all lights on."""
258  if self._device.requires_turn_on or not kwargs:
259  if not self.is_onis_onis_on:
260  await self._device.async_turn_on()
261  if not kwargs:
262  return
263 
264  if MODE_ATTRS.intersection(kwargs):
265  await self._async_set_mode_async_set_mode(**kwargs)
266  return
267  await self._device.async_set_brightness(self._async_brightness_async_brightness(**kwargs))
268 
269  async def _async_set_effect(self, effect: str, brightness: int) -> None:
270  """Set an effect."""
271  # Custom effect
272  if effect == EFFECT_CUSTOM:
273  if self._custom_effect_colors_custom_effect_colors:
274  await self._device.async_set_custom_pattern(
275  self._custom_effect_colors_custom_effect_colors,
276  self._custom_effect_speed_pct_custom_effect_speed_pct,
277  self._custom_effect_transition_custom_effect_transition,
278  )
279  return
280  await self._device.async_set_effect(
281  effect,
282  self._device.speed or DEFAULT_EFFECT_SPEED,
283  _effect_brightness(brightness),
284  )
285 
286  @callback
287  def _async_brightness(self, **kwargs: Any) -> int:
288  """Determine brightness from kwargs or current value."""
289  if (brightness := kwargs.get(ATTR_BRIGHTNESS)) is None:
290  brightness = self.brightnessbrightnessbrightness
291  # If the brightness was previously 0, the light
292  # will not turn on unless brightness is at least 1
293  #
294  # We previously had a problem with the brightness
295  # sometimes reporting as 0 when an effect was in progress,
296  # however this has since been resolved in the upstream library
297  return max(MIN_RGB_BRIGHTNESS, brightness)
298 
299  async def _async_set_mode(self, **kwargs: Any) -> None:
300  """Set an effect or color mode."""
301  brightness = self._async_brightness_async_brightness(**kwargs)
302  # Handle switch to Effect Mode
303  if effect := kwargs.get(ATTR_EFFECT):
304  await self._async_set_effect_async_set_effect(effect, brightness)
305  return
306  # Handle switch to CCT Color Mode
307  if color_temp_mired := kwargs.get(ATTR_COLOR_TEMP):
308  color_temp_kelvin = color_temperature_mired_to_kelvin(color_temp_mired)
309  if (
310  ATTR_BRIGHTNESS not in kwargs
311  and self.color_modecolor_modecolor_mode in MULTI_BRIGHTNESS_COLOR_MODES
312  ):
313  # When switching to color temp from RGBWW or RGB&W mode,
314  # we do not want the overall brightness of the RGB channels
315  brightness = max(MIN_CCT_BRIGHTNESS, *self._device.rgb)
316  await self._device.async_set_white_temp(color_temp_kelvin, brightness)
317  return
318  # Handle switch to RGB Color Mode
319  if rgb := kwargs.get(ATTR_RGB_COLOR):
320  if not self._device.requires_turn_on:
321  rgb = _min_rgb_brightness(rgb)
322  red, green, blue = rgb
323  await self._device.async_set_levels(red, green, blue, brightness=brightness)
324  return
325  # Handle switch to RGBW Color Mode
326  if rgbw := kwargs.get(ATTR_RGBW_COLOR):
327  if ATTR_BRIGHTNESS in kwargs:
328  rgbw = rgbw_brightness(rgbw, brightness)
329  rgbw = _min_rgbw_brightness(rgbw, self._device.rgbw)
330  await self._device.async_set_levels(*rgbw)
331  return
332  # Handle switch to RGBWW Color Mode
333  if rgbcw := kwargs.get(ATTR_RGBWW_COLOR):
334  if ATTR_BRIGHTNESS in kwargs:
335  rgbcw = rgbcw_brightness(kwargs[ATTR_RGBWW_COLOR], brightness)
336  rgbwc = rgbcw_to_rgbwc(rgbcw)
337  rgbwc = _min_rgbwc_brightness(rgbwc, self._device.rgbww)
338  await self._device.async_set_levels(*rgbwc)
339  return
340  if (white := kwargs.get(ATTR_WHITE)) is not None:
341  await self._device.async_set_levels(w=white)
342  return
343 
345  self, colors: list[tuple[int, int, int]], speed_pct: int, transition: str
346  ) -> None:
347  """Set a custom effect on the bulb."""
348  await self._device.async_set_custom_pattern(
349  colors,
350  speed_pct,
351  transition,
352  )
353 
354  async def async_set_zones(
355  self, colors: list[tuple[int, int, int]], speed_pct: int, effect: str
356  ) -> None:
357  """Set a colors for zones."""
358  await self._device.async_set_zones(
359  colors,
360  speed_pct,
362  )
363 
365  self,
366  sensitivity: int,
367  brightness: int,
368  effect: int,
369  light_screen: bool,
370  foreground_color: tuple[int, int, int] | None = None,
371  background_color: tuple[int, int, int] | None = None,
372  ) -> None:
373  """Configure music mode."""
374  await self._async_ensure_device_on_async_ensure_device_on()
375  await self._device.async_set_music_mode(
376  sensitivity=sensitivity,
377  brightness=brightness,
378  mode=MusicMode.LIGHT_SCREEN.value if light_screen else None,
379  effect=effect,
380  foreground_color=foreground_color,
381  background_color=background_color,
382  )
tuple[int, int, int] rgb_color(self)
Definition: light.py:230
int _async_brightness(self, **Any kwargs)
Definition: light.py:287
None async_set_custom_effect(self, list[tuple[int, int, int]] colors, int speed_pct, str transition)
Definition: light.py:346
None _async_set_mode(self, **Any kwargs)
Definition: light.py:299
None async_set_music_mode(self, int sensitivity, int brightness, int effect, bool light_screen, tuple[int, int, int]|None foreground_color=None, tuple[int, int, int]|None background_color=None)
Definition: light.py:372
tuple[int, int, int, int] rgbw_color(self)
Definition: light.py:235
None __init__(self, FluxLedUpdateCoordinator coordinator, str base_unique_id, list[tuple[int, int, int]] custom_effect_colors, int custom_effect_speed_pct, str custom_effect_transition)
Definition: light.py:205
None _async_set_effect(self, str effect, int brightness)
Definition: light.py:269
tuple[int, int, int, int, int] rgbww_color(self)
Definition: light.py:240
None _async_turn_on(self, **Any kwargs)
Definition: light.py:256
None async_set_zones(self, list[tuple[int, int, int]] colors, int speed_pct, str effect)
Definition: light.py:356
ColorMode|str|None color_mode(self)
Definition: __init__.py:909
None async_setup_entry(HomeAssistant hass, config_entries.ConfigEntry entry, AddEntitiesCallback async_add_entities)
Definition: light.py:144
int _effect_brightness(int brightness)
Definition: util.py:55
ColorMode _flux_color_mode_to_hass(str|None flux_color_mode, set[str] flux_color_modes)
Definition: util.py:44
tuple[int, int, int] _min_rgb_brightness(tuple[int, int, int] rgb)
Definition: util.py:74
tuple[int, int, int, int] _min_rgbw_brightness(tuple[int, int, int, int] rgbw, tuple[int, int, int, int] current_rgbw)
Definition: util.py:88
tuple[int, int, int, int, int] _min_rgbwc_brightness(tuple[int, int, int, int, int] rgbwc, tuple[int, int, int, int, int] current_rgbwc)
Definition: util.py:104
MultiColorEffects _str_to_multi_color_effect(str effect_str)
Definition: util.py:60
set[str] _hass_color_modes(AIOWifiLedBulb device)
Definition: util.py:14
int color_temperature_mired_to_kelvin(float mired_temperature)
Definition: color.py:631
int color_temperature_kelvin_to_mired(float kelvin_temperature)
Definition: color.py:636