mirror of
https://github.com/home-assistant/core.git
synced 2025-07-27 15:17:35 +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_REMAINING = "remaining"
|
||||||
ATTR_ZONES = "zones"
|
ATTR_ZONES = "zones"
|
||||||
|
|
||||||
|
ATTR_THEME = "theme"
|
||||||
|
|
||||||
HEV_CYCLE_STATE = "hev_cycle_state"
|
HEV_CYCLE_STATE = "hev_cycle_state"
|
||||||
INFRARED_BRIGHTNESS = "infrared_brightness"
|
INFRARED_BRIGHTNESS = "infrared_brightness"
|
||||||
INFRARED_BRIGHTNESS_VALUES_MAP = {
|
INFRARED_BRIGHTNESS_VALUES_MAP = {
|
||||||
|
@ -14,6 +14,7 @@ from aiolifx.aiolifx import (
|
|||||||
TileEffectType,
|
TileEffectType,
|
||||||
)
|
)
|
||||||
from aiolifx.connection import LIFXConnection
|
from aiolifx.connection import LIFXConnection
|
||||||
|
from aiolifx_themes.themes import ThemeLibrary, ThemePainter
|
||||||
|
|
||||||
from homeassistant.const import Platform
|
from homeassistant.const import Platform
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
@ -69,6 +70,7 @@ class LIFXUpdateCoordinator(DataUpdateCoordinator):
|
|||||||
self.lock = asyncio.Lock()
|
self.lock = asyncio.Lock()
|
||||||
self.active_effect = FirmwareEffect.OFF
|
self.active_effect = FirmwareEffect.OFF
|
||||||
update_interval = timedelta(seconds=10)
|
update_interval = timedelta(seconds=10)
|
||||||
|
self.last_used_theme: str = ""
|
||||||
|
|
||||||
super().__init__(
|
super().__init__(
|
||||||
hass,
|
hass,
|
||||||
@ -286,8 +288,9 @@ class LIFXUpdateCoordinator(DataUpdateCoordinator):
|
|||||||
async def async_set_multizone_effect(
|
async def async_set_multizone_effect(
|
||||||
self,
|
self,
|
||||||
effect: str,
|
effect: str,
|
||||||
speed: float = 3,
|
speed: float = 3.0,
|
||||||
direction: str = "RIGHT",
|
direction: str = "RIGHT",
|
||||||
|
theme_name: str | None = None,
|
||||||
power_on: bool = True,
|
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."""
|
||||||
@ -295,6 +298,12 @@ class LIFXUpdateCoordinator(DataUpdateCoordinator):
|
|||||||
if power_on and self.device.power_level == 0:
|
if power_on and self.device.power_level == 0:
|
||||||
await self.async_set_power(True, 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(
|
await async_execute_lifx(
|
||||||
partial(
|
partial(
|
||||||
self.device.set_multizone_effect,
|
self.device.set_multizone_effect,
|
||||||
@ -345,3 +354,9 @@ class LIFXUpdateCoordinator(DataUpdateCoordinator):
|
|||||||
"""Set infrared brightness."""
|
"""Set infrared brightness."""
|
||||||
infrared_brightness = infrared_brightness_option_to_value(option)
|
infrared_brightness = infrared_brightness_option_to_value(option)
|
||||||
await async_execute_lifx(partial(self.device.set_infrared, infrared_brightness))
|
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
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.helpers.service import async_extract_referenced_entity_ids
|
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 .coordinator import LIFXUpdateCoordinator, Light
|
||||||
from .util import convert_8_to_16, find_hsbk
|
from .util import convert_8_to_16, find_hsbk
|
||||||
|
|
||||||
@ -51,7 +51,6 @@ ATTR_CHANGE = "change"
|
|||||||
ATTR_DIRECTION = "direction"
|
ATTR_DIRECTION = "direction"
|
||||||
ATTR_SPEED = "speed"
|
ATTR_SPEED = "speed"
|
||||||
ATTR_PALETTE = "palette"
|
ATTR_PALETTE = "palette"
|
||||||
ATTR_THEME = "theme"
|
|
||||||
|
|
||||||
EFFECT_FLAME = "FLAME"
|
EFFECT_FLAME = "FLAME"
|
||||||
EFFECT_MORPH = "MORPH"
|
EFFECT_MORPH = "MORPH"
|
||||||
@ -177,6 +176,7 @@ LIFX_EFFECT_MOVE_SCHEMA = cv.make_entity_service_schema(
|
|||||||
**LIFX_EFFECT_SCHEMA,
|
**LIFX_EFFECT_SCHEMA,
|
||||||
ATTR_SPEED: vol.All(vol.Coerce(float), vol.Clamp(min=0.1, max=60)),
|
ATTR_SPEED: vol.All(vol.Coerce(float), vol.Clamp(min=0.1, max=60)),
|
||||||
ATTR_DIRECTION: vol.In(EFFECT_MOVE_DIRECTIONS),
|
ATTR_DIRECTION: vol.In(EFFECT_MOVE_DIRECTIONS),
|
||||||
|
ATTR_THEME: vol.Optional(vol.In(ThemeLibrary().themes)),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -324,6 +324,7 @@ class LIFXManager:
|
|||||||
direction=kwargs.get(
|
direction=kwargs.get(
|
||||||
ATTR_DIRECTION, EFFECT_MOVE_DEFAULT_DIRECTION
|
ATTR_DIRECTION, EFFECT_MOVE_DEFAULT_DIRECTION
|
||||||
),
|
),
|
||||||
|
theme_name=kwargs.get(ATTR_THEME, None),
|
||||||
power_on=kwargs.get(ATTR_POWER_ON, False),
|
power_on=kwargs.get(ATTR_POWER_ON, False),
|
||||||
)
|
)
|
||||||
for coordinator in coordinators
|
for coordinator in coordinators
|
||||||
|
@ -1,17 +1,26 @@
|
|||||||
"""Select sensor entities for LIFX integration."""
|
"""Select sensor entities for LIFX integration."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from aiolifx_themes.themes import ThemeLibrary
|
||||||
|
|
||||||
from homeassistant.components.select import SelectEntity, SelectEntityDescription
|
from homeassistant.components.select import SelectEntity, SelectEntityDescription
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.helpers.entity import EntityCategory
|
from homeassistant.helpers.entity import EntityCategory
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
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 .coordinator import LIFXUpdateCoordinator
|
||||||
from .entity import LIFXEntity
|
from .entity import LIFXEntity
|
||||||
from .util import lifx_features
|
from .util import lifx_features
|
||||||
|
|
||||||
|
THEME_NAMES = [theme_name.lower() for theme_name in ThemeLibrary().themes]
|
||||||
|
|
||||||
INFRARED_BRIGHTNESS_ENTITY = SelectEntityDescription(
|
INFRARED_BRIGHTNESS_ENTITY = SelectEntityDescription(
|
||||||
key=INFRARED_BRIGHTNESS,
|
key=INFRARED_BRIGHTNESS,
|
||||||
name="Infrared brightness",
|
name="Infrared brightness",
|
||||||
@ -19,6 +28,13 @@ INFRARED_BRIGHTNESS_ENTITY = SelectEntityDescription(
|
|||||||
options=list(INFRARED_BRIGHTNESS_VALUES_MAP.values()),
|
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(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||||
@ -30,11 +46,16 @@ async def async_setup_entry(
|
|||||||
async_add_entities(
|
async_add_entities(
|
||||||
[
|
[
|
||||||
LIFXInfraredBrightnessSelectEntity(
|
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):
|
class LIFXInfraredBrightnessSelectEntity(LIFXEntity, SelectEntity):
|
||||||
"""LIFX Nightvision infrared brightness configuration entity."""
|
"""LIFX Nightvision infrared brightness configuration entity."""
|
||||||
@ -65,3 +86,36 @@ class LIFXInfraredBrightnessSelectEntity(LIFXEntity, SelectEntity):
|
|||||||
async def async_select_option(self, option: str) -> None:
|
async def async_select_option(self, option: str) -> None:
|
||||||
"""Update the infrared brightness value."""
|
"""Update the infrared brightness value."""
|
||||||
await self.coordinator.async_set_infrared_brightness(option)
|
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
|
name: Speed
|
||||||
description: How long in seconds for the effect to move across the length of the light.
|
description: How long in seconds for the effect to move across the length of the light.
|
||||||
default: 3.0
|
default: 3.0
|
||||||
|
example: 3.0
|
||||||
selector:
|
selector:
|
||||||
number:
|
number:
|
||||||
min: 0.1
|
min: 0.1
|
||||||
@ -193,12 +194,46 @@ effect_move:
|
|||||||
name: Direction
|
name: Direction
|
||||||
description: Direction the effect will move across the device.
|
description: Direction the effect will move across the device.
|
||||||
default: right
|
default: right
|
||||||
|
example: right
|
||||||
selector:
|
selector:
|
||||||
select:
|
select:
|
||||||
mode: dropdown
|
mode: dropdown
|
||||||
options:
|
options:
|
||||||
- right
|
- right
|
||||||
- left
|
- 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:
|
power_on:
|
||||||
name: Power on
|
name: Power on
|
||||||
description: Powered off lights will be turned on before starting the effect.
|
description: Powered off lights will be turned on before starting the effect.
|
||||||
@ -271,7 +306,7 @@ effect_morph:
|
|||||||
- "halloween"
|
- "halloween"
|
||||||
- "hanukkah"
|
- "hanukkah"
|
||||||
- "holly"
|
- "holly"
|
||||||
- "independence day"
|
- "independence_day"
|
||||||
- "intense"
|
- "intense"
|
||||||
- "mellow"
|
- "mellow"
|
||||||
- "peaceful"
|
- "peaceful"
|
||||||
|
@ -24,7 +24,7 @@ from homeassistant.core import HomeAssistant, callback
|
|||||||
from homeassistant.helpers import device_registry as dr
|
from homeassistant.helpers import device_registry as dr
|
||||||
import homeassistant.util.color as color_util
|
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")
|
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
|
# us by async_timeout when we hit the OVERALL_TIMEOUT
|
||||||
future.set_result(message)
|
future.set_result(message)
|
||||||
|
|
||||||
_LOGGER.debug("Sending LIFX command: %s", method)
|
# _LOGGER.debug("Sending LIFX command: %s", method)
|
||||||
|
|
||||||
method(callb=_callback)
|
method(callb=_callback)
|
||||||
result = None
|
result = None
|
||||||
|
@ -801,6 +801,7 @@ async def test_lightstrip_move_effect(hass: HomeAssistant) -> None:
|
|||||||
)
|
)
|
||||||
config_entry.add_to_hass(hass)
|
config_entry.add_to_hass(hass)
|
||||||
bulb = _mocked_light_strip()
|
bulb = _mocked_light_strip()
|
||||||
|
bulb.product = 38
|
||||||
bulb.power_level = 0
|
bulb.power_level = 0
|
||||||
bulb.color = [65535, 65535, 65535, 65535]
|
bulb.color = [65535, 65535, 65535, 65535]
|
||||||
with _patch_discovery(device=bulb), _patch_config_flow_try_connect(
|
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,
|
"speed": 3.0,
|
||||||
"direction": 0,
|
"direction": 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
bulb.get_multizone_effect.reset_mock()
|
bulb.get_multizone_effect.reset_mock()
|
||||||
bulb.set_multizone_effect.reset_mock()
|
bulb.set_multizone_effect.reset_mock()
|
||||||
bulb.set_power.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(
|
await hass.services.async_call(
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
SERVICE_EFFECT_MOVE,
|
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,
|
blocking=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -849,6 +856,7 @@ async def test_lightstrip_move_effect(hass: HomeAssistant) -> None:
|
|||||||
assert state.state == STATE_ON
|
assert state.state == STATE_ON
|
||||||
|
|
||||||
assert len(bulb.set_power.calls) == 1
|
assert len(bulb.set_power.calls) == 1
|
||||||
|
assert len(bulb.set_extended_color_zones.calls) == 1
|
||||||
assert len(bulb.set_multizone_effect.calls) == 1
|
assert len(bulb.set_multizone_effect.calls) == 1
|
||||||
call_dict = bulb.set_multizone_effect.calls[0][1]
|
call_dict = bulb.set_multizone_effect.calls[0][1]
|
||||||
call_dict.pop("callb")
|
call_dict.pop("callb")
|
||||||
|
@ -17,6 +17,7 @@ from . import (
|
|||||||
SERIAL,
|
SERIAL,
|
||||||
MockLifxCommand,
|
MockLifxCommand,
|
||||||
_mocked_infrared_bulb,
|
_mocked_infrared_bulb,
|
||||||
|
_mocked_light_strip,
|
||||||
_patch_config_flow_try_connect,
|
_patch_config_flow_try_connect,
|
||||||
_patch_device,
|
_patch_device,
|
||||||
_patch_discovery,
|
_patch_discovery,
|
||||||
@ -25,6 +26,43 @@ from . import (
|
|||||||
from tests.common import MockConfigEntry, async_fire_time_changed
|
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:
|
async def test_infrared_brightness(hass: HomeAssistant) -> None:
|
||||||
"""Test getting and setting infrared brightness."""
|
"""Test getting and setting infrared brightness."""
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user