8a17bb5caa
Lint & Test / test (push) Successful in 1m20s
Introduce BindableFloat abstraction that allows any numeric property to be
either a static value or dynamically driven by a ValueSource. Backward-compatible
serialization: plain float when unbound, {value, source_id} dict when bound.
Backend:
- storage/bindable.py — BindableFloat dataclass + bfloat() helper
- 25+ scalar properties converted across all entity types
- Runtime VS acquisition in ColorStripStreamManager for CSS bindings
- All stream hot loops use self.resolve() for live values
- KeyColorsColorStripStream now inherits ColorStripStream
Frontend:
- BindableScalarWidget (slider + VS picker toggle) for all editors
- TypeScript BindableFloat type + helpers
- Graph editor edges for all bindable properties
- Audio source channel IconSelect grid
Fixes: daylight longitude, candlelight wind_strength/candle_type from_dict
179 lines
6.2 KiB
Python
179 lines
6.2 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 WLEDScreenControllerCoordinator
|
|
|
|
_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: WLEDScreenControllerCoordinator = data[DATA_COORDINATOR]
|
|
|
|
entities: list[SelectEntity] = []
|
|
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: WLEDScreenControllerCoordinator,
|
|
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: WLEDScreenControllerCoordinator,
|
|
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,
|
|
)
|