Move service registration to async_setup in lifx (#181196)

This commit is contained in:
epenet
2026-09-14 11:36:02 +02:00
committed by GitHub
parent b94ec35d31
commit c8035dc829
8 changed files with 412 additions and 243 deletions
+5 -5
View File
@@ -28,6 +28,7 @@ from .coordinator import LIFXConfigEntry, LIFXUpdateCoordinator
from .discovery import async_discover_devices, async_trigger_discovery
from .manager import LIFXManager
from .migration import async_migrate_entities_devices, async_migrate_legacy_entries
from .services import async_setup_services
from .util import async_entry_is_legacy, async_get_legacy_entry, formatted_serial
CONF_SERVER = "server"
@@ -156,6 +157,8 @@ class LIFXDiscoveryManager:
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the LIFX component."""
async_setup_services(hass)
migrating = bool(async_get_legacy_entry(hass))
discovery_manager = LIFXDiscoveryManager(hass, migrating)
@@ -197,9 +200,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: LIFXConfigEntry) -> bool
assert entry.unique_id is not None
if DATA_LIFX_MANAGER not in hass.data:
manager = LIFXManager(hass)
hass.data[DATA_LIFX_MANAGER] = manager
manager.async_setup()
hass.data[DATA_LIFX_MANAGER] = LIFXManager(hass)
host = entry.data[CONF_HOST]
connection = LIFXConnection(host, TARGET_ANY)
@@ -240,6 +241,5 @@ async def async_unload_entry(hass: HomeAssistant, entry: LIFXConfigEntry) -> boo
entry.runtime_data.connection.async_stop()
# Only the DATA_LIFX_MANAGER left, remove it.
if len(hass.config_entries.async_loaded_entries(DOMAIN)) == 0:
manager = hass.data.pop(DATA_LIFX_MANAGER)
manager.async_unload()
hass.data.pop(DATA_LIFX_MANAGER)
return unload_ok
+23
View File
@@ -58,6 +58,29 @@ ATTR_ZONES = "zones"
ATTR_THEME = "theme"
ATTR_CHANGE = "change"
ATTR_CLOUD_SATURATION_MAX = "cloud_saturation_max"
ATTR_CLOUD_SATURATION_MIN = "cloud_saturation_min"
ATTR_CYCLES = "cycles"
ATTR_DIRECTION = "direction"
ATTR_PALETTE = "palette"
ATTR_PERIOD = "period"
ATTR_POWER_ON = "power_on"
ATTR_SATURATION_MAX = "saturation_max"
ATTR_SATURATION_MIN = "saturation_min"
ATTR_SKY_TYPE = "sky_type"
ATTR_SPEED = "speed"
ATTR_SPREAD = "spread"
SERVICE_EFFECT_COLORLOOP = "effect_colorloop"
SERVICE_EFFECT_FLAME = "effect_flame"
SERVICE_EFFECT_MORPH = "effect_morph"
SERVICE_EFFECT_MOVE = "effect_move"
SERVICE_EFFECT_PULSE = "effect_pulse"
SERVICE_EFFECT_SKY = "effect_sky"
SERVICE_EFFECT_STOP = "effect_stop"
SERVICE_PAINT_THEME = "paint_theme"
HEV_CYCLE_STATE = "hev_cycle_state"
INFRARED_BRIGHTNESS = "infrared_brightness"
INFRARED_BRIGHTNESS_VALUES_MAP = {
+3 -5
View File
@@ -36,10 +36,6 @@ from .const import (
INFRARED_BRIGHTNESS,
LIFX_CEILING_PRODUCT_IDS,
LOGGER,
)
from .coordinator import FirmwareEffect, LIFXConfigEntry, LIFXUpdateCoordinator
from .entity import LIFXEntity
from .manager import (
SERVICE_EFFECT_COLORLOOP,
SERVICE_EFFECT_FLAME,
SERVICE_EFFECT_MORPH,
@@ -47,8 +43,10 @@ from .manager import (
SERVICE_EFFECT_PULSE,
SERVICE_EFFECT_SKY,
SERVICE_EFFECT_STOP,
LIFXManager,
)
from .coordinator import FirmwareEffect, LIFXConfigEntry, LIFXUpdateCoordinator
from .entity import LIFXEntity
from .manager import LIFXManager
from .util import convert_8_to_16, convert_16_to_8, find_hsbk, lifx_features, merge_hsbk
LIFX_STATE_SETTLE_DELAY = 0.3
+25 -230
View File
@@ -8,30 +8,39 @@ from typing import TYPE_CHECKING, Any
import aiolifx_effects
from aiolifx_themes.painter import ThemePainter
from aiolifx_themes.themes import Theme, ThemeLibrary
import probatio
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
ATTR_BRIGHTNESS_PCT,
ATTR_COLOR_NAME,
ATTR_COLOR_TEMP_KELVIN,
ATTR_HS_COLOR,
ATTR_RGB_COLOR,
ATTR_TRANSITION,
ATTR_XY_COLOR,
COLOR_GROUP,
VALID_BRIGHTNESS,
VALID_BRIGHTNESS_PCT,
)
from homeassistant.const import ATTR_MODE
from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.target import (
TargetSelection,
async_extract_referenced_entity_ids,
)
from homeassistant.core import HomeAssistant, callback
from .const import ATTR_THEME, DOMAIN
from .const import (
ATTR_CHANGE,
ATTR_CLOUD_SATURATION_MAX,
ATTR_CLOUD_SATURATION_MIN,
ATTR_CYCLES,
ATTR_DIRECTION,
ATTR_PALETTE,
ATTR_PERIOD,
ATTR_POWER_ON,
ATTR_SATURATION_MAX,
ATTR_SATURATION_MIN,
ATTR_SKY_TYPE,
ATTR_SPEED,
ATTR_SPREAD,
ATTR_THEME,
SERVICE_EFFECT_COLORLOOP,
SERVICE_EFFECT_FLAME,
SERVICE_EFFECT_MORPH,
SERVICE_EFFECT_MOVE,
SERVICE_EFFECT_PULSE,
SERVICE_EFFECT_SKY,
SERVICE_EFFECT_STOP,
SERVICE_PAINT_THEME,
)
from .coordinator import LIFXUpdateCoordinator
from .util import convert_8_to_16, find_hsbk
@@ -40,30 +49,6 @@ if TYPE_CHECKING:
SCAN_INTERVAL = timedelta(seconds=10)
SERVICE_EFFECT_COLORLOOP = "effect_colorloop"
SERVICE_EFFECT_FLAME = "effect_flame"
SERVICE_EFFECT_MORPH = "effect_morph"
SERVICE_EFFECT_MOVE = "effect_move"
SERVICE_EFFECT_PULSE = "effect_pulse"
SERVICE_EFFECT_SKY = "effect_sky"
SERVICE_EFFECT_STOP = "effect_stop"
SERVICE_PAINT_THEME = "paint_theme"
ATTR_CHANGE = "change"
ATTR_CLOUD_SATURATION_MIN = "cloud_saturation_min"
ATTR_CLOUD_SATURATION_MAX = "cloud_saturation_max"
ATTR_CYCLES = "cycles"
ATTR_DIRECTION = "direction"
ATTR_PALETTE = "palette"
ATTR_PERIOD = "period"
ATTR_POWER_OFF = "power_off"
ATTR_POWER_ON = "power_on"
ATTR_SATURATION_MAX = "saturation_max"
ATTR_SATURATION_MIN = "saturation_min"
ATTR_SKY_TYPE = "sky_type"
ATTR_SPEED = "speed"
ATTR_SPREAD = "spread"
EFFECT_FLAME = "FLAME"
EFFECT_MORPH = "MORPH"
EFFECT_MOVE = "MOVE"
@@ -77,179 +62,13 @@ EFFECT_MORPH_DEFAULT_THEME = "exciting"
EFFECT_MOVE_DEFAULT_SPEED = 3
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]
EFFECT_SKY_DEFAULT_SPEED = 50
EFFECT_SKY_DEFAULT_SKY_TYPE = "Clouds"
EFFECT_SKY_DEFAULT_CLOUD_SATURATION_MIN = 50
EFFECT_SKY_DEFAULT_CLOUD_SATURATION_MAX = 180
EFFECT_SKY_SKY_TYPES = ["Sunrise", "Sunset", "Clouds"]
PAINT_THEME_DEFAULT_TRANSITION = 1
PULSE_MODE_BLINK = "blink"
PULSE_MODE_BREATHE = "breathe"
PULSE_MODE_PING = "ping"
PULSE_MODE_SOLID = "solid"
PULSE_MODE_STROBE = "strobe"
PULSE_MODES = [
PULSE_MODE_BLINK,
PULSE_MODE_BREATHE,
PULSE_MODE_PING,
PULSE_MODE_STROBE,
PULSE_MODE_SOLID,
]
LIFX_EFFECT_SCHEMA = {
probatio.Optional(ATTR_POWER_ON, default=True): cv.boolean,
}
LIFX_EFFECT_PULSE_SCHEMA = cv.make_entity_service_schema(
{
**LIFX_EFFECT_SCHEMA,
probatio.Exclusive(ATTR_BRIGHTNESS, ATTR_BRIGHTNESS): VALID_BRIGHTNESS,
probatio.Exclusive(ATTR_BRIGHTNESS_PCT, ATTR_BRIGHTNESS): VALID_BRIGHTNESS_PCT,
probatio.Exclusive(ATTR_COLOR_NAME, COLOR_GROUP): cv.string,
probatio.Exclusive(ATTR_RGB_COLOR, COLOR_GROUP): probatio.All(
probatio.Coerce(tuple), probatio.ExactSequence((cv.byte, cv.byte, cv.byte))
),
probatio.Exclusive(ATTR_XY_COLOR, COLOR_GROUP): probatio.All(
probatio.Coerce(tuple),
probatio.ExactSequence((cv.small_float, cv.small_float)),
),
probatio.Exclusive(ATTR_HS_COLOR, COLOR_GROUP): probatio.All(
probatio.Coerce(tuple),
probatio.ExactSequence(
(
probatio.All(
probatio.Coerce(float), probatio.Range(min=0, max=360)
),
probatio.All(
probatio.Coerce(float), probatio.Range(min=0, max=100)
),
)
),
),
probatio.Exclusive(ATTR_COLOR_TEMP_KELVIN, COLOR_GROUP): probatio.All(
probatio.Coerce(int), probatio.Range(min=1500, max=9000)
),
ATTR_PERIOD: probatio.All(probatio.Coerce(float), probatio.Range(min=0.05)),
ATTR_CYCLES: probatio.All(probatio.Coerce(float), probatio.Range(min=1)),
ATTR_MODE: probatio.In(PULSE_MODES),
}
)
LIFX_EFFECT_COLORLOOP_SCHEMA = cv.make_entity_service_schema(
{
**LIFX_EFFECT_SCHEMA,
probatio.Exclusive(ATTR_BRIGHTNESS, ATTR_BRIGHTNESS): VALID_BRIGHTNESS,
probatio.Exclusive(ATTR_BRIGHTNESS_PCT, ATTR_BRIGHTNESS): VALID_BRIGHTNESS_PCT,
ATTR_SATURATION_MAX: probatio.All(
probatio.Coerce(int), probatio.Clamp(min=0, max=100)
),
ATTR_SATURATION_MIN: probatio.All(
probatio.Coerce(int), probatio.Clamp(min=0, max=100)
),
ATTR_PERIOD: probatio.All(probatio.Coerce(float), probatio.Clamp(min=0.05)),
ATTR_CHANGE: probatio.All(
probatio.Coerce(float), probatio.Clamp(min=0, max=360)
),
ATTR_SPREAD: probatio.All(
probatio.Coerce(float), probatio.Clamp(min=0, max=360)
),
ATTR_TRANSITION: cv.positive_float,
}
)
LIFX_EFFECT_STOP_SCHEMA = cv.make_entity_service_schema({})
LIFX_EFFECT_FLAME_SCHEMA = cv.make_entity_service_schema(
{
**LIFX_EFFECT_SCHEMA,
ATTR_SPEED: probatio.All(probatio.Coerce(int), probatio.Clamp(min=1, max=25)),
}
)
HSBK_SCHEMA = probatio.All(
probatio.Coerce(tuple),
probatio.ExactSequence(
(
probatio.All(probatio.Coerce(float), probatio.Range(min=0, max=360)),
probatio.All(probatio.Coerce(float), probatio.Range(min=0, max=100)),
probatio.All(probatio.Coerce(float), probatio.Clamp(min=0, max=100)),
probatio.All(probatio.Coerce(int), probatio.Clamp(min=1500, max=9000)),
)
),
)
LIFX_EFFECT_MORPH_SCHEMA = cv.make_entity_service_schema(
{
**LIFX_EFFECT_SCHEMA,
ATTR_SPEED: probatio.All(probatio.Coerce(int), probatio.Clamp(min=1, max=25)),
probatio.Exclusive(ATTR_THEME, COLOR_GROUP): probatio.In(ThemeLibrary().themes),
probatio.Exclusive(ATTR_PALETTE, COLOR_GROUP): probatio.All(
cv.ensure_list, [HSBK_SCHEMA]
),
}
)
LIFX_EFFECT_MOVE_SCHEMA = cv.make_entity_service_schema(
{
**LIFX_EFFECT_SCHEMA,
ATTR_SPEED: probatio.All(
probatio.Coerce(float), probatio.Clamp(min=0.1, max=60)
),
ATTR_DIRECTION: probatio.In(EFFECT_MOVE_DIRECTIONS),
probatio.Optional(ATTR_THEME): probatio.In(ThemeLibrary().themes),
}
)
LIFX_EFFECT_SKY_SCHEMA = cv.make_entity_service_schema(
{
**LIFX_EFFECT_SCHEMA,
ATTR_SPEED: probatio.All(
probatio.Coerce(int), probatio.Clamp(min=1, max=86400)
),
ATTR_SKY_TYPE: probatio.In(EFFECT_SKY_SKY_TYPES),
ATTR_CLOUD_SATURATION_MIN: probatio.All(
probatio.Coerce(int), probatio.Clamp(min=0, max=255)
),
ATTR_CLOUD_SATURATION_MAX: probatio.All(
probatio.Coerce(int), probatio.Clamp(min=0, max=255)
),
ATTR_PALETTE: probatio.All(cv.ensure_list, [HSBK_SCHEMA]),
}
)
LIFX_PAINT_THEME_SCHEMA = cv.make_entity_service_schema(
{
**LIFX_EFFECT_SCHEMA,
ATTR_TRANSITION: probatio.All(
probatio.Coerce(int), probatio.Clamp(min=1, max=3600)
),
probatio.Exclusive(ATTR_THEME, COLOR_GROUP): probatio.In(ThemeLibrary().themes),
probatio.Exclusive(ATTR_PALETTE, COLOR_GROUP): probatio.All(
cv.ensure_list, [HSBK_SCHEMA]
),
}
)
SERVICES_SCHEMA = {
SERVICE_EFFECT_COLORLOOP: LIFX_EFFECT_COLORLOOP_SCHEMA,
SERVICE_EFFECT_FLAME: LIFX_EFFECT_FLAME_SCHEMA,
SERVICE_EFFECT_MORPH: LIFX_EFFECT_MORPH_SCHEMA,
SERVICE_EFFECT_MOVE: LIFX_EFFECT_MOVE_SCHEMA,
SERVICE_EFFECT_PULSE: LIFX_EFFECT_PULSE_SCHEMA,
SERVICE_EFFECT_SKY: LIFX_EFFECT_SKY_SCHEMA,
SERVICE_EFFECT_STOP: LIFX_EFFECT_STOP_SCHEMA,
SERVICE_PAINT_THEME: LIFX_PAINT_THEME_SCHEMA,
}
class LIFXManager:
"""Representation of all known LIFX entities."""
@@ -260,12 +79,6 @@ class LIFXManager:
self.effects_conductor = aiolifx_effects.Conductor(hass.loop)
self.entity_id_to_coordinator: dict[str, LIFXUpdateCoordinator] = {}
@callback
def async_unload(self) -> None:
"""Release resources."""
for service in SERVICES_SCHEMA:
self.hass.services.async_remove(DOMAIN, service)
@callback
def async_register_entity(
self, entity_id: str, coordinator: LIFXUpdateCoordinator
@@ -280,24 +93,6 @@ class LIFXManager:
return unregister_entity
@callback
def async_setup(self) -> None:
"""Register the LIFX effects as hass service calls."""
async def service_handler(service: ServiceCall) -> None:
"""Apply a service, i.e. start an effect."""
referenced = async_extract_referenced_entity_ids(
self.hass, TargetSelection(service.data)
)
all_referenced = referenced.referenced | referenced.indirectly_referenced
if all_referenced:
await self.start_effect(all_referenced, service.service, **service.data)
for service, schema in SERVICES_SCHEMA.items():
self.hass.services.async_register(
DOMAIN, service, service_handler, schema=schema
)
@staticmethod
def build_theme(theme_name: str = "exciting", palette: list | None = None) -> Theme:
"""Either return the predefined theme or build one from the palette."""
+262
View File
@@ -0,0 +1,262 @@
"""Support for LIFX services."""
from typing import TYPE_CHECKING
from aiolifx_themes.themes import ThemeLibrary
import probatio
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
ATTR_BRIGHTNESS_PCT,
ATTR_COLOR_NAME,
ATTR_COLOR_TEMP_KELVIN,
ATTR_HS_COLOR,
ATTR_RGB_COLOR,
ATTR_TRANSITION,
ATTR_XY_COLOR,
COLOR_GROUP,
VALID_BRIGHTNESS,
VALID_BRIGHTNESS_PCT,
)
from homeassistant.const import ATTR_MODE
from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.target import (
TargetSelection,
async_extract_referenced_entity_ids,
)
from .const import (
ATTR_CHANGE,
ATTR_CLOUD_SATURATION_MAX,
ATTR_CLOUD_SATURATION_MIN,
ATTR_CYCLES,
ATTR_DIRECTION,
ATTR_PALETTE,
ATTR_PERIOD,
ATTR_POWER_ON,
ATTR_SATURATION_MAX,
ATTR_SATURATION_MIN,
ATTR_SKY_TYPE,
ATTR_SPEED,
ATTR_SPREAD,
ATTR_THEME,
DATA_LIFX_MANAGER,
DOMAIN,
SERVICE_EFFECT_COLORLOOP,
SERVICE_EFFECT_FLAME,
SERVICE_EFFECT_MORPH,
SERVICE_EFFECT_MOVE,
SERVICE_EFFECT_PULSE,
SERVICE_EFFECT_SKY,
SERVICE_EFFECT_STOP,
SERVICE_PAINT_THEME,
)
from .util import async_entry_is_legacy
if TYPE_CHECKING:
from .manager import LIFXManager
EFFECT_MOVE_DIRECTION_LEFT = "left"
EFFECT_MOVE_DIRECTION_RIGHT = "right"
EFFECT_MOVE_DIRECTIONS = [EFFECT_MOVE_DIRECTION_LEFT, EFFECT_MOVE_DIRECTION_RIGHT]
EFFECT_SKY_SKY_TYPES = ["Sunrise", "Sunset", "Clouds"]
PULSE_MODE_BLINK = "blink"
PULSE_MODE_BREATHE = "breathe"
PULSE_MODE_PING = "ping"
PULSE_MODE_SOLID = "solid"
PULSE_MODE_STROBE = "strobe"
PULSE_MODES = [
PULSE_MODE_BLINK,
PULSE_MODE_BREATHE,
PULSE_MODE_PING,
PULSE_MODE_STROBE,
PULSE_MODE_SOLID,
]
LIFX_EFFECT_SCHEMA = {
probatio.Optional(ATTR_POWER_ON, default=True): cv.boolean,
}
LIFX_EFFECT_PULSE_SCHEMA = cv.make_entity_service_schema(
{
**LIFX_EFFECT_SCHEMA,
probatio.Exclusive(ATTR_BRIGHTNESS, ATTR_BRIGHTNESS): VALID_BRIGHTNESS,
probatio.Exclusive(ATTR_BRIGHTNESS_PCT, ATTR_BRIGHTNESS): VALID_BRIGHTNESS_PCT,
probatio.Exclusive(ATTR_COLOR_NAME, COLOR_GROUP): cv.string,
probatio.Exclusive(ATTR_RGB_COLOR, COLOR_GROUP): probatio.All(
probatio.Coerce(tuple), probatio.ExactSequence((cv.byte, cv.byte, cv.byte))
),
probatio.Exclusive(ATTR_XY_COLOR, COLOR_GROUP): probatio.All(
probatio.Coerce(tuple),
probatio.ExactSequence((cv.small_float, cv.small_float)),
),
probatio.Exclusive(ATTR_HS_COLOR, COLOR_GROUP): probatio.All(
probatio.Coerce(tuple),
probatio.ExactSequence(
(
probatio.All(
probatio.Coerce(float), probatio.Range(min=0, max=360)
),
probatio.All(
probatio.Coerce(float), probatio.Range(min=0, max=100)
),
)
),
),
probatio.Exclusive(ATTR_COLOR_TEMP_KELVIN, COLOR_GROUP): probatio.All(
probatio.Coerce(int), probatio.Range(min=1500, max=9000)
),
ATTR_PERIOD: probatio.All(probatio.Coerce(float), probatio.Range(min=0.05)),
ATTR_CYCLES: probatio.All(probatio.Coerce(float), probatio.Range(min=1)),
ATTR_MODE: probatio.In(PULSE_MODES),
}
)
LIFX_EFFECT_COLORLOOP_SCHEMA = cv.make_entity_service_schema(
{
**LIFX_EFFECT_SCHEMA,
probatio.Exclusive(ATTR_BRIGHTNESS, ATTR_BRIGHTNESS): VALID_BRIGHTNESS,
probatio.Exclusive(ATTR_BRIGHTNESS_PCT, ATTR_BRIGHTNESS): VALID_BRIGHTNESS_PCT,
ATTR_SATURATION_MAX: probatio.All(
probatio.Coerce(int), probatio.Clamp(min=0, max=100)
),
ATTR_SATURATION_MIN: probatio.All(
probatio.Coerce(int), probatio.Clamp(min=0, max=100)
),
ATTR_PERIOD: probatio.All(probatio.Coerce(float), probatio.Clamp(min=0.05)),
ATTR_CHANGE: probatio.All(
probatio.Coerce(float), probatio.Clamp(min=0, max=360)
),
ATTR_SPREAD: probatio.All(
probatio.Coerce(float), probatio.Clamp(min=0, max=360)
),
ATTR_TRANSITION: cv.positive_float,
}
)
LIFX_EFFECT_STOP_SCHEMA = cv.make_entity_service_schema({})
LIFX_EFFECT_FLAME_SCHEMA = cv.make_entity_service_schema(
{
**LIFX_EFFECT_SCHEMA,
ATTR_SPEED: probatio.All(probatio.Coerce(int), probatio.Clamp(min=1, max=25)),
}
)
HSBK_SCHEMA = probatio.All(
probatio.Coerce(tuple),
probatio.ExactSequence(
(
probatio.All(probatio.Coerce(float), probatio.Range(min=0, max=360)),
probatio.All(probatio.Coerce(float), probatio.Range(min=0, max=100)),
probatio.All(probatio.Coerce(float), probatio.Clamp(min=0, max=100)),
probatio.All(probatio.Coerce(int), probatio.Clamp(min=1500, max=9000)),
)
),
)
LIFX_EFFECT_MORPH_SCHEMA = cv.make_entity_service_schema(
{
**LIFX_EFFECT_SCHEMA,
ATTR_SPEED: probatio.All(probatio.Coerce(int), probatio.Clamp(min=1, max=25)),
probatio.Exclusive(ATTR_THEME, COLOR_GROUP): probatio.In(ThemeLibrary().themes),
probatio.Exclusive(ATTR_PALETTE, COLOR_GROUP): probatio.All(
cv.ensure_list, [HSBK_SCHEMA]
),
}
)
LIFX_EFFECT_MOVE_SCHEMA = cv.make_entity_service_schema(
{
**LIFX_EFFECT_SCHEMA,
ATTR_SPEED: probatio.All(
probatio.Coerce(float), probatio.Clamp(min=0.1, max=60)
),
ATTR_DIRECTION: probatio.In(EFFECT_MOVE_DIRECTIONS),
probatio.Optional(ATTR_THEME): probatio.In(ThemeLibrary().themes),
}
)
LIFX_EFFECT_SKY_SCHEMA = cv.make_entity_service_schema(
{
**LIFX_EFFECT_SCHEMA,
ATTR_SPEED: probatio.All(
probatio.Coerce(int), probatio.Clamp(min=1, max=86400)
),
ATTR_SKY_TYPE: probatio.In(EFFECT_SKY_SKY_TYPES),
ATTR_CLOUD_SATURATION_MIN: probatio.All(
probatio.Coerce(int), probatio.Clamp(min=0, max=255)
),
ATTR_CLOUD_SATURATION_MAX: probatio.All(
probatio.Coerce(int), probatio.Clamp(min=0, max=255)
),
ATTR_PALETTE: probatio.All(cv.ensure_list, [HSBK_SCHEMA]),
}
)
LIFX_PAINT_THEME_SCHEMA = cv.make_entity_service_schema(
{
**LIFX_EFFECT_SCHEMA,
ATTR_TRANSITION: probatio.All(
probatio.Coerce(int), probatio.Clamp(min=1, max=3600)
),
probatio.Exclusive(ATTR_THEME, COLOR_GROUP): probatio.In(ThemeLibrary().themes),
probatio.Exclusive(ATTR_PALETTE, COLOR_GROUP): probatio.All(
cv.ensure_list, [HSBK_SCHEMA]
),
}
)
SERVICES_SCHEMA = {
SERVICE_EFFECT_COLORLOOP: LIFX_EFFECT_COLORLOOP_SCHEMA,
SERVICE_EFFECT_FLAME: LIFX_EFFECT_FLAME_SCHEMA,
SERVICE_EFFECT_MORPH: LIFX_EFFECT_MORPH_SCHEMA,
SERVICE_EFFECT_MOVE: LIFX_EFFECT_MOVE_SCHEMA,
SERVICE_EFFECT_PULSE: LIFX_EFFECT_PULSE_SCHEMA,
SERVICE_EFFECT_SKY: LIFX_EFFECT_SKY_SCHEMA,
SERVICE_EFFECT_STOP: LIFX_EFFECT_STOP_SCHEMA,
SERVICE_PAINT_THEME: LIFX_PAINT_THEME_SCHEMA,
}
def _get_manager(service: ServiceCall) -> LIFXManager:
"""Return the LIFX manager, raising a user-facing error if unavailable."""
hass = service.hass
# The manager is stored before the connection and first refresh are awaited,
# so its presence alone does not mean a device is usable.
if (manager := hass.data.get(DATA_LIFX_MANAGER)) is None or all(
async_entry_is_legacy(entry)
for entry in hass.config_entries.async_loaded_entries(DOMAIN)
):
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="not_loaded",
)
return manager
async def _async_start_effect(service: ServiceCall) -> None:
"""Apply a service, i.e. start an effect."""
manager = _get_manager(service)
referenced = async_extract_referenced_entity_ids(
service.hass, TargetSelection(service.data)
)
all_referenced = referenced.referenced | referenced.indirectly_referenced
if all_referenced:
await manager.start_effect(all_referenced, service.service, **service.data)
@callback
def async_setup_services(hass: HomeAssistant) -> None:
"""Register the LIFX effect services."""
for service, schema in SERVICES_SCHEMA.items():
hass.services.async_register(
DOMAIN, service, _async_start_effect, schema=schema
)
@@ -50,6 +50,11 @@
}
}
},
"exceptions": {
"not_loaded": {
"message": "LIFX is not loaded, so this action cannot be performed."
}
},
"services": {
"effect_colorloop": {
"description": "Runs an effect with looping colors.",
+3 -3
View File
@@ -9,13 +9,12 @@ import pytest
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 (
from homeassistant.components.lifx.const import (
ATTR_CLOUD_SATURATION_MAX,
ATTR_CLOUD_SATURATION_MIN,
ATTR_DIRECTION,
ATTR_PALETTE,
ATTR_POWER,
ATTR_SATURATION_MAX,
ATTR_SATURATION_MIN,
ATTR_SKY_TYPE,
@@ -27,6 +26,7 @@ from homeassistant.components.lifx.manager import (
SERVICE_EFFECT_SKY,
SERVICE_PAINT_THEME,
)
from homeassistant.components.lifx.light import ATTR_INFRARED, ATTR_ZONES
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
ATTR_BRIGHTNESS_PCT,
+86
View File
@@ -0,0 +1,86 @@
"""Tests for the LIFX services."""
import pytest
from homeassistant.components.lifx import DOMAIN
from homeassistant.components.lifx.const import (
DATA_LIFX_MANAGER,
SERVICE_EFFECT_COLORLOOP,
SERVICE_EFFECT_FLAME,
SERVICE_EFFECT_MORPH,
SERVICE_EFFECT_MOVE,
SERVICE_EFFECT_PULSE,
SERVICE_EFFECT_SKY,
SERVICE_EFFECT_STOP,
SERVICE_PAINT_THEME,
)
from homeassistant.components.lifx.manager import LIFXManager
from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ServiceValidationError
from homeassistant.setup import async_setup_component
SERVICES = (
SERVICE_EFFECT_COLORLOOP,
SERVICE_EFFECT_FLAME,
SERVICE_EFFECT_MORPH,
SERVICE_EFFECT_MOVE,
SERVICE_EFFECT_PULSE,
SERVICE_EFFECT_SKY,
SERVICE_EFFECT_STOP,
SERVICE_PAINT_THEME,
)
@pytest.mark.usefixtures("mock_discovery")
async def test_services_registered_without_entry(hass: HomeAssistant) -> None:
"""Test the effect actions are registered during component setup."""
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
await hass.async_block_till_done()
for service in SERVICES:
assert hass.services.has_service(DOMAIN, service)
@pytest.mark.parametrize("service", SERVICES)
@pytest.mark.usefixtures("mock_discovery")
async def test_service_without_manager_raises(
hass: HomeAssistant, service: str
) -> None:
"""Test the effect actions raise when no config entry is loaded."""
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
await hass.async_block_till_done()
with pytest.raises(ServiceValidationError) as err:
await hass.services.async_call(
DOMAIN, service, {ATTR_ENTITY_ID: "light.test"}, blocking=True
)
assert err.value.translation_domain == DOMAIN
assert err.value.translation_key == "not_loaded"
assert "LIFX is not loaded" in str(err.value)
@pytest.mark.parametrize("service", SERVICES)
@pytest.mark.usefixtures("mock_discovery")
async def test_service_with_manager_but_no_loaded_entry_raises(
hass: HomeAssistant, service: str
) -> None:
"""Test the actions raise when a failed setup left a manager behind.
async_setup_entry stores the manager before awaiting the connection and the
first refresh, so a setup retry leaves an empty manager in hass.data.
"""
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
await hass.async_block_till_done()
# Simulate the leftover manager from a setup that raised ConfigEntryNotReady.
hass.data[DATA_LIFX_MANAGER] = LIFXManager(hass)
assert not hass.config_entries.async_loaded_entries(DOMAIN)
with pytest.raises(ServiceValidationError) as err:
await hass.services.async_call(
DOMAIN, service, {ATTR_ENTITY_ID: "light.test"}, blocking=True
)
assert err.value.translation_key == "not_loaded"