Add support for the Flame and Morph effects for Tile and Candle (#80014)

This commit is contained in:
Avi Miller 2022-10-11 06:01:31 +11:00 committed by GitHub
parent 117c12d135
commit 257ae4d8d3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 405 additions and 15 deletions

View File

@ -7,7 +7,12 @@ from enum import IntEnum
from functools import partial
from typing import Any, cast
from aiolifx.aiolifx import Light, MultiZoneDirection, MultiZoneEffectType
from aiolifx.aiolifx import (
Light,
MultiZoneDirection,
MultiZoneEffectType,
TileEffectType,
)
from aiolifx.connection import LIFXConnection
from homeassistant.const import Platform
@ -279,7 +284,11 @@ class LIFXUpdateCoordinator(DataUpdateCoordinator):
self.active_effect = FirmwareEffect[self.device.effect.get("effect", "OFF")]
async def async_set_multizone_effect(
self, effect: str, speed: float, direction: str, power_on: bool = True
self,
effect: str,
speed: float = 3,
direction: str = "RIGHT",
power_on: bool = True,
) -> None:
"""Control the firmware-based Move effect on a multizone device."""
if lifx_features(self.device)["multizone"] is True:
@ -296,6 +305,31 @@ class LIFXUpdateCoordinator(DataUpdateCoordinator):
)
self.active_effect = FirmwareEffect[effect.upper()]
async def async_set_matrix_effect(
self,
effect: str,
palette: list[tuple[int, int, int, int]] | None = None,
speed: float = 3,
power_on: bool = True,
) -> None:
"""Control the firmware-based effects on a matrix device."""
if lifx_features(self.device)["matrix"] is True:
if power_on and self.device.power_level == 0:
await self.async_set_power(True, 0)
if palette is None:
palette = []
await async_execute_lifx(
partial(
self.device.set_tile_effect,
effect=TileEffectType[effect.upper()].value,
speed=speed,
palette=palette,
)
)
self.active_effect = FirmwareEffect[effect.upper()]
def async_get_active_effect(self) -> int:
"""Return the enum value of the currently active firmware effect."""
return self.active_effect.value

View File

