Initial support for LIFX Ceiling SKY effect (#121820)
This commit is contained in:
parent
162b734be7
commit
5f33e85b30
9 changed files with 394 additions and 23 deletions
|
@ -61,5 +61,6 @@ INFRARED_BRIGHTNESS_VALUES_MAP = {
|
||||||
}
|
}
|
||||||
DATA_LIFX_MANAGER = "lifx_manager"
|
DATA_LIFX_MANAGER = "lifx_manager"
|
||||||
|
|
||||||
|
LIFX_CEILING_PRODUCT_IDS = {176, 177}
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__package__)
|
_LOGGER = logging.getLogger(__package__)
|
||||||
|
|
|
@ -6,7 +6,7 @@ import asyncio
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from enum import IntEnum
|
from enum import IntEnum
|
||||||
from functools import partial
|
from functools import cached_property, partial
|
||||||
from math import floor, log10
|
from math import floor, log10
|
||||||
from typing import Any, cast
|
from typing import Any, cast
|
||||||
|
|
||||||
|
@ -15,6 +15,7 @@ from aiolifx.aiolifx import (
|
||||||
Message,
|
Message,
|
||||||
MultiZoneDirection,
|
MultiZoneDirection,
|
||||||
MultiZoneEffectType,
|
MultiZoneEffectType,
|
||||||
|
TileEffectSkyType,
|
||||||
TileEffectType,
|
TileEffectType,
|
||||||
)
|
)
|
||||||
from aiolifx.connection import LIFXConnection
|
from aiolifx.connection import LIFXConnection
|
||||||
|
@ -70,9 +71,18 @@ class FirmwareEffect(IntEnum):
|
||||||
MOVE = 1
|
MOVE = 1
|
||||||
MORPH = 2
|
MORPH = 2
|
||||||
FLAME = 3
|
FLAME = 3
|
||||||
|
SKY = 5
|
||||||
|
|
||||||
|
|
||||||
class LIFXUpdateCoordinator(DataUpdateCoordinator[None]):
|
class SkyType(IntEnum):
|
||||||
|
"""Enumeration of sky types for SKY firmware effect."""
|
||||||
|
|
||||||
|
SUNRISE = 0
|
||||||
|
SUNSET = 1
|
||||||
|
CLOUDS = 2
|
||||||
|
|
||||||
|
|
||||||
|
class LIFXUpdateCoordinator(DataUpdateCoordinator[None]): # noqa: PLR0904
|
||||||
"""DataUpdateCoordinator to gather data for a specific lifx device."""
|
"""DataUpdateCoordinator to gather data for a specific lifx device."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
|
@ -128,14 +138,14 @@ class LIFXUpdateCoordinator(DataUpdateCoordinator[None]):
|
||||||
"""Return the current infrared brightness as a string."""
|
"""Return the current infrared brightness as a string."""
|
||||||
return infrared_brightness_value_to_option(self.device.infrared_brightness)
|
return infrared_brightness_value_to_option(self.device.infrared_brightness)
|
||||||
|
|
||||||
@property
|
@cached_property
|
||||||
def serial_number(self) -> str:
|
def serial_number(self) -> str:
|
||||||
"""Return the internal mac address."""
|
"""Return the internal mac address."""
|
||||||
return cast(
|
return cast(
|
||||||
str, self.device.mac_addr
|
str, self.device.mac_addr
|
||||||
) # device.mac_addr is not the mac_address, its the serial number
|
) # device.mac_addr is not the mac_address, its the serial number
|
||||||
|
|
||||||
@property
|
@cached_property
|
||||||
def mac_address(self) -> str:
|
def mac_address(self) -> str:
|
||||||
"""Return the physical mac address."""
|
"""Return the physical mac address."""
|
||||||
return get_real_mac_addr(
|
return get_real_mac_addr(
|
||||||
|
@ -149,6 +159,23 @@ class LIFXUpdateCoordinator(DataUpdateCoordinator[None]):
|
||||||
"""Return the label of the bulb."""
|
"""Return the label of the bulb."""
|
||||||
return cast(str, self.device.label)
|
return cast(str, self.device.label)
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def is_extended_multizone(self) -> bool:
|
||||||
|
"""Return true if this is a multizone device."""
|
||||||
|
return bool(lifx_features(self.device)["extended_multizone"])
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def is_legacy_multizone(self) -> bool:
|
||||||
|
"""Return true if this is a legacy multizone device."""
|
||||||
|
return bool(
|
||||||
|
lifx_features(self.device)["multizone"] and not self.is_extended_multizone
|
||||||
|
)
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def is_matrix(self) -> bool:
|
||||||
|
"""Return true if this is a matrix device."""
|
||||||
|
return bool(lifx_features(self.device)["matrix"])
|
||||||
|
|
||||||
async def diagnostics(self) -> dict[str, Any]:
|
async def diagnostics(self) -> dict[str, Any]:
|
||||||
"""Return diagnostic information about the device."""
|
"""Return diagnostic information about the device."""
|
||||||
features = lifx_features(self.device)
|
features = lifx_features(self.device)
|
||||||
|
@ -269,17 +296,23 @@ class LIFXUpdateCoordinator(DataUpdateCoordinator[None]):
|
||||||
|
|
||||||
num_zones = self.get_number_of_zones()
|
num_zones = self.get_number_of_zones()
|
||||||
features = lifx_features(self.device)
|
features = lifx_features(self.device)
|
||||||
is_extended_multizone = features["extended_multizone"]
|
|
||||||
is_legacy_multizone = not is_extended_multizone and features["multizone"]
|
|
||||||
update_rssi = self._update_rssi
|
update_rssi = self._update_rssi
|
||||||
methods: list[Callable] = [self.device.get_color]
|
methods: list[Callable] = [self.device.get_color]
|
||||||
if update_rssi:
|
if update_rssi:
|
||||||
methods.append(self.device.get_wifiinfo)
|
methods.append(self.device.get_wifiinfo)
|
||||||
if is_extended_multizone:
|
if self.is_matrix:
|
||||||
|
methods.extend(
|
||||||
|
[
|
||||||
|
self.device.get_tile_effect,
|
||||||
|
self.device.get_device_chain,
|
||||||
|
self.device.get64,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
if self.is_extended_multizone:
|
||||||
methods.append(self.device.get_extended_color_zones)
|
methods.append(self.device.get_extended_color_zones)
|
||||||
elif is_legacy_multizone:
|
elif self.is_legacy_multizone:
|
||||||
methods.extend(self._async_build_color_zones_update_requests())
|
methods.extend(self._async_build_color_zones_update_requests())
|
||||||
if is_extended_multizone or is_legacy_multizone:
|
if self.is_extended_multizone or self.is_legacy_multizone:
|
||||||
methods.append(self.device.get_multizone_effect)
|
methods.append(self.device.get_multizone_effect)
|
||||||
if features["hev"]:
|
if features["hev"]:
|
||||||
methods.append(self.device.get_hev_cycle)
|
methods.append(self.device.get_hev_cycle)
|
||||||
|
@ -297,9 +330,9 @@ class LIFXUpdateCoordinator(DataUpdateCoordinator[None]):
|
||||||
# We always send the rssi request second
|
# We always send the rssi request second
|
||||||
self._rssi = int(floor(10 * log10(responses[1].signal) + 0.5))
|
self._rssi = int(floor(10 * log10(responses[1].signal) + 0.5))
|
||||||
|
|
||||||
if is_extended_multizone or is_legacy_multizone:
|
if self.is_matrix or self.is_extended_multizone or self.is_legacy_multizone:
|
||||||
self.active_effect = FirmwareEffect[self.device.effect.get("effect", "OFF")]
|
self.active_effect = FirmwareEffect[self.device.effect.get("effect", "OFF")]
|
||||||
if is_legacy_multizone and num_zones != self.get_number_of_zones():
|
if self.is_legacy_multizone and num_zones != self.get_number_of_zones():
|
||||||
# The number of zones has changed so we need
|
# The number of zones has changed so we need
|
||||||
# to update the zones again. This happens rarely.
|
# to update the zones again. This happens rarely.
|
||||||
await self.async_get_color_zones()
|
await self.async_get_color_zones()
|
||||||
|
@ -402,7 +435,7 @@ class LIFXUpdateCoordinator(DataUpdateCoordinator[None]):
|
||||||
power_on: bool = True,
|
power_on: bool = True,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Control the firmware-based Move effect on a multizone device."""
|
"""Control the firmware-based Move effect on a multizone device."""
|
||||||
if lifx_features(self.device)["multizone"] is True:
|
if self.is_extended_multizone or self.is_legacy_multizone:
|
||||||
if power_on and self.device.power_level == 0:
|
if power_on and self.device.power_level == 0:
|
||||||
await self.async_set_power(True, 0)
|
await self.async_set_power(True, 0)
|
||||||
|
|
||||||
|
@ -422,27 +455,36 @@ class LIFXUpdateCoordinator(DataUpdateCoordinator[None]):
|
||||||
)
|
)
|
||||||
self.active_effect = FirmwareEffect[effect.upper()]
|
self.active_effect = FirmwareEffect[effect.upper()]
|
||||||
|
|
||||||
async def async_set_matrix_effect(
|
async def async_set_matrix_effect( # noqa: PLR0917
|
||||||
self,
|
self,
|
||||||
effect: str,
|
effect: str,
|
||||||
palette: list[tuple[int, int, int, int]] | None = None,
|
palette: list[tuple[int, int, int, int]] | None = None,
|
||||||
speed: float = 3,
|
speed: float | None = None,
|
||||||
power_on: bool = True,
|
power_on: bool = True,
|
||||||
|
sky_type: str | None = None,
|
||||||
|
cloud_saturation_min: int | None = None,
|
||||||
|
cloud_saturation_max: int | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Control the firmware-based effects on a matrix device."""
|
"""Control the firmware-based effects on a matrix device."""
|
||||||
if lifx_features(self.device)["matrix"] is True:
|
if self.is_matrix:
|
||||||
if power_on and self.device.power_level == 0:
|
if power_on and self.device.power_level == 0:
|
||||||
await self.async_set_power(True, 0)
|
await self.async_set_power(True, 0)
|
||||||
|
|
||||||
if palette is None:
|
if palette is None:
|
||||||
palette = []
|
palette = []
|
||||||
|
|
||||||
|
if sky_type is not None:
|
||||||
|
sky_type = TileEffectSkyType[sky_type.upper()].value
|
||||||
|
|
||||||
await async_execute_lifx(
|
await async_execute_lifx(
|
||||||
partial(
|
partial(
|
||||||
self.device.set_tile_effect,
|
self.device.set_tile_effect,
|
||||||
effect=TileEffectType[effect.upper()].value,
|
effect=TileEffectType[effect.upper()].value,
|
||||||
speed=speed,
|
speed=speed,
|
||||||
palette=palette,
|
palette=palette,
|
||||||
|
sky_type=sky_type,
|
||||||
|
cloud_saturation_min=cloud_saturation_min,
|
||||||
|
cloud_saturation_max=cloud_saturation_max,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
self.active_effect = FirmwareEffect[effect.upper()]
|
self.active_effect = FirmwareEffect[effect.upper()]
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
"effect_move": "mdi:cube-send",
|
"effect_move": "mdi:cube-send",
|
||||||
"effect_flame": "mdi:fire",
|
"effect_flame": "mdi:fire",
|
||||||
"effect_morph": "mdi:shape-outline",
|
"effect_morph": "mdi:shape-outline",
|
||||||
|
"effect_sky": "mdi:clouds",
|
||||||
"effect_stop": "mdi:stop"
|
"effect_stop": "mdi:stop"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,6 +36,7 @@ from .const import (
|
||||||
DATA_LIFX_MANAGER,
|
DATA_LIFX_MANAGER,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
INFRARED_BRIGHTNESS,
|
INFRARED_BRIGHTNESS,
|
||||||
|
LIFX_CEILING_PRODUCT_IDS,
|
||||||
)
|
)
|
||||||
from .coordinator import FirmwareEffect, LIFXUpdateCoordinator
|
from .coordinator import FirmwareEffect, LIFXUpdateCoordinator
|
||||||
from .entity import LIFXEntity
|
from .entity import LIFXEntity
|
||||||
|
@ -45,6 +46,7 @@ from .manager import (
|
||||||
SERVICE_EFFECT_MORPH,
|
SERVICE_EFFECT_MORPH,
|
||||||
SERVICE_EFFECT_MOVE,
|
SERVICE_EFFECT_MOVE,
|
||||||
SERVICE_EFFECT_PULSE,
|
SERVICE_EFFECT_PULSE,
|
||||||
|
SERVICE_EFFECT_SKY,
|
||||||
SERVICE_EFFECT_STOP,
|
SERVICE_EFFECT_STOP,
|
||||||
LIFXManager,
|
LIFXManager,
|
||||||
)
|
)
|
||||||
|
@ -97,7 +99,10 @@ async def async_setup_entry(
|
||||||
"set_hev_cycle_state",
|
"set_hev_cycle_state",
|
||||||
)
|
)
|
||||||
if lifx_features(device)["matrix"]:
|
if lifx_features(device)["matrix"]:
|
||||||
entity: LIFXLight = LIFXMatrix(coordinator, manager, entry)
|
if device.product in LIFX_CEILING_PRODUCT_IDS:
|
||||||
|
entity: LIFXLight = LIFXCeiling(coordinator, manager, entry)
|
||||||
|
else:
|
||||||
|
entity = LIFXMatrix(coordinator, manager, entry)
|
||||||
elif lifx_features(device)["extended_multizone"]:
|
elif lifx_features(device)["extended_multizone"]:
|
||||||
entity = LIFXExtendedMultiZone(coordinator, manager, entry)
|
entity = LIFXExtendedMultiZone(coordinator, manager, entry)
|
||||||
elif lifx_features(device)["multizone"]:
|
elif lifx_features(device)["multizone"]:
|
||||||
|
@ -499,3 +504,16 @@ class LIFXMatrix(LIFXColor):
|
||||||
SERVICE_EFFECT_MORPH,
|
SERVICE_EFFECT_MORPH,
|
||||||
SERVICE_EFFECT_STOP,
|
SERVICE_EFFECT_STOP,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class LIFXCeiling(LIFXMatrix):
|
||||||
|
"""Representation of a LIFX Ceiling device."""
|
||||||
|
|
||||||
|
_attr_effect_list = [
|
||||||
|
SERVICE_EFFECT_COLORLOOP,
|
||||||
|
SERVICE_EFFECT_FLAME,
|
||||||
|
SERVICE_EFFECT_PULSE,
|
||||||
|
SERVICE_EFFECT_MORPH,
|
||||||
|
SERVICE_EFFECT_SKY,
|
||||||
|
SERVICE_EFFECT_STOP,
|
||||||
|
]
|
||||||
|
|
|
@ -41,9 +41,12 @@ SERVICE_EFFECT_FLAME = "effect_flame"
|
||||||
SERVICE_EFFECT_MORPH = "effect_morph"
|
SERVICE_EFFECT_MORPH = "effect_morph"
|
||||||
SERVICE_EFFECT_MOVE = "effect_move"
|
SERVICE_EFFECT_MOVE = "effect_move"
|
||||||
SERVICE_EFFECT_PULSE = "effect_pulse"
|
SERVICE_EFFECT_PULSE = "effect_pulse"
|
||||||
|
SERVICE_EFFECT_SKY = "effect_sky"
|
||||||
SERVICE_EFFECT_STOP = "effect_stop"
|
SERVICE_EFFECT_STOP = "effect_stop"
|
||||||
|
|
||||||
ATTR_CHANGE = "change"
|
ATTR_CHANGE = "change"
|
||||||
|
ATTR_CLOUD_SATURATION_MIN = "cloud_saturation_min"
|
||||||
|
ATTR_CLOUD_SATURATION_MAX = "cloud_saturation_max"
|
||||||
ATTR_CYCLES = "cycles"
|
ATTR_CYCLES = "cycles"
|
||||||
ATTR_DIRECTION = "direction"
|
ATTR_DIRECTION = "direction"
|
||||||
ATTR_PALETTE = "palette"
|
ATTR_PALETTE = "palette"
|
||||||
|
@ -52,6 +55,7 @@ ATTR_POWER_OFF = "power_off"
|
||||||
ATTR_POWER_ON = "power_on"
|
ATTR_POWER_ON = "power_on"
|
||||||
ATTR_SATURATION_MAX = "saturation_max"
|
ATTR_SATURATION_MAX = "saturation_max"
|
||||||
ATTR_SATURATION_MIN = "saturation_min"
|
ATTR_SATURATION_MIN = "saturation_min"
|
||||||
|
ATTR_SKY_TYPE = "sky_type"
|
||||||
ATTR_SPEED = "speed"
|
ATTR_SPEED = "speed"
|
||||||
ATTR_SPREAD = "spread"
|
ATTR_SPREAD = "spread"
|
||||||
|
|
||||||
|
@ -59,6 +63,7 @@ EFFECT_FLAME = "FLAME"
|
||||||
EFFECT_MORPH = "MORPH"
|
EFFECT_MORPH = "MORPH"
|
||||||
EFFECT_MOVE = "MOVE"
|
EFFECT_MOVE = "MOVE"
|
||||||
EFFECT_OFF = "OFF"
|
EFFECT_OFF = "OFF"
|
||||||
|
EFFECT_SKY = "SKY"
|
||||||
|
|
||||||
EFFECT_FLAME_DEFAULT_SPEED = 3
|
EFFECT_FLAME_DEFAULT_SPEED = 3
|
||||||
|
|
||||||
|
@ -72,6 +77,13 @@ EFFECT_MOVE_DIRECTION_LEFT = "left"
|
||||||
|
|
||||||
EFFECT_MOVE_DIRECTIONS = [EFFECT_MOVE_DIRECTION_LEFT, EFFECT_MOVE_DIRECTION_RIGHT]
|
EFFECT_MOVE_DIRECTIONS = [EFFECT_MOVE_DIRECTION_LEFT, EFFECT_MOVE_DIRECTION_RIGHT]
|
||||||
|
|
||||||
|
EFFECT_SKY_DEFAULT_SPEED = 50
|
||||||
|
EFFECT_SKY_DEFAULT_SKY_TYPE = "Clouds"
|
||||||
|
EFFECT_SKY_DEFAULT_CLOUD_SATURATION_MIN = 50
|
||||||
|
EFFECT_SKY_DEFAULT_CLOUD_SATURATION_MAX = 180
|
||||||
|
|
||||||
|
EFFECT_SKY_SKY_TYPES = ["Sunrise", "Sunset", "Clouds"]
|
||||||
|
|
||||||
PULSE_MODE_BLINK = "blink"
|
PULSE_MODE_BLINK = "blink"
|
||||||
PULSE_MODE_BREATHE = "breathe"
|
PULSE_MODE_BREATHE = "breathe"
|
||||||
PULSE_MODE_PING = "ping"
|
PULSE_MODE_PING = "ping"
|
||||||
|
@ -137,13 +149,6 @@ LIFX_EFFECT_COLORLOOP_SCHEMA = cv.make_entity_service_schema(
|
||||||
|
|
||||||
LIFX_EFFECT_STOP_SCHEMA = cv.make_entity_service_schema({})
|
LIFX_EFFECT_STOP_SCHEMA = cv.make_entity_service_schema({})
|
||||||
|
|
||||||
SERVICES = (
|
|
||||||
SERVICE_EFFECT_STOP,
|
|
||||||
SERVICE_EFFECT_PULSE,
|
|
||||||
SERVICE_EFFECT_MOVE,
|
|
||||||
SERVICE_EFFECT_COLORLOOP,
|
|
||||||
)
|
|
||||||
|
|
||||||
LIFX_EFFECT_FLAME_SCHEMA = cv.make_entity_service_schema(
|
LIFX_EFFECT_FLAME_SCHEMA = cv.make_entity_service_schema(
|
||||||
{
|
{
|
||||||
**LIFX_EFFECT_SCHEMA,
|
**LIFX_EFFECT_SCHEMA,
|
||||||
|
@ -185,6 +190,28 @@ LIFX_EFFECT_MOVE_SCHEMA = cv.make_entity_service_schema(
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
LIFX_EFFECT_SKY_SCHEMA = cv.make_entity_service_schema(
|
||||||
|
{
|
||||||
|
**LIFX_EFFECT_SCHEMA,
|
||||||
|
ATTR_SPEED: vol.All(vol.Coerce(int), vol.Clamp(min=1, max=86400)),
|
||||||
|
ATTR_SKY_TYPE: vol.In(EFFECT_SKY_SKY_TYPES),
|
||||||
|
ATTR_CLOUD_SATURATION_MIN: vol.All(vol.Coerce(int), vol.Clamp(min=0, max=255)),
|
||||||
|
ATTR_CLOUD_SATURATION_MAX: vol.All(vol.Coerce(int), vol.Clamp(min=0, max=255)),
|
||||||
|
ATTR_PALETTE: vol.All(cv.ensure_list, [HSBK_SCHEMA]),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
SERVICES = (
|
||||||
|
SERVICE_EFFECT_COLORLOOP,
|
||||||
|
SERVICE_EFFECT_FLAME,
|
||||||
|
SERVICE_EFFECT_MORPH,
|
||||||
|
SERVICE_EFFECT_MOVE,
|
||||||
|
SERVICE_EFFECT_PULSE,
|
||||||
|
SERVICE_EFFECT_SKY,
|
||||||
|
SERVICE_EFFECT_STOP,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class LIFXManager:
|
class LIFXManager:
|
||||||
"""Representation of all known LIFX entities."""
|
"""Representation of all known LIFX entities."""
|
||||||
|
@ -261,6 +288,13 @@ class LIFXManager:
|
||||||
schema=LIFX_EFFECT_MOVE_SCHEMA,
|
schema=LIFX_EFFECT_MOVE_SCHEMA,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self.hass.services.async_register(
|
||||||
|
DOMAIN,
|
||||||
|
SERVICE_EFFECT_SKY,
|
||||||
|
service_handler,
|
||||||
|
schema=LIFX_EFFECT_SKY_SCHEMA,
|
||||||
|
)
|
||||||
|
|
||||||
self.hass.services.async_register(
|
self.hass.services.async_register(
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
SERVICE_EFFECT_STOP,
|
SERVICE_EFFECT_STOP,
|
||||||
|
@ -375,6 +409,39 @@ class LIFXManager:
|
||||||
)
|
)
|
||||||
await self.effects_conductor.start(effect, bulbs)
|
await self.effects_conductor.start(effect, bulbs)
|
||||||
|
|
||||||
|
elif service == SERVICE_EFFECT_SKY:
|
||||||
|
palette = kwargs.get(ATTR_PALETTE, None)
|
||||||
|
if palette is not None:
|
||||||
|
theme = Theme()
|
||||||
|
for hsbk in palette:
|
||||||
|
theme.add_hsbk(hsbk[0], hsbk[1], hsbk[2], hsbk[3])
|
||||||
|
|
||||||
|
speed = kwargs.get(ATTR_SPEED, EFFECT_SKY_DEFAULT_SPEED)
|
||||||
|
sky_type = kwargs.get(ATTR_SKY_TYPE, EFFECT_SKY_DEFAULT_SKY_TYPE)
|
||||||
|
|
||||||
|
cloud_saturation_min = kwargs.get(
|
||||||
|
ATTR_CLOUD_SATURATION_MIN,
|
||||||
|
EFFECT_SKY_DEFAULT_CLOUD_SATURATION_MIN,
|
||||||
|
)
|
||||||
|
cloud_saturation_max = kwargs.get(
|
||||||
|
ATTR_CLOUD_SATURATION_MAX,
|
||||||
|
EFFECT_SKY_DEFAULT_CLOUD_SATURATION_MAX,
|
||||||
|
)
|
||||||
|
|
||||||
|
await asyncio.gather(
|
||||||
|
*(
|
||||||
|
coordinator.async_set_matrix_effect(
|
||||||
|
effect=EFFECT_SKY,
|
||||||
|
speed=speed,
|
||||||
|
sky_type=sky_type,
|
||||||
|
cloud_saturation_min=cloud_saturation_min,
|
||||||
|
cloud_saturation_max=cloud_saturation_max,
|
||||||
|
palette=theme.colors,
|
||||||
|
)
|
||||||
|
for coordinator in coordinators
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
elif service == SERVICE_EFFECT_STOP:
|
elif service == SERVICE_EFFECT_STOP:
|
||||||
await self.effects_conductor.stop(bulbs)
|
await self.effects_conductor.stop(bulbs)
|
||||||
|
|
||||||
|
|
|
@ -281,6 +281,58 @@ effect_morph:
|
||||||
default: true
|
default: true
|
||||||
selector:
|
selector:
|
||||||
boolean:
|
boolean:
|
||||||
|
effect_sky:
|
||||||
|
target:
|
||||||
|
entity:
|
||||||
|
integration: lifx
|
||||||
|
domain: light
|
||||||
|
fields:
|
||||||
|
power_on:
|
||||||
|
default: true
|
||||||
|
selector:
|
||||||
|
boolean:
|
||||||
|
speed:
|
||||||
|
default: 50
|
||||||
|
example: 50
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
min: 1
|
||||||
|
max: 86400
|
||||||
|
step: 1
|
||||||
|
unit_of_measurement: seconds
|
||||||
|
sky_type:
|
||||||
|
default: "Clouds"
|
||||||
|
example: "Clouds"
|
||||||
|
selector:
|
||||||
|
select:
|
||||||
|
options:
|
||||||
|
- "Clouds"
|
||||||
|
- "Sunrise"
|
||||||
|
- "Sunset"
|
||||||
|
cloud_saturation_min:
|
||||||
|
default: 50
|
||||||
|
example: 50
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
min: 0
|
||||||
|
max: 255
|
||||||
|
cloud_saturation_max:
|
||||||
|
default: 180
|
||||||
|
example: 180
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
min: 0
|
||||||
|
max: 255
|
||||||
|
palette:
|
||||||
|
example:
|
||||||
|
- "[200, 1, 1, 3500]"
|
||||||
|
- "[241, 1, 0.01, 3500]"
|
||||||
|
- "[189, 1, 0.08, 3500]"
|
||||||
|
- "[40, 1, 1, 3500]"
|
||||||
|
- "[40, 0.5, 1, 3500]"
|
||||||
|
- "[40, 0, 1, 6500]"
|
||||||
|
selector:
|
||||||
|
object:
|
||||||
effect_stop:
|
effect_stop:
|
||||||
target:
|
target:
|
||||||
entity:
|
entity:
|
||||||
|
|
|
@ -220,6 +220,36 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"effect_sky": {
|
||||||
|
"name": "Sky effect",
|
||||||
|
"description": "Starts the firmware-based Sky effect on LIFX Ceiling.",
|
||||||
|
"fields": {
|
||||||
|
"speed": {
|
||||||
|
"name": "Speed",
|
||||||
|
"description": "How long the Sunrise and Sunset sky types will take to complete. For the Cloud sky type, it is the speed of the clouds across the device."
|
||||||
|
},
|
||||||
|
"sky_type": {
|
||||||
|
"name": "Sky type",
|
||||||
|
"description": "The style of sky that will be animated by the effect."
|
||||||
|
},
|
||||||
|
"cloud_saturation_min": {
|
||||||
|
"name": "Cloud saturation Minimum",
|
||||||
|
"description": "Minimum cloud saturation."
|
||||||
|
},
|
||||||
|
"cloud_saturation_max": {
|
||||||
|
"name": "Cloud Saturation maximum",
|
||||||
|
"description": "Maximum cloud saturation."
|
||||||
|
},
|
||||||
|
"palette": {
|
||||||
|
"name": "Palette",
|
||||||
|
"description": "List of 1 to 6 colors as hue (0-360), saturation (0-100), brightness (0-100) and kelvin (1500-9000) values to use for this effect."
|
||||||
|
},
|
||||||
|
"power_on": {
|
||||||
|
"name": "Power on",
|
||||||
|
"description": "[%key:component::lifx::services::effect_move::fields::power_on::description%]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"effect_stop": {
|
"effect_stop": {
|
||||||
"name": "Stop effect",
|
"name": "Stop effect",
|
||||||
"description": "Stops a running effect."
|
"description": "Stops a running effect."
|
||||||
|
|
|
@ -172,6 +172,19 @@ def _mocked_tile() -> Light:
|
||||||
bulb.effect = {"effect": "OFF"}
|
bulb.effect = {"effect": "OFF"}
|
||||||
bulb.get_tile_effect = MockLifxCommand(bulb)
|
bulb.get_tile_effect = MockLifxCommand(bulb)
|
||||||
bulb.set_tile_effect = MockLifxCommand(bulb)
|
bulb.set_tile_effect = MockLifxCommand(bulb)
|
||||||
|
bulb.get64 = MockLifxCommand(bulb)
|
||||||
|
bulb.get_device_chain = MockLifxCommand(bulb)
|
||||||
|
return bulb
|
||||||
|
|
||||||
|
|
||||||
|
def _mocked_ceiling() -> Light:
|
||||||
|
bulb = _mocked_bulb()
|
||||||
|
bulb.product = 176 # LIFX Ceiling
|
||||||
|
bulb.effect = {"effect": "OFF"}
|
||||||
|
bulb.get_tile_effect = MockLifxCommand(bulb)
|
||||||
|
bulb.set_tile_effect = MockLifxCommand(bulb)
|
||||||
|
bulb.get64 = MockLifxCommand(bulb)
|
||||||
|
bulb.get_device_chain = MockLifxCommand(bulb)
|
||||||
return bulb
|
return bulb
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -11,15 +11,19 @@ from homeassistant.components.lifx import DOMAIN
|
||||||
from homeassistant.components.lifx.const import ATTR_POWER
|
from homeassistant.components.lifx.const import ATTR_POWER
|
||||||
from homeassistant.components.lifx.light import ATTR_INFRARED, ATTR_ZONES
|
from homeassistant.components.lifx.light import ATTR_INFRARED, ATTR_ZONES
|
||||||
from homeassistant.components.lifx.manager import (
|
from homeassistant.components.lifx.manager import (
|
||||||
|
ATTR_CLOUD_SATURATION_MAX,
|
||||||
|
ATTR_CLOUD_SATURATION_MIN,
|
||||||
ATTR_DIRECTION,
|
ATTR_DIRECTION,
|
||||||
ATTR_PALETTE,
|
ATTR_PALETTE,
|
||||||
ATTR_SATURATION_MAX,
|
ATTR_SATURATION_MAX,
|
||||||
ATTR_SATURATION_MIN,
|
ATTR_SATURATION_MIN,
|
||||||
|
ATTR_SKY_TYPE,
|
||||||
ATTR_SPEED,
|
ATTR_SPEED,
|
||||||
ATTR_THEME,
|
ATTR_THEME,
|
||||||
SERVICE_EFFECT_COLORLOOP,
|
SERVICE_EFFECT_COLORLOOP,
|
||||||
SERVICE_EFFECT_MORPH,
|
SERVICE_EFFECT_MORPH,
|
||||||
SERVICE_EFFECT_MOVE,
|
SERVICE_EFFECT_MOVE,
|
||||||
|
SERVICE_EFFECT_SKY,
|
||||||
)
|
)
|
||||||
from homeassistant.components.light import (
|
from homeassistant.components.light import (
|
||||||
ATTR_BRIGHTNESS,
|
ATTR_BRIGHTNESS,
|
||||||
|
@ -62,6 +66,7 @@ from . import (
|
||||||
_mocked_brightness_bulb,
|
_mocked_brightness_bulb,
|
||||||
_mocked_bulb,
|
_mocked_bulb,
|
||||||
_mocked_bulb_new_firmware,
|
_mocked_bulb_new_firmware,
|
||||||
|
_mocked_ceiling,
|
||||||
_mocked_clean_bulb,
|
_mocked_clean_bulb,
|
||||||
_mocked_light_strip,
|
_mocked_light_strip,
|
||||||
_mocked_tile,
|
_mocked_tile,
|
||||||
|
@ -691,6 +696,7 @@ async def test_matrix_flame_morph_effects(hass: HomeAssistant) -> None:
|
||||||
|
|
||||||
entity_id = "light.my_bulb"
|
entity_id = "light.my_bulb"
|
||||||
|
|
||||||
|
# FLAME effect test
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
LIGHT_DOMAIN,
|
LIGHT_DOMAIN,
|
||||||
"turn_on",
|
"turn_on",
|
||||||
|
@ -707,11 +713,15 @@ async def test_matrix_flame_morph_effects(hass: HomeAssistant) -> None:
|
||||||
"effect": 3,
|
"effect": 3,
|
||||||
"speed": 3,
|
"speed": 3,
|
||||||
"palette": [],
|
"palette": [],
|
||||||
|
"sky_type": None,
|
||||||
|
"cloud_saturation_min": None,
|
||||||
|
"cloud_saturation_max": None,
|
||||||
}
|
}
|
||||||
bulb.get_tile_effect.reset_mock()
|
bulb.get_tile_effect.reset_mock()
|
||||||
bulb.set_tile_effect.reset_mock()
|
bulb.set_tile_effect.reset_mock()
|
||||||
bulb.set_power.reset_mock()
|
bulb.set_power.reset_mock()
|
||||||
|
|
||||||
|
# MORPH effect tests
|
||||||
bulb.power_level = 0
|
bulb.power_level = 0
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
|
@ -750,6 +760,9 @@ async def test_matrix_flame_morph_effects(hass: HomeAssistant) -> None:
|
||||||
(8920, 65535, 32768, 3500),
|
(8920, 65535, 32768, 3500),
|
||||||
(10558, 65535, 32768, 3500),
|
(10558, 65535, 32768, 3500),
|
||||||
],
|
],
|
||||||
|
"sky_type": None,
|
||||||
|
"cloud_saturation_min": None,
|
||||||
|
"cloud_saturation_max": None,
|
||||||
}
|
}
|
||||||
bulb.get_tile_effect.reset_mock()
|
bulb.get_tile_effect.reset_mock()
|
||||||
bulb.set_tile_effect.reset_mock()
|
bulb.set_tile_effect.reset_mock()
|
||||||
|
@ -808,6 +821,140 @@ async def test_matrix_flame_morph_effects(hass: HomeAssistant) -> None:
|
||||||
(43690, 65535, 65535, 3500),
|
(43690, 65535, 65535, 3500),
|
||||||
(54613, 65535, 65535, 3500),
|
(54613, 65535, 65535, 3500),
|
||||||
],
|
],
|
||||||
|
"sky_type": None,
|
||||||
|
"cloud_saturation_min": None,
|
||||||
|
"cloud_saturation_max": None,
|
||||||
|
}
|
||||||
|
bulb.get_tile_effect.reset_mock()
|
||||||
|
bulb.set_tile_effect.reset_mock()
|
||||||
|
bulb.set_power.reset_mock()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("mock_discovery")
|
||||||
|
async def test_sky_effect(hass: HomeAssistant) -> None:
|
||||||
|
"""Test the firmware sky effect on a ceiling device."""
|
||||||
|
config_entry = MockConfigEntry(
|
||||||
|
domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=SERIAL
|
||||||
|
)
|
||||||
|
config_entry.add_to_hass(hass)
|
||||||
|
bulb = _mocked_ceiling()
|
||||||
|
bulb.power_level = 0
|
||||||
|
bulb.color = [65535, 65535, 65535, 65535]
|
||||||
|
with (
|
||||||
|
_patch_discovery(device=bulb),
|
||||||
|
_patch_config_flow_try_connect(device=bulb),
|
||||||
|
_patch_device(device=bulb),
|
||||||
|
):
|
||||||
|
await async_setup_component(hass, lifx.DOMAIN, {lifx.DOMAIN: {}})
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
entity_id = "light.my_bulb"
|
||||||
|
|
||||||
|
# SKY effect test
|
||||||
|
bulb.power_level = 0
|
||||||
|
await hass.services.async_call(
|
||||||
|
DOMAIN,
|
||||||
|
SERVICE_EFFECT_SKY,
|
||||||
|
{
|
||||||
|
ATTR_ENTITY_ID: entity_id,
|
||||||
|
ATTR_PALETTE: [],
|
||||||
|
ATTR_SKY_TYPE: "Clouds",
|
||||||
|
ATTR_CLOUD_SATURATION_MAX: 180,
|
||||||
|
ATTR_CLOUD_SATURATION_MIN: 50,
|
||||||
|
},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
bulb.power_level = 65535
|
||||||
|
bulb.effect = {
|
||||||
|
"effect": "SKY",
|
||||||
|
"palette": [],
|
||||||
|
"sky_type": 2,
|
||||||
|
"cloud_saturation_min": 50,
|
||||||
|
"cloud_saturation_max": 180,
|
||||||
|
}
|
||||||
|
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=30))
|
||||||
|
await hass.async_block_till_done(wait_background_tasks=True)
|
||||||
|
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
assert state.state == STATE_ON
|
||||||
|
|
||||||
|
assert len(bulb.set_power.calls) == 1
|
||||||
|
assert len(bulb.set_tile_effect.calls) == 1
|
||||||
|
call_dict = bulb.set_tile_effect.calls[0][1]
|
||||||
|
call_dict.pop("callb")
|
||||||
|
assert call_dict == {
|
||||||
|
"effect": 5,
|
||||||
|
"speed": 50,
|
||||||
|
"palette": [],
|
||||||
|
"sky_type": 2,
|
||||||
|
"cloud_saturation_min": 50,
|
||||||
|
"cloud_saturation_max": 180,
|
||||||
|
}
|
||||||
|
bulb.get_tile_effect.reset_mock()
|
||||||
|
bulb.set_tile_effect.reset_mock()
|
||||||
|
bulb.set_power.reset_mock()
|
||||||
|
|
||||||
|
bulb.power_level = 0
|
||||||
|
await hass.services.async_call(
|
||||||
|
DOMAIN,
|
||||||
|
SERVICE_EFFECT_SKY,
|
||||||
|
{
|
||||||
|
ATTR_ENTITY_ID: entity_id,
|
||||||
|
ATTR_PALETTE: [
|
||||||
|
(200, 100, 1, 3500),
|
||||||
|
(241, 100, 1, 3500),
|
||||||
|
(189, 100, 8, 3500),
|
||||||
|
(40, 100, 100, 3500),
|
||||||
|
(40, 50, 100, 3500),
|
||||||
|
(0, 0, 100, 6500),
|
||||||
|
],
|
||||||
|
ATTR_SKY_TYPE: "Sunrise",
|
||||||
|
ATTR_CLOUD_SATURATION_MAX: 180,
|
||||||
|
ATTR_CLOUD_SATURATION_MIN: 50,
|
||||||
|
},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
bulb.power_level = 65535
|
||||||
|
bulb.effect = {
|
||||||
|
"effect": "SKY",
|
||||||
|
"palette": [
|
||||||
|
(200, 100, 1, 3500),
|
||||||
|
(241, 100, 1, 3500),
|
||||||
|
(189, 100, 8, 3500),
|
||||||
|
(40, 100, 100, 3500),
|
||||||
|
(40, 50, 100, 3500),
|
||||||
|
(0, 0, 100, 6500),
|
||||||
|
],
|
||||||
|
"sky_type": 0,
|
||||||
|
"cloud_saturation_min": 50,
|
||||||
|
"cloud_saturation_max": 180,
|
||||||
|
}
|
||||||
|
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=30))
|
||||||
|
await hass.async_block_till_done(wait_background_tasks=True)
|
||||||
|
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
assert state.state == STATE_ON
|
||||||
|
|
||||||
|
assert len(bulb.set_power.calls) == 1
|
||||||
|
assert len(bulb.set_tile_effect.calls) == 1
|
||||||
|
call_dict = bulb.set_tile_effect.calls[0][1]
|
||||||
|
call_dict.pop("callb")
|
||||||
|
assert call_dict == {
|
||||||
|
"effect": 5,
|
||||||
|
"speed": 50,
|
||||||
|
"palette": [
|
||||||
|
(36408, 65535, 65535, 3500),
|
||||||
|
(43872, 65535, 65535, 3500),
|
||||||
|
(34406, 65535, 5243, 3500),
|
||||||
|
(7281, 65535, 65535, 3500),
|
||||||
|
(7281, 32768, 65535, 3500),
|
||||||
|
(0, 0, 65535, 6500),
|
||||||
|
],
|
||||||
|
"sky_type": 0,
|
||||||
|
"cloud_saturation_min": 50,
|
||||||
|
"cloud_saturation_max": 180,
|
||||||
}
|
}
|
||||||
bulb.get_tile_effect.reset_mock()
|
bulb.get_tile_effect.reset_mock()
|
||||||
bulb.set_tile_effect.reset_mock()
|
bulb.set_tile_effect.reset_mock()
|
||||||
|
|
Loading…
Add table
Reference in a new issue