1 """Support for LIFX lights."""
3 from __future__
import annotations
6 from collections.abc
import Callable
7 from datetime
import timedelta
10 import aiolifx_effects
11 from aiolifx_themes.themes
import Theme, ThemeLibrary
12 import voluptuous
as vol
19 ATTR_COLOR_TEMP_KELVIN,
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
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"
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"
60 ATTR_SPREAD =
"spread"
62 EFFECT_FLAME =
"FLAME"
63 EFFECT_MORPH =
"MORPH"
68 EFFECT_FLAME_DEFAULT_SPEED = 3
70 EFFECT_MORPH_DEFAULT_SPEED = 3
71 EFFECT_MORPH_DEFAULT_THEME =
"exciting"
73 EFFECT_MOVE_DEFAULT_SPEED = 3
74 EFFECT_MOVE_DEFAULT_DIRECTION =
"right"
75 EFFECT_MOVE_DIRECTION_RIGHT =
"right"
76 EFFECT_MOVE_DIRECTION_LEFT =
"left"
78 EFFECT_MOVE_DIRECTIONS = [EFFECT_MOVE_DIRECTION_LEFT, EFFECT_MOVE_DIRECTION_RIGHT]
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
85 EFFECT_SKY_SKY_TYPES = [
"Sunrise",
"Sunset",
"Clouds"]
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"
101 LIFX_EFFECT_SCHEMA = {
102 vol.Optional(ATTR_POWER_ON, default=
True): cv.boolean,
105 LIFX_EFFECT_PULSE_SCHEMA = cv.make_entity_service_schema(
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))
114 vol.Exclusive(ATTR_XY_COLOR, COLOR_GROUP): vol.All(
115 vol.Coerce(tuple), vol.ExactSequence((cv.small_float, cv.small_float))
117 vol.Exclusive(ATTR_HS_COLOR, COLOR_GROUP): vol.All(
121 vol.All(vol.Coerce(float), vol.Range(min=0, max=360)),
122 vol.All(vol.Coerce(float), vol.Range(min=0, max=100)),
126 vol.Exclusive(ATTR_COLOR_TEMP_KELVIN, COLOR_GROUP): vol.All(
127 vol.Coerce(int), vol.Range(min=1500, max=9000)
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),
136 LIFX_EFFECT_COLORLOOP_SCHEMA = cv.make_entity_service_schema(
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,
150 LIFX_EFFECT_STOP_SCHEMA = cv.make_entity_service_schema({})
152 LIFX_EFFECT_FLAME_SCHEMA = cv.make_entity_service_schema(
154 **LIFX_EFFECT_SCHEMA,
155 ATTR_SPEED: vol.All(vol.Coerce(int), vol.Clamp(min=1, max=25)),
159 HSBK_SCHEMA = vol.All(
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)),
171 LIFX_EFFECT_MORPH_SCHEMA = cv.make_entity_service_schema(
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)
178 vol.Exclusive(ATTR_PALETTE, COLOR_GROUP): vol.All(
179 cv.ensure_list, [HSBK_SCHEMA]
184 LIFX_EFFECT_MOVE_SCHEMA = cv.make_entity_service_schema(
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)),
193 LIFX_EFFECT_SKY_SCHEMA = cv.make_entity_service_schema(
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]),
206 SERVICE_EFFECT_COLORLOOP,
207 SERVICE_EFFECT_FLAME,
208 SERVICE_EFFECT_MORPH,
210 SERVICE_EFFECT_PULSE,
217 """Representation of all known LIFX entities."""
220 """Initialize the manager."""
223 self.entry_id_to_entity_id: dict[str, str] = {}
227 """Release resources."""
228 for service
in SERVICES:
229 self.
hasshass.services.async_remove(DOMAIN, service)
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
239 def unregister_entity() -> None:
240 """Unregister entity when it is being destroyed."""
241 self.entry_id_to_entity_id.pop(entry_id)
243 return unregister_entity
247 """Register the LIFX effects as hass service calls."""
249 async
def service_handler(service: ServiceCall) ->
None:
250 """Apply a service, i.e. start an effect."""
252 all_referenced = referenced.referenced | referenced.indirectly_referenced
254 await self.
start_effectstart_effect(all_referenced, service.service, **service.data)
256 self.
hasshass.services.async_register(
258 SERVICE_EFFECT_PULSE,
260 schema=LIFX_EFFECT_PULSE_SCHEMA,
263 self.
hasshass.services.async_register(
265 SERVICE_EFFECT_COLORLOOP,
267 schema=LIFX_EFFECT_COLORLOOP_SCHEMA,
270 self.
hasshass.services.async_register(
272 SERVICE_EFFECT_FLAME,
274 schema=LIFX_EFFECT_FLAME_SCHEMA,
277 self.
hasshass.services.async_register(
279 SERVICE_EFFECT_MORPH,
281 schema=LIFX_EFFECT_MORPH_SCHEMA,
284 self.
hasshass.services.async_register(
288 schema=LIFX_EFFECT_MOVE_SCHEMA,
291 self.
hasshass.services.async_register(
295 schema=LIFX_EFFECT_SKY_SCHEMA,
298 self.
hasshass.services.async_register(
302 schema=LIFX_EFFECT_STOP_SCHEMA,
306 self, entity_ids: set[str], service: str, **kwargs: Any
308 """Start a light effect on entities."""
310 coordinators: list[LIFXUpdateCoordinator] = []
311 bulbs: list[Light] = []
313 for entry_id, coordinator
in self.
hasshass.data[DOMAIN].items():
315 entry_id != DATA_LIFX_MANAGER
316 and self.entry_id_to_entity_id[entry_id]
in entity_ids
318 coordinators.append(coordinator)
319 bulbs.append(coordinator.device)
321 if service == SERVICE_EFFECT_FLAME:
322 await asyncio.gather(
324 coordinator.async_set_matrix_effect(
326 speed=kwargs.get(ATTR_SPEED, EFFECT_FLAME_DEFAULT_SPEED),
327 power_on=kwargs.get(ATTR_POWER_ON,
True),
329 for coordinator
in coordinators
333 elif service == SERVICE_EFFECT_MORPH:
334 theme_name = kwargs.get(ATTR_THEME,
"exciting")
335 palette = kwargs.get(ATTR_PALETTE)
337 if palette
is not None:
340 theme.add_hsbk(hsbk[0], hsbk[1], hsbk[2], hsbk[3])
342 theme = ThemeLibrary().get_theme(theme_name)
344 await asyncio.gather(
346 coordinator.async_set_matrix_effect(
348 speed=kwargs.get(ATTR_SPEED, EFFECT_MORPH_DEFAULT_SPEED),
349 palette=theme.colors,
350 power_on=kwargs.get(ATTR_POWER_ON,
True),
352 for coordinator
in coordinators
356 elif service == SERVICE_EFFECT_MOVE:
357 await asyncio.gather(
359 coordinator.async_set_multizone_effect(
361 speed=kwargs.get(ATTR_SPEED, EFFECT_MOVE_DEFAULT_SPEED),
362 direction=kwargs.get(
363 ATTR_DIRECTION, EFFECT_MOVE_DEFAULT_DIRECTION
365 theme_name=kwargs.get(ATTR_THEME),
366 power_on=kwargs.get(ATTR_POWER_ON,
False),
368 for coordinator
in coordinators
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),
382 elif service == SERVICE_EFFECT_COLORLOOP:
384 saturation_max =
None
385 saturation_min =
None
387 if ATTR_BRIGHTNESS
in kwargs:
389 elif ATTR_BRIGHTNESS_PCT
in kwargs:
391 round(255 * kwargs[ATTR_BRIGHTNESS_PCT] / 100)
394 if ATTR_SATURATION_MAX
in kwargs:
395 saturation_max =
int(kwargs[ATTR_SATURATION_MAX] / 100 * 65535)
397 if ATTR_SATURATION_MIN
in kwargs:
398 saturation_min =
int(kwargs[ATTR_SATURATION_MIN] / 100 * 65535)
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,
412 elif service == SERVICE_EFFECT_SKY:
413 palette = kwargs.get(ATTR_PALETTE)
414 if palette
is not None:
417 theme.add_hsbk(hsbk[0], hsbk[1], hsbk[2], hsbk[3])
419 speed = kwargs.get(ATTR_SPEED, EFFECT_SKY_DEFAULT_SPEED)
420 sky_type = kwargs.get(ATTR_SKY_TYPE, EFFECT_SKY_DEFAULT_SKY_TYPE)
422 cloud_saturation_min = kwargs.get(
423 ATTR_CLOUD_SATURATION_MIN,
424 EFFECT_SKY_DEFAULT_CLOUD_SATURATION_MIN,
426 cloud_saturation_max = kwargs.get(
427 ATTR_CLOUD_SATURATION_MAX,
428 EFFECT_SKY_DEFAULT_CLOUD_SATURATION_MAX,
431 await asyncio.gather(
433 coordinator.async_set_matrix_effect(
437 cloud_saturation_min=cloud_saturation_min,
438 cloud_saturation_max=cloud_saturation_max,
439 palette=theme.colors,
441 for coordinator
in coordinators
445 elif service == SERVICE_EFFECT_STOP:
448 for coordinator
in coordinators:
449 await coordinator.async_set_matrix_effect(
450 effect=EFFECT_OFF, power_on=
False
452 await coordinator.async_set_multizone_effect(
453 effect=EFFECT_OFF, power_on=
False
Callable[[], None] async_register_entity(self, str entity_id, str entry_id)
None start_effect(self, set[str] entity_ids, str service, **Any kwargs)
None __init__(self, HomeAssistant hass)
list[float|int|None]|None find_hsbk(HomeAssistant hass, **Any kwargs)
int convert_8_to_16(int value)
SelectedEntities async_extract_referenced_entity_ids(HomeAssistant hass, ServiceCall service_call, bool expand_group=True)