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.
245 lines
8.4 KiB
Python
245 lines
8.4 KiB
Python
"""Select platform for LED Screen Controller (CSS source & brightness source)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from typing import Any
|
|
|
|
from homeassistant.components.select import SelectEntity
|
|
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__)
|
|
|
|
NONE_OPTION = "\u2014 None \u2014"
|
|
|
|
|
|
async def async_setup_entry(
|
|
hass: HomeAssistant,
|
|
entry: ConfigEntry,
|
|
async_add_entities: AddEntitiesCallback,
|
|
) -> None:
|
|
"""Set up LED Screen Controller select entities."""
|
|
data = hass.data[DOMAIN][entry.entry_id]
|
|
coordinator: LedGrabCoordinator = data[DATA_COORDINATOR]
|
|
|
|
entities: list[SelectEntity] = []
|
|
if coordinator.data:
|
|
for source in coordinator.data.get("css_sources") or []:
|
|
if source.get("source_type") == "api_input":
|
|
entities.append(ApiInputInterpolationSelect(coordinator, source, entry.entry_id))
|
|
|
|
if coordinator.data and "targets" in coordinator.data:
|
|
for target_id, target_data in coordinator.data["targets"].items():
|
|
info = target_data["info"]
|
|
target_type = info.get("target_type", "led")
|
|
|
|
# Both LED and HA Light targets have a CSS source
|
|
entities.append(CSSSourceSelect(coordinator, target_id, entry.entry_id))
|
|
|
|
# Only LED targets have a brightness value source
|
|
if target_type != TARGET_TYPE_HA_LIGHT:
|
|
entities.append(BrightnessSourceSelect(coordinator, target_id, entry.entry_id))
|
|
|
|
async_add_entities(entities)
|
|
|
|
|
|
class CSSSourceSelect(CoordinatorEntity, SelectEntity):
|
|
"""Select entity for choosing a color strip source for a target."""
|
|
|
|
_attr_has_entity_name = True
|
|
_attr_icon = "mdi:palette"
|
|
|
|
def __init__(
|
|
self,
|
|
coordinator: LedGrabCoordinator,
|
|
target_id: str,
|
|
entry_id: str,
|
|
) -> None:
|
|
super().__init__(coordinator)
|
|
self._target_id = target_id
|
|
self._entry_id = entry_id
|
|
self._attr_unique_id = f"{target_id}_css_source"
|
|
self._attr_translation_key = "color_strip_source"
|
|
|
|
@property
|
|
def device_info(self) -> dict[str, Any]:
|
|
return {"identifiers": {(DOMAIN, self._target_id)}}
|
|
|
|
@property
|
|
def options(self) -> list[str]:
|
|
if not self.coordinator.data:
|
|
return []
|
|
sources = self.coordinator.data.get("css_sources") or []
|
|
return [s["name"] for s in sources]
|
|
|
|
@property
|
|
def current_option(self) -> str | None:
|
|
if not self.coordinator.data:
|
|
return None
|
|
target_data = self.coordinator.data.get("targets", {}).get(self._target_id)
|
|
if not target_data:
|
|
return None
|
|
current_id = target_data["info"].get("color_strip_source_id", "")
|
|
sources = self.coordinator.data.get("css_sources") or []
|
|
for s in sources:
|
|
if s["id"] == current_id:
|
|
return s["name"]
|
|
return None
|
|
|
|
@property
|
|
def available(self) -> bool:
|
|
if not self.coordinator.data:
|
|
return False
|
|
return self._target_id in self.coordinator.data.get("targets", {})
|
|
|
|
async def async_select_option(self, option: str) -> None:
|
|
source_id = self._name_to_id_map().get(option)
|
|
if source_id is None:
|
|
_LOGGER.error("CSS source not found: %s", option)
|
|
return
|
|
await self.coordinator.update_target(self._target_id, color_strip_source_id=source_id)
|
|
|
|
def _name_to_id_map(self) -> dict[str, str]:
|
|
sources = (self.coordinator.data or {}).get("css_sources") or []
|
|
return {s["name"]: s["id"] for s in sources}
|
|
|
|
|
|
class BrightnessSourceSelect(CoordinatorEntity, SelectEntity):
|
|
"""Select entity for choosing a brightness value source for an LED target."""
|
|
|
|
_attr_has_entity_name = True
|
|
_attr_icon = "mdi:brightness-auto"
|
|
|
|
def __init__(
|
|
self,
|
|
coordinator: LedGrabCoordinator,
|
|
target_id: str,
|
|
entry_id: str,
|
|
) -> None:
|
|
super().__init__(coordinator)
|
|
self._target_id = target_id
|
|
self._entry_id = entry_id
|
|
self._attr_unique_id = f"{target_id}_brightness_source"
|
|
self._attr_translation_key = "brightness_source"
|
|
|
|
@property
|
|
def device_info(self) -> dict[str, Any]:
|
|
return {"identifiers": {(DOMAIN, self._target_id)}}
|
|
|
|
@property
|
|
def options(self) -> list[str]:
|
|
if not self.coordinator.data:
|
|
return [NONE_OPTION]
|
|
sources = self.coordinator.data.get("value_sources") or []
|
|
return [NONE_OPTION] + [s["name"] for s in sources]
|
|
|
|
@property
|
|
def current_option(self) -> str | None:
|
|
if not self.coordinator.data:
|
|
return None
|
|
target_data = self.coordinator.data.get("targets", {}).get(self._target_id)
|
|
if not target_data:
|
|
return None
|
|
# BindableFloat: brightness is either a plain float or {"value": float, "source_id": str}
|
|
brightness = target_data["info"].get("brightness", "")
|
|
if isinstance(brightness, dict):
|
|
current_id = brightness.get("source_id", "")
|
|
else:
|
|
current_id = target_data["info"].get("brightness_value_source_id", "")
|
|
if not current_id:
|
|
return NONE_OPTION
|
|
sources = self.coordinator.data.get("value_sources") or []
|
|
for s in sources:
|
|
if s["id"] == current_id:
|
|
return s["name"]
|
|
return NONE_OPTION
|
|
|
|
@property
|
|
def available(self) -> bool:
|
|
if not self.coordinator.data:
|
|
return False
|
|
return self._target_id in self.coordinator.data.get("targets", {})
|
|
|
|
async def async_select_option(self, option: str) -> None:
|
|
if option == NONE_OPTION:
|
|
source_id = ""
|
|
else:
|
|
name_map = {
|
|
s["name"]: s["id"] for s in (self.coordinator.data or {}).get("value_sources") or []
|
|
}
|
|
source_id = name_map.get(option)
|
|
if source_id is None:
|
|
_LOGGER.error("Value source not found: %s", option)
|
|
return
|
|
await self.coordinator.update_target(
|
|
self._target_id,
|
|
brightness={"value": 1.0, "source_id": source_id} if source_id else 1.0,
|
|
)
|
|
|
|
|
|
# --- api_input CSS source select entities ---
|
|
|
|
INTERPOLATION_OPTIONS = ["none", "linear", "nearest"]
|
|
|
|
|
|
class ApiInputInterpolationSelect(CoordinatorEntity, SelectEntity):
|
|
"""LED count interpolation mode for an api_input CSS source.
|
|
|
|
Controls how the source resamples pushed colors when the target strip
|
|
has a different LED count than the input. Server accepts
|
|
``none`` | ``linear`` | ``nearest``.
|
|
"""
|
|
|
|
_attr_has_entity_name = True
|
|
_attr_icon = "mdi:vector-polyline"
|
|
_attr_options = INTERPOLATION_OPTIONS
|
|
|
|
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}_interpolation"
|
|
self._attr_translation_key = "api_input_interpolation"
|
|
|
|
@property
|
|
def device_info(self) -> dict[str, Any]:
|
|
return {"identifiers": {(DOMAIN, self._source_id)}}
|
|
|
|
@property
|
|
def current_option(self) -> str | None:
|
|
source = self._get_source()
|
|
if not source:
|
|
return None
|
|
value = source.get("interpolation")
|
|
return value if value in INTERPOLATION_OPTIONS else None
|
|
|
|
@property
|
|
def available(self) -> bool:
|
|
return self._get_source() is not None
|
|
|
|
async def async_select_option(self, option: str) -> None:
|
|
if option not in INTERPOLATION_OPTIONS:
|
|
_LOGGER.error("Invalid interpolation option: %s", option)
|
|
return
|
|
await self.coordinator.update_source(self._source_id, interpolation=option)
|
|
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
|