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 functools import partial
from typing import Any, cast 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 aiolifx.connection import LIFXConnection
from homeassistant.const import Platform from homeassistant.const import Platform
@ -279,7 +284,11 @@ class LIFXUpdateCoordinator(DataUpdateCoordinator):
self.active_effect = FirmwareEffect[self.device.effect.get("effect", "OFF")] self.active_effect = FirmwareEffect[self.device.effect.get("effect", "OFF")]
async def async_set_multizone_effect( 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: ) -> 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 lifx_features(self.device)["multizone"] is True:
@ -296,6 +305,31 @@ class LIFXUpdateCoordinator(DataUpdateCoordinator):
) )
self.active_effect = FirmwareEffect[effect.upper()] 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: def async_get_active_effect(self) -> int:
"""Return the enum value of the currently active firmware effect.""" """Return the enum value of the currently active firmware effect."""
return self.active_effect.value return self.active_effect.value

View File

@ -40,6 +40,8 @@ from .coordinator import FirmwareEffect, LIFXUpdateCoordinator
from .entity import LIFXEntity from .entity import LIFXEntity
from .manager import ( from .manager import (
SERVICE_EFFECT_COLORLOOP, SERVICE_EFFECT_COLORLOOP,
SERVICE_EFFECT_FLAME,
SERVICE_EFFECT_MORPH,
SERVICE_EFFECT_MOVE, SERVICE_EFFECT_MOVE,
SERVICE_EFFECT_PULSE, SERVICE_EFFECT_PULSE,
SERVICE_EFFECT_STOP, SERVICE_EFFECT_STOP,
@ -93,8 +95,10 @@ async def async_setup_entry(
LIFX_SET_HEV_CYCLE_STATE_SCHEMA, LIFX_SET_HEV_CYCLE_STATE_SCHEMA,
"set_hev_cycle_state", "set_hev_cycle_state",
) )
if lifx_features(device)["extended_multizone"]: if lifx_features(device)["matrix"]:
entity: LIFXLight = LIFXExtendedMultiZone(coordinator, manager, entry) entity: LIFXLight = LIFXMatrix(coordinator, manager, entry)
elif lifx_features(device)["extended_multizone"]:
entity = LIFXExtendedMultiZone(coordinator, manager, entry)
elif lifx_features(device)["multizone"]: elif lifx_features(device)["multizone"]:
entity = LIFXMultiZone(coordinator, manager, entry) entity = LIFXMultiZone(coordinator, manager, entry)
elif lifx_features(device)["color"]: elif lifx_features(device)["color"]:
@ -471,3 +475,15 @@ class LIFXExtendedMultiZone(LIFXMultiZone):
# set_extended_color_zones does not update the # set_extended_color_zones does not update the
# state of the device, so we need to do that # state of the device, so we need to do that
await self.get_color() 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 from typing import Any
import aiolifx_effects import aiolifx_effects
from aiolifx_themes.themes import Theme, ThemeLibrary
import voluptuous as vol import voluptuous as vol
from homeassistant.components.light import ( from homeassistant.components.light import (
@ -34,9 +35,11 @@ from .util import convert_8_to_16, find_hsbk
SCAN_INTERVAL = timedelta(seconds=10) SCAN_INTERVAL = timedelta(seconds=10)
SERVICE_EFFECT_PULSE = "effect_pulse"
SERVICE_EFFECT_COLORLOOP = "effect_colorloop" SERVICE_EFFECT_COLORLOOP = "effect_colorloop"
SERVICE_EFFECT_FLAME = "effect_flame"
SERVICE_EFFECT_MORPH = "effect_morph"
SERVICE_EFFECT_MOVE = "effect_move" SERVICE_EFFECT_MOVE = "effect_move"
SERVICE_EFFECT_PULSE = "effect_pulse"
SERVICE_EFFECT_STOP = "effect_stop" SERVICE_EFFECT_STOP = "effect_stop"
ATTR_POWER_OFF = "power_off" ATTR_POWER_OFF = "power_off"
@ -47,11 +50,20 @@ ATTR_SPREAD = "spread"
ATTR_CHANGE = "change" ATTR_CHANGE = "change"
ATTR_DIRECTION = "direction" ATTR_DIRECTION = "direction"
ATTR_SPEED = "speed" ATTR_SPEED = "speed"
ATTR_PALETTE = "palette"
ATTR_THEME = "theme"
EFFECT_FLAME = "FLAME"
EFFECT_MORPH = "MORPH"
EFFECT_MOVE = "MOVE" EFFECT_MOVE = "MOVE"
EFFECT_OFF = "OFF" 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_DEFAULT_DIRECTION = "right"
EFFECT_MOVE_DIRECTION_RIGHT = "right" EFFECT_MOVE_DIRECTION_RIGHT = "right"
EFFECT_MOVE_DIRECTION_LEFT = "left" EFFECT_MOVE_DIRECTION_LEFT = "left"
@ -128,6 +140,37 @@ SERVICES = (
SERVICE_EFFECT_COLORLOOP, 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( LIFX_EFFECT_MOVE_SCHEMA = cv.make_entity_service_schema(
{ {
@ -192,6 +235,20 @@ class LIFXManager:
schema=LIFX_EFFECT_COLORLOOP_SCHEMA, 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( self.hass.services.async_register(
DOMAIN, DOMAIN,
SERVICE_EFFECT_MOVE, SERVICE_EFFECT_MOVE,
@ -222,7 +279,43 @@ class LIFXManager:
coordinators.append(coordinator) coordinators.append(coordinator)
bulbs.append(coordinator.device) 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( await asyncio.gather(
*( *(
coordinator.async_set_multizone_effect( coordinator.async_set_multizone_effect(
@ -269,9 +362,9 @@ class LIFXManager:
await self.effects_conductor.stop(bulbs) await self.effects_conductor.stop(bulbs)
for coordinator in coordinators: for coordinator in coordinators:
await coordinator.async_set_multizone_effect( await coordinator.async_set_matrix_effect(
effect=EFFECT_OFF, effect=EFFECT_OFF, power_on=False
speed=EFFECT_MOVE_DEFAULT_SPEED, )
direction=EFFECT_MOVE_DEFAULT_DIRECTION, await coordinator.async_set_multizone_effect(
power_on=False, effect=EFFECT_OFF, power_on=False
) )

View File

@ -3,7 +3,11 @@
"name": "LIFX", "name": "LIFX",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/lifx", "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", "quality_scale": "platinum",
"dependencies": ["network"], "dependencies": ["network"],
"homekit": { "homekit": {

View File

@ -205,7 +205,91 @@ effect_move:
default: true default: true
selector: selector:
boolean: 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: effect_stop:
name: Stop effect name: Stop effect
description: Stop a running effect. description: Stop a running effect.

View File

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

View File

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

View File

@ -156,6 +156,15 @@ def _mocked_light_strip() -> Light:
return bulb 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: def _mocked_bulb_new_firmware() -> Light:
bulb = _mocked_bulb() bulb = _mocked_bulb()
bulb.host_firmware_version = "3.90" 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.light import ATTR_INFRARED, ATTR_ZONES
from homeassistant.components.lifx.manager import ( from homeassistant.components.lifx.manager import (
ATTR_DIRECTION, ATTR_DIRECTION,
ATTR_PALETTE,
ATTR_SPEED, ATTR_SPEED,
ATTR_THEME,
SERVICE_EFFECT_COLORLOOP, SERVICE_EFFECT_COLORLOOP,
SERVICE_EFFECT_MORPH,
SERVICE_EFFECT_MOVE, SERVICE_EFFECT_MOVE,
) )
from homeassistant.components.light import ( from homeassistant.components.light import (
@ -55,6 +58,7 @@ from . import (
_mocked_bulb_new_firmware, _mocked_bulb_new_firmware,
_mocked_clean_bulb, _mocked_clean_bulb,
_mocked_light_strip, _mocked_light_strip,
_mocked_tile,
_mocked_white_bulb, _mocked_white_bulb,
_patch_config_flow_try_connect, _patch_config_flow_try_connect,
_patch_device, _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: async def test_lightstrip_move_effect(hass: HomeAssistant) -> None:
"""Test the firmware move effect on a light strip.""" """Test the firmware move effect on a light strip."""
config_entry = MockConfigEntry( config_entry = MockConfigEntry(
@ -697,7 +841,7 @@ async def test_lightstrip_move_effect(hass: HomeAssistant) -> None:
) )
bulb.power_level = 65535 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)) async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=30))
await hass.async_block_till_done() await hass.async_block_till_done()