0a16dc9058
- coordinator.update_source now auto-injects source_type from cached data so the server's discriminated-union body validates (was rejecting fallback_color updates with 422). - Add Fallback Timeout (number) and Interpolation (select) entities per api_input CSS source. - Light no longer zeros fallback_color on turn_off; state restored across HA restarts via RestoreEntity so the chosen default color persists. - Bump manifest to 0.2.2 and update en/ru translations.
303 lines
10 KiB
Python
303 lines
10 KiB
Python
"""Number platform for LED Screen Controller (device brightness & HA light settings)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from typing import Any
|
|
|
|
from homeassistant.components.number import NumberEntity, NumberMode
|
|
from homeassistant.config_entries import ConfigEntry
|
|
from homeassistant.core import HomeAssistant
|
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
|
|
|
from .const import DOMAIN, DATA_COORDINATOR, TARGET_TYPE_HA_LIGHT
|
|
from .coordinator import LedGrabCoordinator
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
|
async def async_setup_entry(
|
|
hass: HomeAssistant,
|
|
entry: ConfigEntry,
|
|
async_add_entities: AddEntitiesCallback,
|
|
) -> None:
|
|
"""Set up LED Screen Controller number entities."""
|
|
data = hass.data[DOMAIN][entry.entry_id]
|
|
coordinator: LedGrabCoordinator = data[DATA_COORDINATOR]
|
|
|
|
entities: list[NumberEntity] = []
|
|
if coordinator.data:
|
|
for source in coordinator.data.get("css_sources") or []:
|
|
if source.get("source_type") == "api_input":
|
|
entities.append(ApiInputTimeout(coordinator, source, entry.entry_id))
|
|
|
|
if coordinator.data and "targets" in coordinator.data:
|
|
devices = coordinator.data.get("devices") or {}
|
|
|
|
for target_id, target_data in coordinator.data["targets"].items():
|
|
info = target_data["info"]
|
|
target_type = info.get("target_type", "led")
|
|
|
|
if target_type == TARGET_TYPE_HA_LIGHT:
|
|
# HA Light target — expose tunable settings
|
|
entities.append(HALightUpdateRate(coordinator, target_id, entry.entry_id))
|
|
entities.append(HALightTransition(coordinator, target_id, entry.entry_id))
|
|
entities.append(HALightMinBrightness(coordinator, target_id, entry.entry_id))
|
|
entities.append(HALightColorTolerance(coordinator, target_id, entry.entry_id))
|
|
continue
|
|
|
|
# LED target — brightness lives on the device
|
|
device_id = info.get("device_id", "")
|
|
if not device_id:
|
|
continue
|
|
|
|
device_data = devices.get(device_id)
|
|
if not device_data:
|
|
continue
|
|
|
|
capabilities = device_data.get("info", {}).get("capabilities") or []
|
|
if "brightness_control" not in capabilities or "static_color" in capabilities:
|
|
continue
|
|
|
|
entities.append(
|
|
LedGrabBrightness(
|
|
coordinator,
|
|
target_id,
|
|
device_id,
|
|
entry.entry_id,
|
|
)
|
|
)
|
|
|
|
async_add_entities(entities)
|
|
|
|
|
|
class LedGrabBrightness(CoordinatorEntity, NumberEntity):
|
|
"""Brightness control for an LED device associated with a target."""
|
|
|
|
_attr_has_entity_name = True
|
|
_attr_native_min_value = 0
|
|
_attr_native_max_value = 255
|
|
_attr_native_step = 1
|
|
_attr_mode = NumberMode.SLIDER
|
|
_attr_icon = "mdi:brightness-6"
|
|
|
|
def __init__(
|
|
self,
|
|
coordinator: LedGrabCoordinator,
|
|
target_id: str,
|
|
device_id: str,
|
|
entry_id: str,
|
|
) -> None:
|
|
"""Initialize the brightness number."""
|
|
super().__init__(coordinator)
|
|
self._target_id = target_id
|
|
self._device_id = device_id
|
|
self._entry_id = entry_id
|
|
self._attr_unique_id = f"{target_id}_brightness"
|
|
self._attr_translation_key = "brightness"
|
|
|
|
@property
|
|
def device_info(self) -> dict[str, Any]:
|
|
"""Return device information."""
|
|
return {"identifiers": {(DOMAIN, self._target_id)}}
|
|
|
|
@property
|
|
def native_value(self) -> float | None:
|
|
"""Return the current brightness value."""
|
|
if not self.coordinator.data:
|
|
return None
|
|
device_data = self.coordinator.data.get("devices", {}).get(self._device_id)
|
|
if not device_data:
|
|
return None
|
|
return device_data.get("brightness")
|
|
|
|
@property
|
|
def available(self) -> bool:
|
|
"""Return if entity is available."""
|
|
if not self.coordinator.data:
|
|
return False
|
|
targets = self.coordinator.data.get("targets", {})
|
|
devices = self.coordinator.data.get("devices", {})
|
|
return self._target_id in targets and self._device_id in devices
|
|
|
|
async def async_set_native_value(self, value: float) -> None:
|
|
"""Set brightness value."""
|
|
await self.coordinator.set_brightness(self._device_id, int(value))
|
|
|
|
|
|
# --- HA Light target number entities ---
|
|
|
|
|
|
class _HALightNumberBase(CoordinatorEntity, NumberEntity):
|
|
"""Base class for HA Light target number entities."""
|
|
|
|
_attr_has_entity_name = True
|
|
_attr_mode = NumberMode.SLIDER
|
|
|
|
def __init__(
|
|
self,
|
|
coordinator: LedGrabCoordinator,
|
|
target_id: str,
|
|
entry_id: str,
|
|
*,
|
|
field_name: str,
|
|
) -> None:
|
|
super().__init__(coordinator)
|
|
self._target_id = target_id
|
|
self._entry_id = entry_id
|
|
self._field_name = field_name
|
|
|
|
@property
|
|
def device_info(self) -> dict[str, Any]:
|
|
return {"identifiers": {(DOMAIN, self._target_id)}}
|
|
|
|
@property
|
|
def native_value(self) -> float | None:
|
|
target_data = self._get_target_data()
|
|
if not target_data:
|
|
return None
|
|
return target_data.get("info", {}).get(self._field_name)
|
|
|
|
@property
|
|
def available(self) -> bool:
|
|
return self._get_target_data() is not None
|
|
|
|
async def async_set_native_value(self, value: float) -> None:
|
|
await self.coordinator.update_target(self._target_id, **{self._field_name: round(value, 2)})
|
|
|
|
def _get_target_data(self) -> dict[str, Any] | None:
|
|
if not self.coordinator.data:
|
|
return None
|
|
return self.coordinator.data.get("targets", {}).get(self._target_id)
|
|
|
|
|
|
class HALightUpdateRate(_HALightNumberBase):
|
|
"""Update rate (Hz) for an HA Light target."""
|
|
|
|
_attr_native_min_value = 0.5
|
|
_attr_native_max_value = 5.0
|
|
_attr_native_step = 0.5
|
|
_attr_native_unit_of_measurement = "Hz"
|
|
_attr_icon = "mdi:update"
|
|
|
|
def __init__(
|
|
self, coordinator: LedGrabCoordinator, target_id: str, entry_id: str
|
|
) -> None:
|
|
super().__init__(coordinator, target_id, entry_id, field_name="update_rate")
|
|
self._attr_unique_id = f"{target_id}_update_rate"
|
|
self._attr_translation_key = "ha_light_update_rate"
|
|
|
|
|
|
class HALightTransition(_HALightNumberBase):
|
|
"""Transition time (seconds) for an HA Light target."""
|
|
|
|
_attr_native_min_value = 0.0
|
|
_attr_native_max_value = 10.0
|
|
_attr_native_step = 0.1
|
|
_attr_native_unit_of_measurement = "s"
|
|
_attr_icon = "mdi:transition-masked"
|
|
|
|
def __init__(
|
|
self, coordinator: LedGrabCoordinator, target_id: str, entry_id: str
|
|
) -> None:
|
|
super().__init__(coordinator, target_id, entry_id, field_name="transition")
|
|
self._attr_unique_id = f"{target_id}_transition"
|
|
self._attr_translation_key = "ha_light_transition"
|
|
|
|
|
|
class HALightMinBrightness(_HALightNumberBase):
|
|
"""Minimum brightness threshold for an HA Light target."""
|
|
|
|
_attr_native_min_value = 0
|
|
_attr_native_max_value = 255
|
|
_attr_native_step = 1
|
|
_attr_icon = "mdi:brightness-4"
|
|
|
|
def __init__(
|
|
self, coordinator: LedGrabCoordinator, target_id: str, entry_id: str
|
|
) -> None:
|
|
super().__init__(coordinator, target_id, entry_id, field_name="min_brightness_threshold")
|
|
self._attr_unique_id = f"{target_id}_min_brightness"
|
|
self._attr_translation_key = "ha_light_min_brightness"
|
|
|
|
|
|
class HALightColorTolerance(_HALightNumberBase):
|
|
"""Color tolerance (RGB delta skip threshold) for an HA Light target."""
|
|
|
|
_attr_native_min_value = 0
|
|
_attr_native_max_value = 50
|
|
_attr_native_step = 1
|
|
_attr_icon = "mdi:palette-outline"
|
|
|
|
def __init__(
|
|
self, coordinator: LedGrabCoordinator, target_id: str, entry_id: str
|
|
) -> None:
|
|
super().__init__(coordinator, target_id, entry_id, field_name="color_tolerance")
|
|
self._attr_unique_id = f"{target_id}_color_tolerance"
|
|
self._attr_translation_key = "ha_light_color_tolerance"
|
|
|
|
|
|
# --- api_input CSS source number entities ---
|
|
|
|
|
|
class ApiInputTimeout(CoordinatorEntity, NumberEntity):
|
|
"""Fallback timeout for an api_input CSS source.
|
|
|
|
The server reverts the strip to ``fallback_color`` when no LED data has
|
|
arrived for this many seconds. Stored server-side as a ``BindableFloat``
|
|
(plain number when unbound, ``{"value", "source_id"}`` when bound).
|
|
"""
|
|
|
|
_attr_has_entity_name = True
|
|
_attr_native_min_value = 0.0
|
|
_attr_native_max_value = 300.0
|
|
_attr_native_step = 0.5
|
|
_attr_native_unit_of_measurement = "s"
|
|
_attr_mode = NumberMode.BOX
|
|
_attr_icon = "mdi:timer-sand"
|
|
|
|
def __init__(
|
|
self,
|
|
coordinator: LedGrabCoordinator,
|
|
source: dict[str, Any],
|
|
entry_id: str,
|
|
) -> None:
|
|
super().__init__(coordinator)
|
|
self._source_id: str = source["id"]
|
|
self._entry_id = entry_id
|
|
self._attr_unique_id = f"{self._source_id}_timeout"
|
|
self._attr_translation_key = "api_input_timeout"
|
|
|
|
@property
|
|
def device_info(self) -> dict[str, Any]:
|
|
return {"identifiers": {(DOMAIN, self._source_id)}}
|
|
|
|
@property
|
|
def native_value(self) -> float | None:
|
|
source = self._get_source()
|
|
if not source:
|
|
return None
|
|
raw = source.get("timeout")
|
|
if isinstance(raw, dict):
|
|
return raw.get("value")
|
|
if isinstance(raw, (int, float)):
|
|
return float(raw)
|
|
return None
|
|
|
|
@property
|
|
def available(self) -> bool:
|
|
return self._get_source() is not None
|
|
|
|
async def async_set_native_value(self, value: float) -> None:
|
|
await self.coordinator.update_source(self._source_id, timeout=round(value, 2))
|
|
await self.coordinator.async_request_refresh()
|
|
|
|
def _get_source(self) -> dict[str, Any] | None:
|
|
if not self.coordinator.data:
|
|
return None
|
|
for source in self.coordinator.data.get("css_sources") or []:
|
|
if source.get("id") == self._source_id:
|
|
return source
|
|
return None
|