Home Assistant Unofficial Reference 2024.12.1
manager.py
Go to the documentation of this file.
1 """Support for LIFX lights."""
2 
3 from __future__ import annotations
4 
5 import asyncio
6 from collections.abc import Callable
7 from datetime import timedelta
8 from typing import Any
9 
10 import aiolifx_effects
11 from aiolifx_themes.themes import Theme, ThemeLibrary
12 import voluptuous as vol
13 
15  ATTR_BRIGHTNESS,
16  ATTR_BRIGHTNESS_PCT,
17  ATTR_COLOR_NAME,
18  ATTR_COLOR_TEMP,
19  ATTR_COLOR_TEMP_KELVIN,
20  ATTR_HS_COLOR,
21  ATTR_RGB_COLOR,
22  ATTR_TRANSITION,
23  ATTR_XY_COLOR,
24  COLOR_GROUP,
25  VALID_BRIGHTNESS,
26  VALID_BRIGHTNESS_PCT,
27 )
28 from homeassistant.const import ATTR_MODE
29 from homeassistant.core import HomeAssistant, ServiceCall, callback
31 from homeassistant.helpers.service import async_extract_referenced_entity_ids
32 
33 from .const import ATTR_THEME, DATA_LIFX_MANAGER, DOMAIN
34 from .coordinator import LIFXUpdateCoordinator, Light
35 from .util import convert_8_to_16, find_hsbk
36 
37 SCAN_INTERVAL = timedelta(seconds=10)
38 
39 SERVICE_EFFECT_COLORLOOP = "effect_colorloop"
40 SERVICE_EFFECT_FLAME = "effect_flame"
41 SERVICE_EFFECT_MORPH = "effect_morph"
42 SERVICE_EFFECT_MOVE = "effect_move"
43 SERVICE_EFFECT_PULSE = "effect_pulse"
44 SERVICE_EFFECT_SKY = "effect_sky"
45 SERVICE_EFFECT_STOP = "effect_stop"
46 
47 ATTR_CHANGE = "change"
48 ATTR_CLOUD_SATURATION_MIN = "cloud_saturation_min"
49 ATTR_CLOUD_SATURATION_MAX = "cloud_saturation_max"
50 ATTR_CYCLES = "cycles"
51 ATTR_DIRECTION = "direction"
52 ATTR_PALETTE = "palette"
53 ATTR_PERIOD = "period"
54 ATTR_POWER_OFF = "power_off"
55 ATTR_POWER_ON = "power_on"
56 ATTR_SATURATION_MAX = "saturation_max"
57 ATTR_SATURATION_MIN = "saturation_min"
58 ATTR_SKY_TYPE = "sky_type"
59 ATTR_SPEED = "speed"
60 ATTR_SPREAD = "spread"
61 
62 EFFECT_FLAME = "FLAME"
63 EFFECT_MORPH = "MORPH"
64 EFFECT_MOVE = "MOVE"
65 EFFECT_OFF = "OFF"
66 EFFECT_SKY = "SKY"
67 
68 EFFECT_FLAME_DEFAULT_SPEED = 3
69 
70 EFFECT_MORPH_DEFAULT_SPEED = 3
71 EFFECT_MORPH_DEFAULT_THEME = "exciting"
72 
73 EFFECT_MOVE_DEFAULT_SPEED = 3
74 EFFECT_MOVE_DEFAULT_DIRECTION = "right"
75 EFFECT_MOVE_DIRECTION_RIGHT = "right"
76 EFFECT_MOVE_DIRECTION_LEFT = "left"
77 
78 EFFECT_MOVE_DIRECTIONS = [EFFECT_MOVE_DIRECTION_LEFT, EFFECT_MOVE_DIRECTION_RIGHT]
79 
80 EFFECT_SKY_DEFAULT_SPEED = 50
81 EFFECT_SKY_DEFAULT_SKY_TYPE = "Clouds"
82 EFFECT_SKY_DEFAULT_CLOUD_SATURATION_MIN = 50
83 EFFECT_SKY_DEFAULT_CLOUD_SATURATION_MAX = 180
84 
85 EFFECT_SKY_SKY_TYPES = ["Sunrise", "Sunset", "Clouds"]
86 
87 PULSE_MODE_BLINK = "blink"
88 PULSE_MODE_BREATHE = "breathe"
89 PULSE_MODE_PING = "ping"
90 PULSE_MODE_SOLID = "solid"
91 PULSE_MODE_STROBE = "strobe"
92 
93 PULSE_MODES = [
94  PULSE_MODE_BLINK,
95  PULSE_MODE_BREATHE,
96  PULSE_MODE_PING,
97  PULSE_MODE_STROBE,
98  PULSE_MODE_SOLID,
99 ]
100 
101 LIFX_EFFECT_SCHEMA = {
102  vol.Optional(ATTR_POWER_ON, default=True): cv.boolean,
103 }
104 
105 LIFX_EFFECT_PULSE_SCHEMA = cv.make_entity_service_schema(
106  {
107  **LIFX_EFFECT_SCHEMA,
108  vol.Exclusive(ATTR_BRIGHTNESS, ATTR_BRIGHTNESS): VALID_BRIGHTNESS,
109  vol.Exclusive(ATTR_BRIGHTNESS_PCT, ATTR_BRIGHTNESS): VALID_BRIGHTNESS_PCT,
110  vol.Exclusive(ATTR_COLOR_NAME, COLOR_GROUP): cv.string,
111  vol.Exclusive(ATTR_RGB_COLOR, COLOR_GROUP): vol.All(
112  vol.Coerce(tuple), vol.ExactSequence((cv.byte, cv.byte, cv.byte))
113  ),
114  vol.Exclusive(ATTR_XY_COLOR, COLOR_GROUP): vol.All(
115  vol.Coerce(tuple), vol.ExactSequence((cv.small_float, cv.small_float))
116  ),
117  vol.Exclusive(ATTR_HS_COLOR, COLOR_GROUP): vol.All(
118  vol.Coerce(tuple),
119  vol.ExactSequence(
120  (
121  vol.All(vol.Coerce(float), vol.Range(min=0, max=360)),
122  vol.All(vol.Coerce(float), vol.Range(min=0, max=100)),
123  )
124  ),
125  ),
126  vol.Exclusive(ATTR_COLOR_TEMP_KELVIN, COLOR_GROUP): vol.All(
127  vol.Coerce(int), vol.Range(min=1500, max=9000)
128  ),
129  vol.Exclusive(ATTR_COLOR_TEMP, COLOR_GROUP): cv.positive_int,
130  ATTR_PERIOD: vol.All(vol.Coerce(float), vol.Range(min=0.05)),
131  ATTR_CYCLES: vol.All(vol.Coerce(float), vol.Range(min=1)),
132  ATTR_MODE: vol.In(PULSE_MODES),
133  }
134 )
135 
136 LIFX_EFFECT_COLORLOOP_SCHEMA = cv.make_entity_service_schema(
137  {
138  **LIFX_EFFECT_SCHEMA,
139  vol.Exclusive(ATTR_BRIGHTNESS, ATTR_BRIGHTNESS): VALID_BRIGHTNESS,
140  vol.Exclusive(ATTR_BRIGHTNESS_PCT, ATTR_BRIGHTNESS): VALID_BRIGHTNESS_PCT,
141  ATTR_SATURATION_MAX: vol.All(vol.Coerce(int), vol.Clamp(min=0, max=100)),
142  ATTR_SATURATION_MIN: vol.All(vol.Coerce(int), vol.Clamp(min=0, max=100)),
143  ATTR_PERIOD: vol.All(vol.Coerce(float), vol.Clamp(min=0.05)),
144  ATTR_CHANGE: vol.All(vol.Coerce(float), vol.Clamp(min=0, max=360)),
145  ATTR_SPREAD: vol.All(vol.Coerce(float), vol.Clamp(min=0, max=360)),
146  ATTR_TRANSITION: cv.positive_float,
147  }
148 )
149 
150 LIFX_EFFECT_STOP_SCHEMA = cv.make_entity_service_schema({})
151 
152 LIFX_EFFECT_FLAME_SCHEMA = cv.make_entity_service_schema(
153  {
154  **LIFX_EFFECT_SCHEMA,
155  ATTR_SPEED: vol.All(vol.Coerce(int), vol.Clamp(min=1, max=25)),
156  }
157 )
158 
159 HSBK_SCHEMA = vol.All(
160  vol.Coerce(tuple),
161  vol.ExactSequence(
162  (
163  vol.All(vol.Coerce(float), vol.Range(min=0, max=360)),
164  vol.All(vol.Coerce(float), vol.Range(min=0, max=100)),
165  vol.All(vol.Coerce(float), vol.Clamp(min=0, max=100)),
166  vol.All(vol.Coerce(int), vol.Clamp(min=1500, max=9000)),
167  )
168  ),
169 )
170 
171 LIFX_EFFECT_MORPH_SCHEMA = cv.make_entity_service_schema(
172  {
173  **LIFX_EFFECT_SCHEMA,
174  ATTR_SPEED: vol.All(vol.Coerce(int), vol.Clamp(min=1, max=25)),
175  vol.Exclusive(ATTR_THEME, COLOR_GROUP): vol.Optional(
176  vol.In(ThemeLibrary().themes)
177  ),
178  vol.Exclusive(ATTR_PALETTE, COLOR_GROUP): vol.All(
179  cv.ensure_list, [HSBK_SCHEMA]
180  ),
181  }
182 )
183 
184 LIFX_EFFECT_MOVE_SCHEMA = cv.make_entity_service_schema(
185  {
186  **LIFX_EFFECT_SCHEMA,
187  ATTR_SPEED: vol.All(vol.Coerce(float), vol.Clamp(min=0.1, max=60)),
188  ATTR_DIRECTION: vol.In(EFFECT_MOVE_DIRECTIONS),
189  ATTR_THEME: vol.Optional(vol.In(ThemeLibrary().themes)),
190  }
191 )
192 
193 LIFX_EFFECT_SKY_SCHEMA = cv.make_entity_service_schema(
194  {
195  **LIFX_EFFECT_SCHEMA,
196  ATTR_SPEED: vol.All(vol.Coerce(int), vol.Clamp(min=1, max=86400)),
197  ATTR_SKY_TYPE: vol.In(EFFECT_SKY_SKY_TYPES),
198  ATTR_CLOUD_SATURATION_MIN: vol.All(vol.Coerce(int), vol.Clamp(min=0, max=255)),
199  ATTR_CLOUD_SATURATION_MAX: vol.All(vol.Coerce(int), vol.Clamp(min=0, max=255)),
200  ATTR_PALETTE: vol.All(cv.ensure_list, [HSBK_SCHEMA]),
201  }
202 )
203 
204 
205 SERVICES = (
206  SERVICE_EFFECT_COLORLOOP,
207  SERVICE_EFFECT_FLAME,
208  SERVICE_EFFECT_MORPH,
209  SERVICE_EFFECT_MOVE,
210  SERVICE_EFFECT_PULSE,
211  SERVICE_EFFECT_SKY,
212  SERVICE_EFFECT_STOP,
213 )
214 
215 
217  """Representation of all known LIFX entities."""
218 
219  def __init__(self, hass: HomeAssistant) -> None:
220  """Initialize the manager."""
221  self.hasshass = hass
222  self.effects_conductoreffects_conductor = aiolifx_effects.Conductor(hass.loop)
223  self.entry_id_to_entity_id: dict[str, str] = {}
224 
225  @callback
226  def async_unload(self) -> None:
227  """Release resources."""
228  for service in SERVICES:
229  self.hasshass.services.async_remove(DOMAIN, service)
230 
231  @callback
233  self, entity_id: str, entry_id: str
234  ) -> Callable[[], None]:
235  """Register an entity to the config entry id."""
236  self.entry_id_to_entity_id[entry_id] = entity_id
237 
238  @callback
239  def unregister_entity() -> None:
240  """Unregister entity when it is being destroyed."""
241  self.entry_id_to_entity_id.pop(entry_id)
242 
243  return unregister_entity
244 
245  @callback
246  def async_setup(self) -> None:
247  """Register the LIFX effects as hass service calls."""
248 
249  async def service_handler(service: ServiceCall) -> None:
250  """Apply a service, i.e. start an effect."""
251  referenced = async_extract_referenced_entity_ids(self.hasshass, service)
252  all_referenced = referenced.referenced | referenced.indirectly_referenced
253  if all_referenced:
254  await self.start_effectstart_effect(all_referenced, service.service, **service.data)
255 
256  self.hasshass.services.async_register(
257  DOMAIN,
258  SERVICE_EFFECT_PULSE,
259  service_handler,
260  schema=LIFX_EFFECT_PULSE_SCHEMA,
261  )
262 
263  self.hasshass.services.async_register(
264  DOMAIN,
265  SERVICE_EFFECT_COLORLOOP,
266  service_handler,
267  schema=LIFX_EFFECT_COLORLOOP_SCHEMA,
268  )
269 
270  self.hasshass.services.async_register(
271  DOMAIN,
272  SERVICE_EFFECT_FLAME,
273  service_handler,
274  schema=LIFX_EFFECT_FLAME_SCHEMA,
275  )
276 
277  self.hasshass.services.async_register(
278  DOMAIN,
279  SERVICE_EFFECT_MORPH,
280  service_handler,
281  schema=LIFX_EFFECT_MORPH_SCHEMA,
282  )
283 
284  self.hasshass.services.async_register(
285  DOMAIN,
286  SERVICE_EFFECT_MOVE,
287  service_handler,
288  schema=LIFX_EFFECT_MOVE_SCHEMA,
289  )
290 
291  self.hasshass.services.async_register(
292  DOMAIN,
293  SERVICE_EFFECT_SKY,
294  service_handler,
295  schema=LIFX_EFFECT_SKY_SCHEMA,
296  )
297 
298  self.hasshass.services.async_register(
299  DOMAIN,
300  SERVICE_EFFECT_STOP,
301  service_handler,
302  schema=LIFX_EFFECT_STOP_SCHEMA,
303  )
304 
305  async def start_effect(
306  self, entity_ids: set[str], service: str, **kwargs: Any
307  ) -> None:
308  """Start a light effect on entities."""
309 
310  coordinators: list[LIFXUpdateCoordinator] = []
311  bulbs: list[Light] = []
312 
313  for entry_id, coordinator in self.hasshass.data[DOMAIN].items():
314  if (
315  entry_id != DATA_LIFX_MANAGER
316  and self.entry_id_to_entity_id[entry_id] in entity_ids
317  ):
318  coordinators.append(coordinator)
319  bulbs.append(coordinator.device)
320 
321  if service == SERVICE_EFFECT_FLAME:
322  await asyncio.gather(
323  *(
324  coordinator.async_set_matrix_effect(
325  effect=EFFECT_FLAME,
326  speed=kwargs.get(ATTR_SPEED, EFFECT_FLAME_DEFAULT_SPEED),
327  power_on=kwargs.get(ATTR_POWER_ON, True),
328  )
329  for coordinator in coordinators
330  )
331  )
332 
333  elif service == SERVICE_EFFECT_MORPH:
334  theme_name = kwargs.get(ATTR_THEME, "exciting")
335  palette = kwargs.get(ATTR_PALETTE)
336 
337  if palette is not None:
338  theme = Theme()
339  for hsbk in palette:
340  theme.add_hsbk(hsbk[0], hsbk[1], hsbk[2], hsbk[3])
341  else:
342  theme = ThemeLibrary().get_theme(theme_name)
343 
344  await asyncio.gather(
345  *(
346  coordinator.async_set_matrix_effect(
347  effect=EFFECT_MORPH,
348  speed=kwargs.get(ATTR_SPEED, EFFECT_MORPH_DEFAULT_SPEED),
349  palette=theme.colors,
350  power_on=kwargs.get(ATTR_POWER_ON, True),
351  )
352  for coordinator in coordinators
353  )
354  )
355 
356  elif service == SERVICE_EFFECT_MOVE:
357  await asyncio.gather(
358  *(
359  coordinator.async_set_multizone_effect(
360  effect=EFFECT_MOVE,
361  speed=kwargs.get(ATTR_SPEED, EFFECT_MOVE_DEFAULT_SPEED),
362  direction=kwargs.get(
363  ATTR_DIRECTION, EFFECT_MOVE_DEFAULT_DIRECTION
364  ),
365  theme_name=kwargs.get(ATTR_THEME),
366  power_on=kwargs.get(ATTR_POWER_ON, False),
367  )
368  for coordinator in coordinators
369  )
370  )
371 
372  elif service == SERVICE_EFFECT_PULSE:
373  effect = aiolifx_effects.EffectPulse(
374  power_on=kwargs.get(ATTR_POWER_ON),
375  period=kwargs.get(ATTR_PERIOD),
376  cycles=kwargs.get(ATTR_CYCLES),
377  mode=kwargs.get(ATTR_MODE),
378  hsbk=find_hsbk(self.hasshass, **kwargs),
379  )
380  await self.effects_conductoreffects_conductor.start(effect, bulbs)
381 
382  elif service == SERVICE_EFFECT_COLORLOOP:
383  brightness = None
384  saturation_max = None
385  saturation_min = None
386 
387  if ATTR_BRIGHTNESS in kwargs:
388  brightness = convert_8_to_16(kwargs[ATTR_BRIGHTNESS])
389  elif ATTR_BRIGHTNESS_PCT in kwargs:
390  brightness = convert_8_to_16(
391  round(255 * kwargs[ATTR_BRIGHTNESS_PCT] / 100)
392  )
393 
394  if ATTR_SATURATION_MAX in kwargs:
395  saturation_max = int(kwargs[ATTR_SATURATION_MAX] / 100 * 65535)
396 
397  if ATTR_SATURATION_MIN in kwargs:
398  saturation_min = int(kwargs[ATTR_SATURATION_MIN] / 100 * 65535)
399 
400  effect = aiolifx_effects.EffectColorloop(
401  power_on=kwargs.get(ATTR_POWER_ON),
402  period=kwargs.get(ATTR_PERIOD),
403  change=kwargs.get(ATTR_CHANGE),
404  spread=kwargs.get(ATTR_SPREAD),
405  transition=kwargs.get(ATTR_TRANSITION),
406  brightness=brightness,
407  saturation_max=saturation_max,
408  saturation_min=saturation_min,
409  )
410  await self.effects_conductoreffects_conductor.start(effect, bulbs)
411 
412  elif service == SERVICE_EFFECT_SKY:
413  palette = kwargs.get(ATTR_PALETTE)
414  if palette is not None:
415  theme = Theme()
416  for hsbk in palette:
417  theme.add_hsbk(hsbk[0], hsbk[1], hsbk[2], hsbk[3])
418 
419  speed = kwargs.get(ATTR_SPEED, EFFECT_SKY_DEFAULT_SPEED)
420  sky_type = kwargs.get(ATTR_SKY_TYPE, EFFECT_SKY_DEFAULT_SKY_TYPE)
421 
422  cloud_saturation_min = kwargs.get(
423  ATTR_CLOUD_SATURATION_MIN,
424  EFFECT_SKY_DEFAULT_CLOUD_SATURATION_MIN,
425  )
426  cloud_saturation_max = kwargs.get(
427  ATTR_CLOUD_SATURATION_MAX,
428  EFFECT_SKY_DEFAULT_CLOUD_SATURATION_MAX,
429  )
430 
431  await asyncio.gather(
432  *(
433  coordinator.async_set_matrix_effect(
434  effect=EFFECT_SKY,
435  speed=speed,
436  sky_type=sky_type,
437  cloud_saturation_min=cloud_saturation_min,
438  cloud_saturation_max=cloud_saturation_max,
439  palette=theme.colors,
440  )
441  for coordinator in coordinators
442  )
443  )
444 
445  elif service == SERVICE_EFFECT_STOP:
446  await self.effects_conductoreffects_conductor.stop(bulbs)
447 
448  for coordinator in coordinators:
449  await coordinator.async_set_matrix_effect(
450  effect=EFFECT_OFF, power_on=False
451  )
452  await coordinator.async_set_multizone_effect(
453  effect=EFFECT_OFF, power_on=False
454  )
Callable[[], None] async_register_entity(self, str entity_id, str entry_id)
Definition: manager.py:234
None start_effect(self, set[str] entity_ids, str service, **Any kwargs)
Definition: manager.py:307
None __init__(self, HomeAssistant hass)
Definition: manager.py:219
list[float|int|None]|None find_hsbk(HomeAssistant hass, **Any kwargs)
Definition: util.py:86
int convert_8_to_16(int value)
Definition: util.py:68
SelectedEntities async_extract_referenced_entity_ids(HomeAssistant hass, ServiceCall service_call, bool expand_group=True)
Definition: service.py:507