1 """Coordinator for lifx."""
3 from __future__
import annotations
6 from collections.abc
import Callable
7 from datetime
import timedelta
8 from enum
import IntEnum
9 from functools
import partial
10 from math
import floor, log10
11 from typing
import Any, cast
13 from aiolifx.aiolifx
import (
21 from aiolifx.connection
import LIFXConnection
22 from aiolifx_themes.themes
import ThemeLibrary, ThemePainter
23 from awesomeversion
import AwesomeVersion
24 from propcache
import cached_property
27 SIGNAL_STRENGTH_DECIBELS,
28 SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
43 MAX_ATTEMPTS_PER_UPDATE_REQUEST_MESSAGE,
53 async_multi_execute_lifx_with_retries,
55 infrared_brightness_option_to_value,
56 infrared_brightness_value_to_option,
60 LIGHT_UPDATE_INTERVAL = 10
61 REQUEST_REFRESH_DELAY = 0.35
62 LIFX_IDENTIFY_DELAY = 3.0
63 ZONES_PER_COLOR_UPDATE_REQUEST = 8
65 RSSI_DBM_FW = AwesomeVersion(
"2.77")
69 """Enumeration of LIFX firmware effects."""
79 """Enumeration of sky types for SKY firmware effect."""
87 """DataUpdateCoordinator to gather data for a specific lifx device."""
92 connection: LIFXConnection,
95 """Initialize DataUpdateCoordinator."""
96 assert connection.device
is not None
98 self.device: Light = connection.device
99 self.
locklock = asyncio.Lock()
102 self.
_rssi_rssi: int = 0
108 name=f
"{title} ({self.device.ip_addr})",
109 update_interval=
timedelta(seconds=LIGHT_UPDATE_INTERVAL),
113 hass, _LOGGER, cooldown=REQUEST_REFRESH_DELAY, immediate=
False
119 """Change timeouts."""
120 self.device.timeout = MESSAGE_TIMEOUT
121 self.device.retry_count = MESSAGE_RETRIES
122 self.device.unregister_timeout = UNAVAILABLE_GRACE
126 """Return stored RSSI value."""
127 return self.
_rssi_rssi
131 """Return the RSSI unit of measurement."""
132 if AwesomeVersion(self.device.host_firmware_version) <= RSSI_DBM_FW:
133 return SIGNAL_STRENGTH_DECIBELS
135 return SIGNAL_STRENGTH_DECIBELS_MILLIWATT
139 """Return the current infrared brightness as a string."""
144 """Return the internal mac address."""
146 str, self.device.mac_addr
151 """Return the physical mac address."""
154 self.device.mac_addr,
155 self.device.host_firmware_version,
160 """Return the label of the bulb."""
161 return cast(str, self.device.label)
165 """Return true if this is a multizone device."""
166 return bool(
lifx_features(self.device)[
"extended_multizone"])
170 """Return true if this is a legacy multizone device."""
177 """Return true if this is a matrix device."""
181 """Return diagnostic information about the device."""
184 "firmware": self.device.host_firmware_version,
185 "vendor": self.device.vendor,
186 "product_id": self.device.product,
187 "features": features,
188 "hue": self.device.color[0],
189 "saturation": self.device.color[1],
190 "brightness": self.device.color[2],
191 "kelvin": self.device.color[3],
192 "power": self.device.power_level,
195 if features[
"multizone"]
is True:
196 zones = {
"count": self.device.zones_count,
"state": {}}
197 for index, zone_color
in enumerate(self.device.color_zones):
198 zones[
"state"][index] = {
199 "hue": zone_color[0],
200 "saturation": zone_color[1],
201 "brightness": zone_color[2],
202 "kelvin": zone_color[3],
204 device_data[
"zones"] = zones
206 if features[
"hev"]
is True:
207 device_data[
"hev"] = {
208 "hev_cycle": self.device.hev_cycle,
209 "hev_config": self.device.hev_cycle_configuration,
210 "last_result": self.device.last_hev_cycle_result,
213 if features[
"infrared"]
is True:
214 device_data[
"infrared"] = {
"brightness": self.device.infrared_brightness}
219 """Return the entity_id from the platform and key provided."""
220 ent_reg = er.async_get(self.
hasshass)
221 return ent_reg.async_get_entity_id(
222 platform, DOMAIN, f
"{self.serial_number}_{key}"
226 """Populate device info."""
227 methods: list[Callable] = []
229 if self.device.host_firmware_version
is None:
230 methods.append(device.get_hostfirmware)
231 if self.device.product
is None:
232 methods.append(device.get_version)
233 if self.device.group
is None:
234 methods.append(device.get_group)
235 assert methods,
"Device info already populated"
237 methods, DEFAULT_ATTEMPTS, OVERALL_TIMEOUT
241 """Return the number of zones.
243 If the number of zones is not yet populated, return 1 since
244 the device will have a least one zone.
246 return len(self.device.color_zones)
if self.device.color_zones
else 1
250 """Build a color zones update request."""
252 calls: list[Callable] = []
257 def _wrap_get_color_zones(
258 callb: Callable[[Message, dict[str, Any] |
None],
None],
259 get_color_zones_args: dict[str, Any],
261 """Capture the callback and make sure resp_set_multizonemultizone is called before."""
263 def _wrapped_callback(
270 device.resp_set_multizonemultizone(response)
272 callb(bulb, response, **kwargs)
274 device.get_color_zones(**get_color_zones_args, callb=_wrapped_callback)
278 _wrap_get_color_zones,
279 get_color_zones_args={
281 "end_index": zone + ZONES_PER_COLOR_UPDATE_REQUEST - 1,
289 """Fetch all device data from the api."""
292 device.host_firmware_version
is None
293 or device.product
is None
294 or device.group
is None
301 methods: list[Callable] = [self.device.get_color]
303 methods.append(self.device.get_wifiinfo)
307 self.device.get_tile_effect,
308 self.device.get_device_chain,
313 methods.append(self.device.get_extended_color_zones)
317 methods.append(self.device.get_multizone_effect)
319 methods.append(self.device.get_hev_cycle)
320 if features[
"infrared"]:
321 methods.append(self.device.get_infrared)
324 methods, MAX_ATTEMPTS_PER_UPDATE_REQUEST_MESSAGE, MAX_UPDATE_TIME
327 if device.mac_addr == TARGET_ANY:
328 device.mac_addr = responses[0].target_addr
332 self.
_rssi_rssi =
int(floor(10 * log10(responses[1].signal) + 0.5))
335 self.
active_effectactive_effect = FirmwareEffect[self.device.effect.get(
"effect",
"OFF")]
342 """Get updated color information for each zone."""
350 """Get updated color information for all zones."""
353 except TimeoutError
as ex:
355 f
"Timeout getting color zones from {self.name}"
359 self, value: dict[str, Any], rapid: bool =
False
361 """Send a set_waveform_optional message to the device."""
363 partial(self.device.set_waveform_optional, value=value, rapid=rapid)
367 """Send a get color message to the device."""
371 """Send a set power message to the device."""
373 partial(self.device.set_power, state, duration=duration)
377 self, hsbk: list[float | int |
None], duration: int |
None
379 """Send a set color message to the device."""
381 partial(self.device.set_color, hsbk, duration=duration)
388 hsbk: list[float | int |
None],
389 duration: int |
None,
392 """Send a set color zones message to the device."""
395 self.device.set_color_zones,
396 start_index=start_index,
406 colors: list[tuple[int | float, int | float, int | float, int | float]],
407 colors_count: int |
None =
None,
411 """Send a single set extended color zones message to the device."""
413 if colors_count
is None:
414 colors_count = len(colors)
418 colors.extend([(0, 0, 0, 0)
for _
in range(82 - len(colors))])
422 self.device.set_extended_color_zones,
424 colors_count=colors_count,
434 direction: str =
"RIGHT",
435 theme_name: str |
None =
None,
436 power_on: bool =
True,
438 """Control the firmware-based Move effect on a multizone device."""
440 if power_on
and self.device.power_level == 0:
443 if theme_name
is not None:
444 theme = ThemeLibrary().get_theme(theme_name)
445 await ThemePainter(self.
hasshass.loop).paint(
446 theme, [self.device], round(speed)
451 self.device.set_multizone_effect,
452 effect=MultiZoneEffectType[effect.upper()].value,
454 direction=MultiZoneDirection[direction.upper()].value,
457 self.
active_effectactive_effect = FirmwareEffect[effect.upper()]
462 palette: list[tuple[int, int, int, int]] |
None =
None,
463 speed: float |
None =
None,
464 power_on: bool =
True,
465 sky_type: str |
None =
None,
466 cloud_saturation_min: int |
None =
None,
467 cloud_saturation_max: int |
None =
None,
469 """Control the firmware-based effects on a matrix device."""
471 if power_on
and self.device.power_level == 0:
477 if sky_type
is not None:
478 sky_type = TileEffectSkyType[sky_type.upper()].value
482 self.device.set_tile_effect,
483 effect=TileEffectType[effect.upper()].value,
487 cloud_saturation_min=cloud_saturation_min,
488 cloud_saturation_max=cloud_saturation_max,
491 self.
active_effectactive_effect = FirmwareEffect[effect.upper()]
494 """Return the enum value of the currently active firmware effect."""
498 """Set infrared brightness."""
503 """Identify the device by flashing it three times."""
504 bulb: Light = self.device
512 await asyncio.sleep(LIFX_IDENTIFY_DELAY)
516 """Enable RSSI signal strength updates."""
519 def _async_disable_rssi_updates() -> None:
520 """Disable RSSI updates when sensor removed."""
524 return _async_disable_rssi_updates
527 """Return the current HEV cycle state."""
528 if self.device.hev_cycle
is None:
530 return bool(self.device.hev_cycle.get(ATTR_REMAINING, 0) > 0)
533 """Start or stop an HEV cycle on a LIFX Clean bulb."""
536 partial(self.device.set_hev_cycle, enable=enable, duration=duration)
540 """Apply the selected theme to the device."""
542 theme = ThemeLibrary().get_theme(theme_name)
543 await ThemePainter(self.
hasshass.loop).paint(theme, [self.device])
None async_set_multizone_effect(self, str effect, float speed=3.0, str direction="RIGHT", str|None theme_name=None, bool power_on=True)
None async_get_color(self)
list[Callable] _async_build_color_zones_update_requests(self)
str|None async_get_entity_id(self, Platform platform, str key)
None async_set_color_zones(self, int start_index, int end_index, list[float|int|None] hsbk, int|None duration, int apply)
int async_get_active_effect(self)
None async_identify_bulb(self)
None async_get_extended_color_zones(self)
None async_apply_theme(self, str theme_name)
None _async_update_data(self)
None async_set_extended_color_zones(self, list[tuple[int|float, int|float, int|float, int|float]] colors, int|None colors_count=None, int duration=0, int apply=1)
None __init__(self, HomeAssistant hass, LIFXConnection connection, str title)
None async_set_color(self, list[float|int|None] hsbk, int|None duration)
None async_set_hev_cycle_state(self, bool enable, int duration=0)
None async_set_power(self, bool state, int|None duration)
Callable[[], None] async_enable_rssi_updates(self)
None async_get_color_zones(self)
bool is_legacy_multizone(self)
None async_set_waveform_optional(self, dict[str, Any] value, bool rapid=False)
None _async_populate_device_info(self)
str|None current_infrared_brightness(self)
int get_number_of_zones(self)
None async_set_infrared_brightness(self, str option)
dict[str, Any] diagnostics(self)
bool is_extended_multizone(self)
None async_set_matrix_effect(self, str effect, list[tuple[int, int, int, int]]|None palette=None, float|None speed=None, bool power_on=True, str|None sky_type=None, int|None cloud_saturation_min=None, int|None cloud_saturation_max=None)
bool|None async_get_hev_cycle_state(self)
list[Message] async_multi_execute_lifx_with_retries(list[Callable] methods, int attempts, int overall_timeout)
Message async_execute_lifx(Callable method)
str get_real_mac_addr(str mac_addr, str firmware)
str|None infrared_brightness_value_to_option(int value)
int|None infrared_brightness_option_to_value(str option)
dict[str, Any] lifx_features(Light bulb)