mirror of
https://github.com/home-assistant/core.git
synced 2026-09-26 17:31:15 -04:00
Move service registration to async_setup in lifx (#181196)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
Reference in New Issue
Block a user