mirror of
https://github.com/home-assistant/core.git
synced 2025-04-23 08:47:57 +00:00
Add themes for LIFX multi-zone devices via a new select entity (#80067)
This commit is contained in:
parent
873ccc4493
commit
2966f9ed8e
@ -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 = {
|
||||
|
@ -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])
|
||||
|
@ -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
|
||||
|
@ -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())
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
@ -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")
|
||||
|
@ -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."""
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user