Add themes for LIFX multi-zone devices via a new select entity (#80067)

This commit is contained in:
Avi Miller 2022-10-24 03:28:17 +11:00 committed by GitHub
parent 873ccc4493
commit 2966f9ed8e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 162 additions and 9 deletions

View File

@ -36,6 +36,8 @@ ATTR_POWER = "power"
ATTR_REMAINING = "remaining"
ATTR_ZONES = "zones"
ATTR_THEME = "theme"
HEV_CYCLE_STATE = "hev_cycle_state"
INFRARED_BRIGHTNESS = "infrared_brightness"
INFRARED_BRIGHTNESS_VALUES_MAP = {

View File

@ -14,6 +14,7 @@ from aiolifx.aiolifx import (
TileEffectType,
)
from aiolifx.connection import LIFXConnection
from aiolifx_themes.themes import ThemeLibrary, ThemePainter
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant, callback
@ -69,6 +70,7 @@ class LIFXUpdateCoordinator(DataUpdateCoordinator):
self.lock = asyncio.Lock()
self.active_effect = FirmwareEffect.OFF
update_interval = timedelta(seconds=10)
self.last_used_theme: str = ""
super().__init__(
hass,
@ -286,8 +288,9 @@ class LIFXUpdateCoordinator(DataUpdateCoordinator):
async def async_set_multizone_effect(
self,
effect: str,
speed: float = 3,
speed: float = 3.0,
direction: str = "RIGHT",
theme_name: str | None = None,
power_on: bool = True,
) -> None:
"""Control the firmware-based Move effect on a multizone device."""
@ -295,6 +298,12 @@ class LIFXUpdateCoordinator(DataUpdateCoordinator):
if power_on and self.device.power_level == 0:
await self.async_set_power(True, 0)
if theme_name is not None:
theme = ThemeLibrary().get_theme(theme_name)
await ThemePainter(self.hass.loop).paint(
theme, [self.device], round(speed)
)
await async_execute_lifx(
partial(
self.device.set_multizone_effect,
@ -345,3 +354,9 @@ class LIFXUpdateCoordinator(DataUpdateCoordinator):
"""Set infrared brightness."""
infrared_brightness = infrared_brightness_option_to_value(option)
await async_execute_lifx(partial(self.device.set_infrared, infrared_brightness))
async def async_apply_theme(self, theme_name: str) -> None:
"""Apply the selected theme to the device."""
self.last_used_theme = theme_name
theme = ThemeLibrary().get_theme(theme_name)
await ThemePainter(self.hass.loop).paint(theme, [self.device])

View File

@ -29,7 +29,7 @@ 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 DATA_LIFX_MANAGER, DOMAIN
from .const import ATTR_THEME, DATA_LIFX_MANAGER, DOMAIN
from .coordinator import LIFXUpdateCoordinator, Light
from .util import convert_8_to_16, find_hsbk
@ -51,7 +51,6 @@ ATTR_CHANGE = "change"
ATTR_DIRECTION = "direction"
ATTR_SPEED = "speed"
ATTR_PALETTE = "palette"
ATTR_THEME = "theme"
EFFECT_FLAME = "FLAME"
EFFECT_MORPH = "MORPH"
@ -177,6 +176,7 @@ 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),
ATTR_THEME: vol.Optional(vol.In(ThemeLibrary().themes)),
}
)
@ -324,6 +324,7 @@ class LIFXManager:
direction=kwargs.get(
ATTR_DIRECTION, EFFECT_MOVE_DEFAULT_DIRECTION
),
theme_name=kwargs.get(ATTR_THEME, None),
power_on=kwargs.get(ATTR_POWER_ON, False),
)
for coordinator in coordinators

View File

@ -1,17 +1,26 @@
"""Select sensor entities for LIFX integration."""
from __future__ import annotations
from aiolifx_themes.themes import ThemeLibrary
from homeassistant.components.select import SelectEntity, SelectEntityDescription
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity import EntityCategory
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN, INFRARED_BRIGHTNESS, INFRARED_BRIGHTNESS_VALUES_MAP
from .const import (
ATTR_THEME,
DOMAIN,
INFRARED_BRIGHTNESS,
INFRARED_BRIGHTNESS_VALUES_MAP,
)
from .coordinator import LIFXUpdateCoordinator
from .entity import LIFXEntity
from .util import lifx_features
THEME_NAMES = [theme_name.lower() for theme_name in ThemeLibrary().themes]
INFRARED_BRIGHTNESS_ENTITY = SelectEntityDescription(
key=INFRARED_BRIGHTNESS,
name="Infrared brightness",
@ -19,6 +28,13 @@ INFRARED_BRIGHTNESS_ENTITY = SelectEntityDescription(
options=list(INFRARED_BRIGHTNESS_VALUES_MAP.values()),
)
THEME_ENTITY = SelectEntityDescription(
key=ATTR_THEME,
name="Theme",
entity_category=EntityCategory.CONFIG,
options=THEME_NAMES,
)
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
@ -30,11 +46,16 @@ async def async_setup_entry(
async_add_entities(
[
LIFXInfraredBrightnessSelectEntity(
coordinator, description=INFRARED_BRIGHTNESS_ENTITY
coordinator=coordinator, description=INFRARED_BRIGHTNESS_ENTITY
)
]
)
if lifx_features(coordinator.device)["multizone"] is True:
async_add_entities(
[LIFXThemeSelectEntity(coordinator=coordinator, description=THEME_ENTITY)]
)
class LIFXInfraredBrightnessSelectEntity(LIFXEntity, SelectEntity):
"""LIFX Nightvision infrared brightness configuration entity."""
@ -65,3 +86,36 @@ class LIFXInfraredBrightnessSelectEntity(LIFXEntity, SelectEntity):
async def async_select_option(self, option: str) -> None:
"""Update the infrared brightness value."""
await self.coordinator.async_set_infrared_brightness(option)
class LIFXThemeSelectEntity(LIFXEntity, SelectEntity):
"""Theme entity for LIFX multizone devices."""
_attr_has_entity_name = True
_attr_should_poll = False
def __init__(
self, coordinator: LIFXUpdateCoordinator, description: SelectEntityDescription
) -> None:
"""Initialise the theme selection entity."""
super().__init__(coordinator)
self.entity_description = description
self._attr_name = description.name
self._attr_unique_id = f"{coordinator.serial_number}_{description.key}"
self._attr_current_option = None
@callback
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
self._async_update_attrs()
super()._handle_coordinator_update()
@callback
def _async_update_attrs(self) -> None:
"""Update attrs from coordinator data."""
self._attr_current_option = self.coordinator.last_used_theme
async def async_select_option(self, option: str) -> None:
"""Paint the selected theme onto the device."""
await self.coordinator.async_apply_theme(option.lower())

View File

@ -183,6 +183,7 @@ effect_move:
name: Speed
description: How long in seconds for the effect to move across the length of the light.
default: 3.0
example: 3.0
selector:
number:
min: 0.1
@ -193,12 +194,46 @@ effect_move:
name: Direction
description: Direction the effect will move across the device.
default: right
example: right
selector:
select:
mode: dropdown
options:
- right
- left
theme:
name: Theme
description: (Optional) set one of the predefined themes onto the device before starting the effect.
example: exciting
default: exciting
selector:
select:
mode: dropdown
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.
@ -271,7 +306,7 @@ effect_morph:
- "halloween"
- "hanukkah"
- "holly"
- "independence day"
- "independence_day"
- "intense"
- "mellow"
- "peaceful"

View File

@ -24,7 +24,7 @@ from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import device_registry as dr
import homeassistant.util.color as color_util
from .const import _LOGGER, DOMAIN, INFRARED_BRIGHTNESS_VALUES_MAP, OVERALL_TIMEOUT
from .const import DOMAIN, INFRARED_BRIGHTNESS_VALUES_MAP, OVERALL_TIMEOUT
FIX_MAC_FW = AwesomeVersion("3.70")
@ -154,7 +154,7 @@ async def async_execute_lifx(method: Callable) -> Message:
# us by async_timeout when we hit the OVERALL_TIMEOUT
future.set_result(message)
_LOGGER.debug("Sending LIFX command: %s", method)
# _LOGGER.debug("Sending LIFX command: %s", method)
method(callb=_callback)
result = None

View File

@ -801,6 +801,7 @@ async def test_lightstrip_move_effect(hass: HomeAssistant) -> None:
)
config_entry.add_to_hass(hass)
bulb = _mocked_light_strip()
bulb.product = 38
bulb.power_level = 0
bulb.color = [65535, 65535, 65535, 65535]
with _patch_discovery(device=bulb), _patch_config_flow_try_connect(
@ -828,6 +829,7 @@ async def test_lightstrip_move_effect(hass: HomeAssistant) -> None:
"speed": 3.0,
"direction": 0,
}
bulb.get_multizone_effect.reset_mock()
bulb.set_multizone_effect.reset_mock()
bulb.set_power.reset_mock()
@ -836,7 +838,12 @@ async def test_lightstrip_move_effect(hass: HomeAssistant) -> None:
await hass.services.async_call(
DOMAIN,
SERVICE_EFFECT_MOVE,
{ATTR_ENTITY_ID: entity_id, ATTR_SPEED: 4.5, ATTR_DIRECTION: "left"},
{
ATTR_ENTITY_ID: entity_id,
ATTR_SPEED: 4.5,
ATTR_DIRECTION: "left",
ATTR_THEME: "sports",
},
blocking=True,
)
@ -849,6 +856,7 @@ async def test_lightstrip_move_effect(hass: HomeAssistant) -> None:
assert state.state == STATE_ON
assert len(bulb.set_power.calls) == 1
assert len(bulb.set_extended_color_zones.calls) == 1
assert len(bulb.set_multizone_effect.calls) == 1
call_dict = bulb.set_multizone_effect.calls[0][1]
call_dict.pop("callb")

View File

@ -17,6 +17,7 @@ from . import (
SERIAL,
MockLifxCommand,
_mocked_infrared_bulb,
_mocked_light_strip,
_patch_config_flow_try_connect,
_patch_device,
_patch_discovery,
@ -25,6 +26,43 @@ from . import (
from tests.common import MockConfigEntry, async_fire_time_changed
async def test_theme_select(hass: HomeAssistant) -> None:
"""Test selecting a theme."""
config_entry = MockConfigEntry(
domain=DOMAIN,
title=DEFAULT_ENTRY_TITLE,
data={CONF_HOST: IP_ADDRESS},
unique_id=MAC_ADDRESS,
)
config_entry.add_to_hass(hass)
bulb = _mocked_light_strip()
bulb.product = 38
bulb.power_level = 0
bulb.color = [0, 0, 65535, 3500]
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 = "select.my_bulb_theme"
entity_registry = er.async_get(hass)
entity = entity_registry.async_get(entity_id)
assert entity
assert not entity.disabled
await hass.services.async_call(
SELECT_DOMAIN,
"select_option",
{ATTR_ENTITY_ID: entity_id, "option": "intense"},
blocking=True,
)
assert len(bulb.set_extended_color_zones.calls) == 1
bulb.set_extended_color_zones.reset_mock()
async def test_infrared_brightness(hass: HomeAssistant) -> None:
"""Test getting and setting infrared brightness."""