mirror of
https://github.com/home-assistant/core.git
synced 2025-04-24 09:17:53 +00:00
Enable the move firmware effect on multizone lights (#78918)
Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
parent
75104159c6
commit
691028dfb4
@ -3,10 +3,11 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
from enum import IntEnum
|
||||
from functools import partial
|
||||
from typing import Any, cast
|
||||
|
||||
from aiolifx.aiolifx import Light
|
||||
from aiolifx.aiolifx import Light, MultiZoneDirection, MultiZoneEffectType
|
||||
from aiolifx.connection import LIFXConnection
|
||||
|
||||
from homeassistant.const import Platform
|
||||
@ -37,6 +38,15 @@ REQUEST_REFRESH_DELAY = 0.35
|
||||
LIFX_IDENTIFY_DELAY = 3.0
|
||||
|
||||
|
||||
class FirmwareEffect(IntEnum):
|
||||
"""Enumeration of LIFX firmware effects."""
|
||||
|
||||
OFF = 0
|
||||
MOVE = 1
|
||||
MORPH = 2
|
||||
FLAME = 3
|
||||
|
||||
|
||||
class LIFXUpdateCoordinator(DataUpdateCoordinator):
|
||||
"""DataUpdateCoordinator to gather data for a specific lifx device."""
|
||||
|
||||
@ -51,7 +61,9 @@ class LIFXUpdateCoordinator(DataUpdateCoordinator):
|
||||
self.connection = connection
|
||||
self.device: Light = connection.device
|
||||
self.lock = asyncio.Lock()
|
||||
self.active_effect = FirmwareEffect.OFF
|
||||
update_interval = timedelta(seconds=10)
|
||||
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
@ -139,6 +151,7 @@ class LIFXUpdateCoordinator(DataUpdateCoordinator):
|
||||
# Update model-specific configuration
|
||||
if lifx_features(self.device)["multizone"]:
|
||||
await self.async_update_color_zones()
|
||||
await self.async_update_multizone_effect()
|
||||
|
||||
if lifx_features(self.device)["hev"]:
|
||||
await self.async_get_hev_cycle()
|
||||
@ -219,6 +232,33 @@ class LIFXUpdateCoordinator(DataUpdateCoordinator):
|
||||
)
|
||||
)
|
||||
|
||||
async def async_update_multizone_effect(self) -> None:
|
||||
"""Update the device firmware effect running state."""
|
||||
await async_execute_lifx(self.device.get_multizone_effect)
|
||||
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
|
||||
) -> None:
|
||||
"""Control the firmware-based Move effect on a multizone device."""
|
||||
if lifx_features(self.device)["multizone"] is True:
|
||||
if power_on and self.device.power_level == 0:
|
||||
await self.async_set_power(True, 0)
|
||||
|
||||
await async_execute_lifx(
|
||||
partial(
|
||||
self.device.set_multizone_effect,
|
||||
effect=MultiZoneEffectType[effect.upper()].value,
|
||||
speed=speed,
|
||||
direction=MultiZoneDirection[direction.upper()].value,
|
||||
)
|
||||
)
|
||||
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
|
||||
|
||||
async def async_set_hev_cycle_state(self, enable: bool, duration: int = 0) -> None:
|
||||
"""Start or stop an HEV cycle on a LIFX Clean bulb."""
|
||||
if lifx_features(self.device)["hev"]:
|
||||
|
@ -38,10 +38,11 @@ from .const import (
|
||||
DOMAIN,
|
||||
INFRARED_BRIGHTNESS,
|
||||
)
|
||||
from .coordinator import LIFXUpdateCoordinator
|
||||
from .coordinator import FirmwareEffect, LIFXUpdateCoordinator
|
||||
from .entity import LIFXEntity
|
||||
from .manager import (
|
||||
SERVICE_EFFECT_COLORLOOP,
|
||||
SERVICE_EFFECT_MOVE,
|
||||
SERVICE_EFFECT_PULSE,
|
||||
SERVICE_EFFECT_STOP,
|
||||
LIFXManager,
|
||||
@ -139,6 +140,7 @@ class LIFXLight(LIFXEntity, LightEntity):
|
||||
color_mode = ColorMode.BRIGHTNESS
|
||||
self._attr_color_mode = color_mode
|
||||
self._attr_supported_color_modes = {color_mode}
|
||||
self._attr_effect = None
|
||||
|
||||
@property
|
||||
def brightness(self) -> int:
|
||||
@ -163,6 +165,8 @@ class LIFXLight(LIFXEntity, LightEntity):
|
||||
"""Return the name of the currently running effect."""
|
||||
if effect := self.effects_conductor.effect(self.bulb):
|
||||
return f"effect_{effect.name}"
|
||||
if effect := self.coordinator.async_get_active_effect():
|
||||
return f"effect_{FirmwareEffect(effect).name.lower()}"
|
||||
return None
|
||||
|
||||
async def update_during_transition(self, when: int) -> None:
|
||||
@ -361,6 +365,13 @@ class LIFXColor(LIFXLight):
|
||||
class LIFXStrip(LIFXColor):
|
||||
"""Representation of a LIFX light strip with multiple zones."""
|
||||
|
||||
_attr_effect_list = [
|
||||
SERVICE_EFFECT_COLORLOOP,
|
||||
SERVICE_EFFECT_PULSE,
|
||||
SERVICE_EFFECT_MOVE,
|
||||
SERVICE_EFFECT_STOP,
|
||||
]
|
||||
|
||||
async def set_color(
|
||||
self,
|
||||
hsbk: list[float | int | None],
|
||||
|
@ -1,6 +1,7 @@
|
||||
"""Support for LIFX lights."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Callable
|
||||
from datetime import timedelta
|
||||
from typing import Any
|
||||
@ -28,21 +29,35 @@ from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.service import async_extract_referenced_entity_ids
|
||||
|
||||
from .const import _LOGGER, DATA_LIFX_MANAGER, DOMAIN
|
||||
from .const import DATA_LIFX_MANAGER, DOMAIN
|
||||
from .coordinator import LIFXUpdateCoordinator, Light
|
||||
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_MOVE = "effect_move"
|
||||
SERVICE_EFFECT_STOP = "effect_stop"
|
||||
|
||||
ATTR_POWER_OFF = "power_off"
|
||||
ATTR_POWER_ON = "power_on"
|
||||
ATTR_PERIOD = "period"
|
||||
ATTR_CYCLES = "cycles"
|
||||
ATTR_SPREAD = "spread"
|
||||
ATTR_CHANGE = "change"
|
||||
ATTR_DIRECTION = "direction"
|
||||
ATTR_SPEED = "speed"
|
||||
|
||||
EFFECT_MOVE = "MOVE"
|
||||
EFFECT_OFF = "OFF"
|
||||
|
||||
EFFECT_MOVE_DEFAULT_SPEED = 3.0
|
||||
EFFECT_MOVE_DEFAULT_DIRECTION = "right"
|
||||
EFFECT_MOVE_DIRECTION_RIGHT = "right"
|
||||
EFFECT_MOVE_DIRECTION_LEFT = "left"
|
||||
|
||||
EFFECT_MOVE_DIRECTIONS = [EFFECT_MOVE_DIRECTION_LEFT, EFFECT_MOVE_DIRECTION_RIGHT]
|
||||
|
||||
PULSE_MODE_BLINK = "blink"
|
||||
PULSE_MODE_BREATHE = "breathe"
|
||||
@ -110,10 +125,20 @@ LIFX_EFFECT_STOP_SCHEMA = cv.make_entity_service_schema({})
|
||||
SERVICES = (
|
||||
SERVICE_EFFECT_STOP,
|
||||
SERVICE_EFFECT_PULSE,
|
||||
SERVICE_EFFECT_MOVE,
|
||||
SERVICE_EFFECT_COLORLOOP,
|
||||
)
|
||||
|
||||
|
||||
LIFX_EFFECT_MOVE_SCHEMA = cv.make_entity_service_schema(
|
||||
{
|
||||
**LIFX_EFFECT_SCHEMA,
|
||||
ATTR_SPEED: vol.All(vol.Coerce(float), vol.Clamp(min=0.1, max=60)),
|
||||
ATTR_DIRECTION: vol.In(EFFECT_MOVE_DIRECTIONS),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class LIFXManager:
|
||||
"""Representation of all known LIFX entities."""
|
||||
|
||||
@ -168,6 +193,13 @@ class LIFXManager:
|
||||
schema=LIFX_EFFECT_COLORLOOP_SCHEMA,
|
||||
)
|
||||
|
||||
self.hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_EFFECT_MOVE,
|
||||
service_handler,
|
||||
schema=LIFX_EFFECT_MOVE_SCHEMA,
|
||||
)
|
||||
|
||||
self.hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_EFFECT_STOP,
|
||||
@ -179,15 +211,35 @@ class LIFXManager:
|
||||
self, entity_ids: set[str], service: str, **kwargs: Any
|
||||
) -> None:
|
||||
"""Start a light effect on entities."""
|
||||
bulbs = [
|
||||
coordinator.device
|
||||
for entry_id, coordinator in self.hass.data[DOMAIN].items()
|
||||
if entry_id != DATA_LIFX_MANAGER
|
||||
and self.entry_id_to_entity_id[entry_id] in entity_ids
|
||||
]
|
||||
_LOGGER.debug("Starting effect %s on %s", service, bulbs)
|
||||
|
||||
if service == SERVICE_EFFECT_PULSE:
|
||||
coordinators: list[LIFXUpdateCoordinator] = []
|
||||
bulbs: list[Light] = []
|
||||
|
||||
for entry_id, coordinator in self.hass.data[DOMAIN].items():
|
||||
if (
|
||||
entry_id != DATA_LIFX_MANAGER
|
||||
and self.entry_id_to_entity_id[entry_id] in entity_ids
|
||||
):
|
||||
coordinators.append(coordinator)
|
||||
bulbs.append(coordinator.device)
|
||||
|
||||
if service == SERVICE_EFFECT_MOVE:
|
||||
await asyncio.gather(
|
||||
*(
|
||||
coordinator.async_set_multizone_effect(
|
||||
effect=EFFECT_MOVE,
|
||||
speed=kwargs.get(ATTR_SPEED, EFFECT_MOVE_DEFAULT_SPEED),
|
||||
direction=kwargs.get(
|
||||
ATTR_DIRECTION, EFFECT_MOVE_DEFAULT_DIRECTION
|
||||
),
|
||||
power_on=kwargs.get(ATTR_POWER_ON, False),
|
||||
)
|
||||
for coordinator in coordinators
|
||||
)
|
||||
)
|
||||
|
||||
elif service == SERVICE_EFFECT_PULSE:
|
||||
|
||||
effect = aiolifx_effects.EffectPulse(
|
||||
power_on=kwargs.get(ATTR_POWER_ON),
|
||||
period=kwargs.get(ATTR_PERIOD),
|
||||
@ -196,6 +248,7 @@ class LIFXManager:
|
||||
hsbk=find_hsbk(self.hass, **kwargs),
|
||||
)
|
||||
await self.effects_conductor.start(effect, bulbs)
|
||||
|
||||
elif service == SERVICE_EFFECT_COLORLOOP:
|
||||
preprocess_turn_on_alternatives(self.hass, kwargs)
|
||||
|
||||
@ -212,5 +265,15 @@ class LIFXManager:
|
||||
brightness=brightness,
|
||||
)
|
||||
await self.effects_conductor.start(effect, bulbs)
|
||||
|
||||
elif service == SERVICE_EFFECT_STOP:
|
||||
|
||||
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,
|
||||
)
|
||||
|
@ -3,7 +3,7 @@
|
||||
"name": "LIFX",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/lifx",
|
||||
"requirements": ["aiolifx==0.8.4", "aiolifx_effects==0.2.2"],
|
||||
"requirements": ["aiolifx==0.8.5", "aiolifx_effects==0.2.2"],
|
||||
"quality_scale": "platinum",
|
||||
"dependencies": ["network"],
|
||||
"homekit": {
|
||||
|
@ -171,6 +171,40 @@ effect_colorloop:
|
||||
default: true
|
||||
selector:
|
||||
boolean:
|
||||
effect_move:
|
||||
name: Move effect
|
||||
description: Start the firmware-based Move effect on a LIFX Z, Lightstrip or Beam.
|
||||
target:
|
||||
entity:
|
||||
integration: lifx
|
||||
domain: light
|
||||
fields:
|
||||
speed:
|
||||
name: Speed
|
||||
description: How long in seconds for the effect to move across the length of the light.
|
||||
default: 3.0
|
||||
selector:
|
||||
number:
|
||||
min: 0.1
|
||||
max: 60
|
||||
step: 0.1
|
||||
unit_of_measurement: seconds
|
||||
direction:
|
||||
name: Direction
|
||||
description: Direction the effect will move across the device.
|
||||
default: right
|
||||
selector:
|
||||
select:
|
||||
mode: dropdown
|
||||
options:
|
||||
- right
|
||||
- left
|
||||
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
|
||||
|
@ -193,7 +193,7 @@ aiokafka==0.7.2
|
||||
aiokef==0.2.16
|
||||
|
||||
# homeassistant.components.lifx
|
||||
aiolifx==0.8.4
|
||||
aiolifx==0.8.5
|
||||
|
||||
# homeassistant.components.lifx
|
||||
aiolifx_effects==0.2.2
|
||||
|
@ -171,7 +171,7 @@ aiohue==4.5.0
|
||||
aiokafka==0.7.2
|
||||
|
||||
# homeassistant.components.lifx
|
||||
aiolifx==0.8.4
|
||||
aiolifx==0.8.5
|
||||
|
||||
# homeassistant.components.lifx
|
||||
aiolifx_effects==0.2.2
|
||||
|
@ -145,9 +145,13 @@ def _mocked_infrared_bulb() -> Light:
|
||||
def _mocked_light_strip() -> Light:
|
||||
bulb = _mocked_bulb()
|
||||
bulb.product = 31 # LIFX Z
|
||||
bulb.color_zones = [MagicMock(), MagicMock()]
|
||||
bulb.effect = {"effect": "MOVE", "speed": 3, "duration": 0, "direction": "RIGHT"}
|
||||
bulb.get_color_zones = MockLifxCommand(bulb)
|
||||
bulb.set_color_zones = MockLifxCommand(bulb)
|
||||
bulb.color_zones = [MagicMock(), MagicMock()]
|
||||
bulb.get_multizone_effect = MockLifxCommand(bulb)
|
||||
bulb.set_multizone_effect = MockLifxCommand(bulb)
|
||||
|
||||
return bulb
|
||||
|
||||
|
||||
|
@ -10,7 +10,12 @@ from homeassistant.components import lifx
|
||||
from homeassistant.components.lifx import DOMAIN
|
||||
from homeassistant.components.lifx.const import ATTR_POWER
|
||||
from homeassistant.components.lifx.light import ATTR_INFRARED, ATTR_ZONES
|
||||
from homeassistant.components.lifx.manager import SERVICE_EFFECT_COLORLOOP
|
||||
from homeassistant.components.lifx.manager import (
|
||||
ATTR_DIRECTION,
|
||||
ATTR_SPEED,
|
||||
SERVICE_EFFECT_COLORLOOP,
|
||||
SERVICE_EFFECT_MOVE,
|
||||
)
|
||||
from homeassistant.components.light import (
|
||||
ATTR_BRIGHTNESS,
|
||||
ATTR_COLOR_MODE,
|
||||
@ -24,7 +29,13 @@ from homeassistant.components.light import (
|
||||
DOMAIN as LIGHT_DOMAIN,
|
||||
ColorMode,
|
||||
)
|
||||
from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST, STATE_OFF, STATE_UNAVAILABLE
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
CONF_HOST,
|
||||
STATE_OFF,
|
||||
STATE_ON,
|
||||
STATE_UNAVAILABLE,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
@ -401,6 +412,93 @@ async def test_light_strip(hass: HomeAssistant) -> None:
|
||||
)
|
||||
|
||||
|
||||
async def test_lightstrip_move_effect(hass: HomeAssistant) -> None:
|
||||
"""Test the firmware move effect on a light strip."""
|
||||
config_entry = MockConfigEntry(
|
||||
domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=SERIAL
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
bulb = _mocked_light_strip()
|
||||
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_move"},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert len(bulb.set_power.calls) == 1
|
||||
assert len(bulb.set_multizone_effect.calls) == 1
|
||||
|
||||
call_dict = bulb.set_multizone_effect.calls[0][1]
|
||||
call_dict.pop("callb")
|
||||
assert call_dict == {
|
||||
"effect": 1,
|
||||
"speed": 3.0,
|
||||
"direction": 0,
|
||||
}
|
||||
bulb.get_multizone_effect.reset_mock()
|
||||
bulb.set_multizone_effect.reset_mock()
|
||||
bulb.set_power.reset_mock()
|
||||
|
||||
bulb.power_level = 0
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_EFFECT_MOVE,
|
||||
{ATTR_ENTITY_ID: entity_id, ATTR_SPEED: 4.5, ATTR_DIRECTION: "left"},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
bulb.power_level = 65535
|
||||
bulb.effect = {"name": "effect_move", "enable": 1}
|
||||
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_multizone_effect.calls) == 1
|
||||
call_dict = bulb.set_multizone_effect.calls[0][1]
|
||||
call_dict.pop("callb")
|
||||
assert call_dict == {
|
||||
"effect": 1,
|
||||
"speed": 4.5,
|
||||
"direction": 1,
|
||||
}
|
||||
bulb.get_multizone_effect.reset_mock()
|
||||
bulb.set_multizone_effect.reset_mock()
|
||||
bulb.set_power.reset_mock()
|
||||
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
"turn_on",
|
||||
{ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "effect_stop"},
|
||||
blocking=True,
|
||||
)
|
||||
assert len(bulb.set_power.calls) == 0
|
||||
assert len(bulb.set_multizone_effect.calls) == 1
|
||||
call_dict = bulb.set_multizone_effect.calls[0][1]
|
||||
call_dict.pop("callb")
|
||||
assert call_dict == {
|
||||
"effect": 0,
|
||||
"speed": 3.0,
|
||||
"direction": 0,
|
||||
}
|
||||
bulb.get_multizone_effect.reset_mock()
|
||||
bulb.set_multizone_effect.reset_mock()
|
||||
bulb.set_power.reset_mock()
|
||||
|
||||
|
||||
async def test_color_light_with_temp(
|
||||
hass: HomeAssistant, mock_effect_conductor
|
||||
) -> None:
|
||||
|
Loading…
x
Reference in New Issue
Block a user