@ -40,6 +40,8 @@ from .coordinator import FirmwareEffect, LIFXUpdateCoordinator
from .entity import LIFXEntity
from .manager import (
SERVICE_EFFECT_COLORLOOP,
SERVICE_EFFECT_FLAME,
SERVICE_EFFECT_MORPH,
SERVICE_EFFECT_MOVE,
SERVICE_EFFECT_PULSE,
SERVICE_EFFECT_STOP,
@ -93,8 +95,10 @@ async def async_setup_entry(
LIFX_SET_HEV_CYCLE_STATE_SCHEMA,
"set_hev_cycle_state",
)
if lifx_features(device)["extended_multizone"]:
entity: LIFXLight = LIFXExtendedMultiZone(coordinator, manager, entry)
if lifx_features(device)["matrix"]:
entity: LIFXLight = LIFXMatrix(coordinator, manager, entry)
elif lifx_features(device)["extended_multizone"]:
entity = LIFXExtendedMultiZone(coordinator, manager, entry)
elif lifx_features(device)["multizone"]:
entity = LIFXMultiZone(coordinator, manager, entry)
elif lifx_features(device)["color"]:
@ -471,3 +475,15 @@ class LIFXExtendedMultiZone(LIFXMultiZone):
# set_extended_color_zones does not update the
# state of the device, so we need to do that
await self.get_color()
class LIFXMatrix(LIFXColor):
"""Representation of a LIFX matrix device."""
_attr_effect_list = [
SERVICE_EFFECT_COLORLOOP,
SERVICE_EFFECT_FLAME,
SERVICE_EFFECT_PULSE,
SERVICE_EFFECT_MORPH,
SERVICE_EFFECT_STOP,
]

View File

@ -7,6 +7,7 @@ from datetime import timedelta
from typing import Any
import aiolifx_effects
from aiolifx_themes.themes import Theme, ThemeLibrary
import voluptuous as vol
from homeassistant.components.light import (
@ -34,9 +35,11 @@ from .util import convert_8_to_16, find_hsbk
SCAN_INTERVAL = timedelta(seconds=10)
SERVICE_EFFECT_PULSE = "effect_pulse"
SERVICE_EFFECT_COLORLOOP = "effect_colorloop"
SERVICE_EFFECT_FLAME = "effect_flame"
SERVICE_EFFECT_MORPH = "effect_morph"
SERVICE_EFFECT_MOVE = "effect_move"
SERVICE_EFFECT_PULSE = "effect_pulse"
SERVICE_EFFECT_STOP = "effect_stop"
ATTR_POWER_OFF = "power_off"
@ -47,11 +50,20 @@ ATTR_SPREAD = "spread"
ATTR_CHANGE = "change"
ATTR_DIRECTION = "direction"
ATTR_SPEED = "speed"
ATTR_PALETTE = "palette"
ATTR_THEME = "theme"
EFFECT_FLAME = "FLAME"
EFFECT_MORPH = "MORPH"
EFFECT_MOVE = "MOVE"
EFFECT_OFF = "OFF"
EFFECT_MOVE_DEFAULT_SPEED = 3.0
EFFECT_FLAME_DEFAULT_SPEED = 3
EFFECT_MORPH_DEFAULT_SPEED = 3
EFFECT_MORPH_DEFAULT_THEME = "exciting"
EFFECT_MOVE_DEFAULT_SPEED = 3
EFFECT_MOVE_DEFAULT_DIRECTION = "right"
EFFECT_MOVE_DIRECTION_RIGHT = "right"
EFFECT_MOVE_DIRECTION_LEFT = "left"
@ -128,6 +140,37 @@ SERVICES = (
SERVICE_EFFECT_COLORLOOP,
)
LIFX_EFFECT_FLAME_SCHEMA = cv.make_entity_service_schema(
{
**LIFX_EFFECT_SCHEMA,
ATTR_SPEED: vol.All(vol.Coerce(int), vol.Clamp(min=1, max=25)),
}
)
HSBK_SCHEMA = vol.All(
vol.Coerce(tuple),
vol.ExactSequence(
(
vol.All(vol.Coerce(float), vol.Range(min=0, max=360)),
vol.All(vol.Coerce(float), vol.Range(min=0, max=100)),
vol.All(vol.Coerce(float), vol.Clamp(min=0, max=100)),
vol.All(vol.Coerce(int), vol.Clamp(min=1500, max=9000)),
)
),
)
LIFX_EFFECT_MORPH_SCHEMA = cv.make_entity_service_schema(
{
**LIFX_EFFECT_SCHEMA,
ATTR_SPEED: vol.All(vol.Coerce(int), vol.Clamp(min=1, max=25)),
vol.Exclusive(ATTR_THEME, COLOR_GROUP): vol.Optional(
vol.In(ThemeLibrary().themes)
),
vol.Exclusive(ATTR_PALETTE, COLOR_GROUP): vol.All(
cv.ensure_list, [HSBK_SCHEMA]
),
}
)
LIFX_EFFECT_MOVE_SCHEMA = cv.make_entity_service_schema(
{
@ -192,6 +235,20 @@ class LIFXManager:
schema=LIFX_EFFECT_COLORLOOP_SCHEMA,
)
self.hass.services.async_register(
DOMAIN,
SERVICE_EFFECT_FLAME,
service_handler,
schema=LIFX_EFFECT_FLAME_SCHEMA,
)
self.hass.services.async_register(
DOMAIN,
SERVICE_EFFECT_MORPH,
service_handler,
schema=LIFX_EFFECT_MORPH_SCHEMA,
)
self.hass.services.async_register(
DOMAIN,
SERVICE_EFFECT_MOVE,
@ -222,7 +279,43 @@ class LIFXManager:
coordinators.append(coordinator)
bulbs.append(coordinator.device)
if service == SERVICE_EFFECT_MOVE:
if service == SERVICE_EFFECT_FLAME:
await asyncio.gather(
*(
coordinator.async_set_matrix_effect(
effect=EFFECT_FLAME,
speed=kwargs.get(ATTR_SPEED, EFFECT_FLAME_DEFAULT_SPEED),
power_on=kwargs.get(ATTR_POWER_ON, True),
)
for coordinator in coordinators
)
)
elif service == SERVICE_EFFECT_MORPH:
theme_name = kwargs.get(ATTR_THEME, "exciting")
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])
else:
theme = ThemeLibrary().get_theme(theme_name)
await asyncio.gather(
*(
coordinator.async_set_matrix_effect(
effect=EFFECT_MORPH,
speed=kwargs.get(ATTR_SPEED, EFFECT_MORPH_DEFAULT_SPEED),
palette=theme.colors,
power_on=kwargs.get(ATTR_POWER_ON, True),
)
for coordinator in coordinators
)
)
elif service == SERVICE_EFFECT_MOVE:
await asyncio.gather(
*(
coordinator.async_set_multizone_effect(
@ -269,9 +362,9 @@ class LIFXManager:
await self.effects_conductor.stop(bulbs)
for coordinator in coordinators:
await coordinator.async_set_multizone_effect(
effect=EFFECT_OFF,
speed=EFFECT_MOVE_DEFAULT_SPEED,
direction=EFFECT_MOVE_DEFAULT_DIRECTION,
power_on=False,
await coordinator.async_set_matrix_effect(
effect=EFFECT_OFF, power_on=False
)
await coordinator.async_set_multizone_effect(
effect=EFFECT_OFF, power_on=False
)

View File

@ -3,7 +3,11 @@
"name": "LIFX",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/lifx",
"requirements": ["aiolifx==0.8.6", "aiolifx_effects==0.2.2"],
"requirements": [
"aiolifx==0.8.6",
"aiolifx_effects==0.2.2",
"aiolifx_themes==0.1.1"
],
"quality_scale": "platinum",
"dependencies": ["network"],
"homekit": {

View File

@ -205,7 +205,91 @@ effect_move:
default: true
selector:
boolean:
effect_flame:
name: Flame effect
description: Start the firmware-based Flame effect on LIFX Tiles or Candle.
target:
entity:
integration: lifx
domain: light
fields:
speed:
name: Speed
description: How fast the flames will move.
default: 3
selector:
number:
min: 1
max: 25
step: 1
unit_of_measurement: seconds
power_on:
name: Power on
description: Powered off lights will be turned on before starting the effect.
default: true
selector:
boolean:
effect_morph:
name: Morph effect
description: Start the firmware-based Morph effect on LIFX Tiles on Candle.
target:
entity:
integration: lifx
domain: light
fields:
speed:
name: Speed
description: How fast the colors will move.
default: 3
selector:
number:
min: 1
max: 25
step: 1
unit_of_measurement: seconds
palette:
name: Palette
description: List of at least 2 and at most 16 colors as hue (0-360), saturation (0-100), brightness (0-100) and kelvin (1500-900) values to use for this effect. Overrides the theme attribute.
example:
- "[[0, 100, 100, 3500], [60, 100, 100, 3500]]"
selector:
object:
theme:
name: Theme
description: Predefined color theme to use for the effect. Overridden by the palette attribute.
selector:
select:
options:
- "autumn"
- "blissful"
- "cheerful"
- "dream"
- "energizing"
- "epic"
- "exciting"
- "focusing"
- "halloween"
- "hanukkah"
- "holly"
- "independence day"
- "intense"
- "mellow"
- "peaceful"
- "powerful"
- "relaxing"
- "santa"
- "serene"
- "soothing"
- "sports"
- "spring"
- "tranquil"
- "warming"
power_on:
name: Power on
description: Powered off lights will be turned on before starting the effect.
default: true
selector:
boolean:
effect_stop:
name: Stop effect
description: Stop a running effect.

View File

@ -198,6 +198,9 @@ aiolifx==0.8.6
# homeassistant.components.lifx
aiolifx_effects==0.2.2
# homeassistant.components.lifx
aiolifx_themes==0.1.1
# homeassistant.components.lookin
aiolookin==0.1.1

View File

@ -176,6 +176,9 @@ aiolifx==0.8.6
# homeassistant.components.lifx
aiolifx_effects==0.2.2
# homeassistant.components.lifx
aiolifx_themes==0.1.1
# homeassistant.components.lookin
aiolookin==0.1.1

View File

@ -156,6 +156,15 @@ def _mocked_light_strip() -> Light:
return bulb
def _mocked_tile() -> Light:
bulb = _mocked_bulb()
bulb.product = 55 # LIFX Tile
bulb.effect = {"effect": "OFF"}
bulb.get_tile_effect = MockLifxCommand(bulb)
bulb.set_tile_effect = MockLifxCommand(bulb)
return bulb
def _mocked_bulb_new_firmware() -> Light:
bulb = _mocked_bulb()
bulb.host_firmware_version = "3.90"

View File

@ -12,8 +12,11 @@ from homeassistant.components.lifx.const import ATTR_POWER
from homeassistant.components.lifx.light import ATTR_INFRARED, ATTR_ZONES
from homeassistant.components.lifx.manager import (
ATTR_DIRECTION,
ATTR_PALETTE,
ATTR_SPEED,
ATTR_THEME,
SERVICE_EFFECT_COLORLOOP,
SERVICE_EFFECT_MORPH,
SERVICE_EFFECT_MOVE,
)
from homeassistant.components.light import (
@ -55,6 +58,7 @@ from . import (
_mocked_bulb_new_firmware,
_mocked_clean_bulb,
_mocked_light_strip,
_mocked_tile,
_mocked_white_bulb,
_patch_config_flow_try_connect,
_patch_device,
@ -650,6 +654,146 @@ async def test_extended_multizone_messages(hass: HomeAssistant) -> None:
)
async def test_matrix_flame_morph_effects(hass: HomeAssistant) -> None:
"""Test the firmware flame and morph effects on a matrix device."""
config_entry = MockConfigEntry(
domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=SERIAL
)
config_entry.add_to_hass(hass)
bulb = _mocked_tile()
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"
await hass.services.async_call(
LIGHT_DOMAIN,
"turn_on",
{ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "effect_flame"},
blocking=True,
)
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": 3,
"speed": 3,
"palette": [],
}
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_MORPH,
{ATTR_ENTITY_ID: entity_id, ATTR_SPEED: 4, ATTR_THEME: "autumn"},
blocking=True,
)
bulb.power_level = 65535
bulb.effect = {
"effect": "MORPH",
"speed": 4.0,
"palette": [
(5643, 65535, 32768, 3500),
(15109, 65535, 32768, 3500),
(8920, 65535, 32768, 3500),
(10558, 65535, 32768, 3500),
],
}
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=30))
await hass.async_block_till_done()
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": 2,
"speed": 4,
"palette": [
(5643, 65535, 32768, 3500),
(15109, 65535, 32768, 3500),
(8920, 65535, 32768, 3500),
(10558, 65535, 32768, 3500),
],
}
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_MORPH,
{
ATTR_ENTITY_ID: entity_id,
ATTR_SPEED: 6,
ATTR_PALETTE: [
(0, 100, 255, 3500),
(60, 100, 255, 3500),
(120, 100, 255, 3500),
(180, 100, 255, 3500),
(240, 100, 255, 3500),
(300, 100, 255, 3500),
],
},
blocking=True,
)
bulb.power_level = 65535
bulb.effect = {
"effect": "MORPH",
"speed": 6,
"palette": [
(0, 65535, 65535, 3500),
(10922, 65535, 65535, 3500),
(21845, 65535, 65535, 3500),
(32768, 65535, 65535, 3500),
(43690, 65535, 65535, 3500),
(54612, 65535, 65535, 3500),
],
}
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=30))
await hass.async_block_till_done()
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": 2,
"speed": 6,
"palette": [
(0, 65535, 65535, 3500),
(10922, 65535, 65535, 3500),
(21845, 65535, 65535, 3500),
(32768, 65535, 65535, 3500),
(43690, 65535, 65535, 3500),
(54613, 65535, 65535, 3500),
],
}
bulb.get_tile_effect.reset_mock()
bulb.set_tile_effect.reset_mock()
bulb.set_power.reset_mock()
async def test_lightstrip_move_effect(hass: HomeAssistant) -> None:
"""Test the firmware move effect on a light strip."""
config_entry = MockConfigEntry(
@ -697,7 +841,7 @@ async def test_lightstrip_move_effect(hass: HomeAssistant) -> None:
)
bulb.power_level = 65535
bulb.effect = {"name": "effect_move", "enable": 1}
bulb.effect = {"name": "MOVE", "speed": 4.5, "direction": "Left"}
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=30))
await hass.async_block_till_done()