"""Light platform for LED Screen Controller (api_input CSS sources).""" from __future__ import annotations import logging from typing import Any from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_RGB_COLOR, ColorMode, LightEntity, ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN, DATA_COORDINATOR 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 api_input lights.""" data = hass.data[DOMAIN][entry.entry_id] coordinator: LedGrabCoordinator = data[DATA_COORDINATOR] entities = [] if coordinator.data: for source in coordinator.data.get("css_sources", []): if source.get("source_type") == "api_input": entities.append( ApiInputLight(coordinator, source, entry.entry_id) ) async_add_entities(entities) class ApiInputLight(CoordinatorEntity, LightEntity, RestoreEntity): """Representation of an api_input CSS source as a light entity. The light's RGB color doubles as the source's ``fallback_color`` (the resting color shown when no API input arrives within ``timeout``). Turning the light off only pushes black segments — it does *not* zero ``fallback_color`` — so the chosen default color persists across off/on cycles. The on/off state itself is restored across HA restarts via ``RestoreEntity``. """ _attr_has_entity_name = True _attr_color_mode = ColorMode.RGB _attr_supported_color_modes = {ColorMode.RGB} _attr_translation_key = "api_input_light" _attr_icon = "mdi:led-strip-variant" def __init__( self, coordinator: LedGrabCoordinator, source: dict[str, Any], entry_id: str, ) -> None: """Initialize the light.""" super().__init__(coordinator) self._source_id: str = source["id"] self._source_name: str = source.get("name", self._source_id) self._entry_id = entry_id self._attr_unique_id = f"{self._source_id}_light" # Seed color from the source's current fallback_color; default to # white if it's unset/black so the picker has a sensible starting # value. is_on is provisional here and refined in async_added_to_hass # from RestoreEntity, since fallback_color alone no longer indicates # on/off. fallback = self._get_fallback_color() has_color = fallback != [0, 0, 0] self._rgb_color: tuple[int, int, int] = ( tuple(fallback) if has_color else (255, 255, 255) # type: ignore[arg-type] ) self._is_on: bool = has_color self._brightness: int = 255 async def async_added_to_hass(self) -> None: """Restore on/off state and last color from the previous HA session.""" await super().async_added_to_hass() last_state = await self.async_get_last_state() if last_state is None: return self._is_on = last_state.state == "on" last_rgb = last_state.attributes.get("rgb_color") if ( isinstance(last_rgb, (list, tuple)) and len(last_rgb) == 3 and all(isinstance(c, (int, float)) for c in last_rgb) ): self._rgb_color = tuple(int(c) for c in last_rgb) # type: ignore[assignment] last_brightness = last_state.attributes.get("brightness") if isinstance(last_brightness, (int, float)): self._brightness = int(last_brightness) @property def device_info(self) -> dict[str, Any]: """Return device information — one virtual device per api_input source.""" return { "identifiers": {(DOMAIN, self._source_id)}, "name": self._source_name, "manufacturer": "LedGrab", "model": "API Input CSS Source", } @property def is_on(self) -> bool: """Return true if the light is on.""" return self._is_on @property def rgb_color(self) -> tuple[int, int, int]: """Return the current RGB color.""" return self._rgb_color @property def brightness(self) -> int: """Return the current brightness (0-255).""" return self._brightness async def async_turn_on(self, **kwargs: Any) -> None: """Turn on the light, optionally setting color and brightness.""" if ATTR_RGB_COLOR in kwargs: self._rgb_color = kwargs[ATTR_RGB_COLOR] if ATTR_BRIGHTNESS in kwargs: self._brightness = kwargs[ATTR_BRIGHTNESS] # Scale RGB by brightness scale = self._brightness / 255 r, g, b = self._rgb_color scaled = [round(r * scale), round(g * scale), round(b * scale)] await self.coordinator.push_segments( self._source_id, [{"start": 0, "length": 9999, "mode": "solid", "color": scaled}], ) # Update fallback_color so the color persists beyond the timeout await self.coordinator.update_source( self._source_id, fallback_color=scaled, ) self._is_on = True self.async_write_ha_state() async def async_turn_off(self, **kwargs: Any) -> None: """Turn off the light by pushing black segments. ``fallback_color`` is intentionally left untouched so the chosen default color survives off/on cycles. """ await self.coordinator.push_segments( self._source_id, [{"start": 0, "length": 9999, "mode": "solid", "color": [0, 0, 0]}], ) self._is_on = False self.async_write_ha_state() def _get_fallback_color(self) -> list[int]: """Read fallback_color from the source config in coordinator data.""" if not self.coordinator.data: return [0, 0, 0] for source in self.coordinator.data.get("css_sources", []): if source.get("id") == self._source_id: fallback = source.get("fallback_color") if fallback and len(fallback) >= 3: return list(fallback[:3]) break return [0, 0, 0]