298 lines
9.5 KiB
Python
298 lines
9.5 KiB
Python
"""Support for LED numbers."""
|
|
from __future__ import annotations
|
|
|
|
from abc import abstractmethod
|
|
import logging
|
|
from typing import cast
|
|
|
|
from flux_led.protocol import (
|
|
MUSIC_PIXELS_MAX,
|
|
MUSIC_PIXELS_PER_SEGMENT_MAX,
|
|
MUSIC_SEGMENTS_MAX,
|
|
PIXELS_MAX,
|
|
PIXELS_PER_SEGMENT_MAX,
|
|
SEGMENTS_MAX,
|
|
)
|
|
|
|
from homeassistant import config_entries
|
|
from homeassistant.components.light import EFFECT_RANDOM
|
|
from homeassistant.components.number import NumberEntity, NumberMode
|
|
from homeassistant.const import CONF_NAME
|
|
from homeassistant.core import HomeAssistant
|
|
from homeassistant.exceptions import HomeAssistantError
|
|
from homeassistant.helpers.debounce import Debouncer
|
|
from homeassistant.helpers.entity import EntityCategory
|
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
|
|
|
from .const import DOMAIN
|
|
from .coordinator import FluxLedUpdateCoordinator
|
|
from .entity import FluxEntity
|
|
from .util import _effect_brightness
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
DEBOUNCE_TIME = 1
|
|
|
|
|
|
async def async_setup_entry(
|
|
hass: HomeAssistant,
|
|
entry: config_entries.ConfigEntry,
|
|
async_add_entities: AddEntitiesCallback,
|
|
) -> None:
|
|
"""Set up the Flux lights."""
|
|
coordinator: FluxLedUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
|
|
device = coordinator.device
|
|
entities: list[
|
|
FluxSpeedNumber
|
|
| FluxPixelsPerSegmentNumber
|
|
| FluxSegmentsNumber
|
|
| FluxMusicPixelsPerSegmentNumber
|
|
| FluxMusicSegmentsNumber
|
|
] = []
|
|
name = entry.data.get(CONF_NAME, entry.title)
|
|
base_unique_id = entry.unique_id or entry.entry_id
|
|
|
|
if device.pixels_per_segment is not None:
|
|
entities.append(
|
|
FluxPixelsPerSegmentNumber(
|
|
coordinator,
|
|
base_unique_id,
|
|
f"{name} Pixels Per Segment",
|
|
"pixels_per_segment",
|
|
)
|
|
)
|
|
if device.segments is not None:
|
|
entities.append(
|
|
FluxSegmentsNumber(
|
|
coordinator, base_unique_id, f"{name} Segments", "segments"
|
|
)
|
|
)
|
|
if device.music_pixels_per_segment is not None:
|
|
entities.append(
|
|
FluxMusicPixelsPerSegmentNumber(
|
|
coordinator,
|
|
base_unique_id,
|
|
f"{name} Music Pixels Per Segment",
|
|
"music_pixels_per_segment",
|
|
)
|
|
)
|
|
if device.music_segments is not None:
|
|
entities.append(
|
|
FluxMusicSegmentsNumber(
|
|
coordinator, base_unique_id, f"{name} Music Segments", "music_segments"
|
|
)
|
|
)
|
|
if device.effect_list and device.effect_list != [EFFECT_RANDOM]:
|
|
entities.append(
|
|
FluxSpeedNumber(coordinator, base_unique_id, f"{name} Effect Speed", None)
|
|
)
|
|
|
|
if entities:
|
|
async_add_entities(entities)
|
|
|
|
|
|
class FluxSpeedNumber(
|
|
FluxEntity, CoordinatorEntity[FluxLedUpdateCoordinator], NumberEntity
|
|
):
|
|
"""Defines a flux_led speed number."""
|
|
|
|
_attr_native_min_value = 1
|
|
_attr_native_max_value = 100
|
|
_attr_native_step = 1
|
|
_attr_mode = NumberMode.SLIDER
|
|
_attr_icon = "mdi:speedometer"
|
|
|
|
@property
|
|
def native_value(self) -> float:
|
|
"""Return the effect speed."""
|
|
return cast(float, self._device.speed)
|
|
|
|
async def async_set_native_value(self, value: float) -> None:
|
|
"""Set the flux speed value."""
|
|
current_effect = self._device.effect
|
|
new_speed = int(value)
|
|
if not current_effect:
|
|
raise HomeAssistantError(
|
|
"Speed can only be adjusted when an effect is active"
|
|
)
|
|
if not self._device.speed_adjust_off and not self._device.is_on:
|
|
raise HomeAssistantError("Speed can only be adjusted when the light is on")
|
|
await self._device.async_set_effect(
|
|
current_effect, new_speed, _effect_brightness(self._device.brightness)
|
|
)
|
|
await self.coordinator.async_request_refresh()
|
|
|
|
|
|
class FluxConfigNumber(
|
|
FluxEntity, CoordinatorEntity[FluxLedUpdateCoordinator], NumberEntity
|
|
):
|
|
"""Base class for flux config numbers."""
|
|
|
|
_attr_entity_category = EntityCategory.CONFIG
|
|
_attr_native_min_value = 1
|
|
_attr_native_step = 1
|
|
_attr_mode = NumberMode.BOX
|
|
|
|
def __init__(
|
|
self,
|
|
coordinator: FluxLedUpdateCoordinator,
|
|
base_unique_id: str,
|
|
name: str,
|
|
key: str | None,
|
|
) -> None:
|
|
"""Initialize the flux number."""
|
|
super().__init__(coordinator, base_unique_id, name, key)
|
|
self._debouncer: Debouncer | None = None
|
|
self._pending_value: int | None = None
|
|
|
|
async def async_added_to_hass(self) -> None:
|
|
"""Set up the debouncer when adding to hass."""
|
|
self._debouncer = Debouncer(
|
|
hass=self.hass,
|
|
logger=_LOGGER,
|
|
cooldown=DEBOUNCE_TIME,
|
|
immediate=False,
|
|
function=self._async_set_native_value,
|
|
)
|
|
await super().async_added_to_hass()
|
|
|
|
async def async_set_native_value(self, value: float) -> None:
|
|
"""Set the value."""
|
|
self._pending_value = int(value)
|
|
assert self._debouncer is not None
|
|
await self._debouncer.async_call()
|
|
|
|
@abstractmethod
|
|
async def _async_set_native_value(self) -> None:
|
|
"""Call on debounce to set the value."""
|
|
|
|
def _pixels_and_segments_fit_in_music_mode(self) -> bool:
|
|
"""Check if the base pixel and segment settings will fit for music mode.
|
|
|
|
If they fit, they do not need to be configured.
|
|
"""
|
|
pixels_per_segment = self._device.pixels_per_segment
|
|
segments = self._device.segments
|
|
assert pixels_per_segment is not None
|
|
assert segments is not None
|
|
return bool(
|
|
pixels_per_segment <= MUSIC_PIXELS_PER_SEGMENT_MAX
|
|
and segments <= MUSIC_SEGMENTS_MAX
|
|
and pixels_per_segment * segments <= MUSIC_PIXELS_MAX
|
|
)
|
|
|
|
|
|
class FluxPixelsPerSegmentNumber(FluxConfigNumber):
|
|
"""Defines a flux_led pixels per segment number."""
|
|
|
|
_attr_icon = "mdi:dots-grid"
|
|
|
|
@property
|
|
def native_max_value(self) -> int:
|
|
"""Return the max value."""
|
|
return min(
|
|
PIXELS_PER_SEGMENT_MAX, int(PIXELS_MAX / (self._device.segments or 1))
|
|
)
|
|
|
|
@property
|
|
def native_value(self) -> int:
|
|
"""Return the pixels per segment."""
|
|
assert self._device.pixels_per_segment is not None
|
|
return self._device.pixels_per_segment
|
|
|
|
async def _async_set_native_value(self) -> None:
|
|
"""Set the pixels per segment."""
|
|
assert self._pending_value is not None
|
|
await self._device.async_set_device_config(
|
|
pixels_per_segment=self._pending_value
|
|
)
|
|
|
|
|
|
class FluxSegmentsNumber(FluxConfigNumber):
|
|
"""Defines a flux_led segments number."""
|
|
|
|
_attr_icon = "mdi:segment"
|
|
|
|
@property
|
|
def native_max_value(self) -> int:
|
|
"""Return the max value."""
|
|
assert self._device.pixels_per_segment is not None
|
|
return min(
|
|
SEGMENTS_MAX, int(PIXELS_MAX / (self._device.pixels_per_segment or 1))
|
|
)
|
|
|
|
@property
|
|
def native_value(self) -> int:
|
|
"""Return the segments."""
|
|
assert self._device.segments is not None
|
|
return self._device.segments
|
|
|
|
async def _async_set_native_value(self) -> None:
|
|
"""Set the segments."""
|
|
assert self._pending_value is not None
|
|
await self._device.async_set_device_config(segments=self._pending_value)
|
|
|
|
|
|
class FluxMusicNumber(FluxConfigNumber):
|
|
"""A number that is only available if the base pixels do not fit in music mode."""
|
|
|
|
@property
|
|
def available(self) -> bool:
|
|
"""Return if music pixels per segment can be set."""
|
|
return super().available and not self._pixels_and_segments_fit_in_music_mode()
|
|
|
|
|
|
class FluxMusicPixelsPerSegmentNumber(FluxMusicNumber):
|
|
"""Defines a flux_led music pixels per segment number."""
|
|
|
|
_attr_icon = "mdi:dots-grid"
|
|
|
|
@property
|
|
def native_max_value(self) -> int:
|
|
"""Return the max value."""
|
|
assert self._device.music_segments is not None
|
|
return min(
|
|
MUSIC_PIXELS_PER_SEGMENT_MAX,
|
|
int(MUSIC_PIXELS_MAX / (self._device.music_segments or 1)),
|
|
)
|
|
|
|
@property
|
|
def native_value(self) -> int:
|
|
"""Return the music pixels per segment."""
|
|
assert self._device.music_pixels_per_segment is not None
|
|
return self._device.music_pixels_per_segment
|
|
|
|
async def _async_set_native_value(self) -> None:
|
|
"""Set the music pixels per segment."""
|
|
assert self._pending_value is not None
|
|
await self._device.async_set_device_config(
|
|
music_pixels_per_segment=self._pending_value
|
|
)
|
|
|
|
|
|
class FluxMusicSegmentsNumber(FluxMusicNumber):
|
|
"""Defines a flux_led music segments number."""
|
|
|
|
_attr_icon = "mdi:segment"
|
|
|
|
@property
|
|
def native_max_value(self) -> int:
|
|
"""Return the max value."""
|
|
assert self._device.pixels_per_segment is not None
|
|
return min(
|
|
MUSIC_SEGMENTS_MAX,
|
|
int(MUSIC_PIXELS_MAX / (self._device.music_pixels_per_segment or 1)),
|
|
)
|
|
|
|
@property
|
|
def native_value(self) -> int:
|
|
"""Return the music segments."""
|
|
assert self._device.music_segments is not None
|
|
return self._device.music_segments
|
|
|
|
async def _async_set_native_value(self) -> None:
|
|
"""Set the music segments."""
|
|
assert self._pending_value is not None
|
|
await self._device.async_set_device_config(music_segments=self._pending_value)
|