Die neun Effekte der Firmware waren in Home Assistant nicht erreichbar -
das Auswahlfeld, das es schon gab, sind die Szenen. Jetzt bietet die
Lichtentitaet sie an:
* Gewaehlt wird ueber den NAMEN aus der Firmware ("Regenbogen"),
geschickt wird die Kennung ("rainbow"). Eine Liste englischer
Kennungen waere in einer deutschen Oberflaeche unnoetig sperrig.
* Ein Effekt laeuft wie eine Farbe in ALLEN Segmenten; angezeigt wird er
nur, wenn alle denselben fahren.
* Eine Kennung, die nicht in der Liste steht (neuere Firmware, aeltere
Liste), wird roh angezeigt statt verschluckt.
* Ein unbekannter Name wird abgelehnt, statt stillschweigend nichts zu
tun.
Die Liste steht fest in der Firmware und wird deshalb einmal geholt,
nicht je Abfragezyklus - erneut erst, wenn sich die Version aendert. Eine
sechste Anfrage je Zyklus fuer etwas, das sich nie aendert, waere
Verschwendung.
Effekt-Parameter (Geschwindigkeit, Intensitaet, Palette) fehlen noch;
dafuer braucht es Entitaeten je Segment.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
196 lines
7.3 KiB
Python
196 lines
7.3 KiB
Python
"""Hauptschalter, Helligkeit und Farbe eines ninstrip.
|
|
|
|
Bewusst eine Entitaet je Geraet, nicht je Segment: device/state ist der
|
|
Hauptschalter. Farbe und Farbtemperatur stehen dagegen NICHT dort, sondern je
|
|
Segment - die Entitaet schreibt sie deshalb in alle Segmente zugleich und
|
|
laesst deren Grenzen und Namen stehen. Einzelne Segmententitaeten koennen
|
|
spaeter dazukommen.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
from homeassistant.components.light import (
|
|
ATTR_BRIGHTNESS,
|
|
ATTR_COLOR_TEMP_KELVIN,
|
|
ATTR_EFFECT,
|
|
ATTR_RGB_COLOR,
|
|
ATTR_TRANSITION,
|
|
ColorMode,
|
|
LightEntity,
|
|
LightEntityFeature,
|
|
)
|
|
from homeassistant.core import HomeAssistant
|
|
from homeassistant.exceptions import ServiceValidationError
|
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
|
|
|
from .const import DOMAIN, MAX_TRANSITION_MS
|
|
from .coordinator import NinstripConfigEntry, NinstripCoordinator
|
|
from .entity import NinstripEntity
|
|
|
|
# Die Firmware rechnet Farbtemperaturen ausserhalb dieser Spanne nicht auf die
|
|
# Weisskanaele um (nl_ct.h). Strips ohne Weisskanaele bekommen dieselbe Spanne
|
|
# als RGB-Naeherung, damit "warmweiss" auf jedem Chip leuchtet.
|
|
MIN_KELVIN = 2700
|
|
MAX_KELVIN = 6500
|
|
|
|
|
|
def uebergang_ms(sekunden: float) -> int:
|
|
"""Home Assistant rechnet in Sekunden, das Geraet in Millisekunden.
|
|
|
|
Gekappt auf das, was die Firmware annimmt - ein laengerer Wert wuerde
|
|
sonst die ganze Anfrage mit 400 abweisen, statt nur langsamer zu blenden.
|
|
"""
|
|
return max(0, min(MAX_TRANSITION_MS, round(sekunden * 1000)))
|
|
|
|
|
|
async def async_setup_entry(
|
|
hass: HomeAssistant,
|
|
entry: NinstripConfigEntry,
|
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
|
) -> None:
|
|
async_add_entities([NinstripLight(entry.runtime_data)])
|
|
|
|
|
|
class NinstripLight(NinstripEntity, LightEntity):
|
|
"""Der Strip als Ganzes."""
|
|
|
|
_attr_name = None
|
|
_attr_supported_color_modes = {ColorMode.RGB, ColorMode.COLOR_TEMP}
|
|
_attr_supported_features = LightEntityFeature.TRANSITION | LightEntityFeature.EFFECT
|
|
_attr_min_color_temp_kelvin = MIN_KELVIN
|
|
_attr_max_color_temp_kelvin = MAX_KELVIN
|
|
|
|
def __init__(self, coordinator: NinstripCoordinator) -> None:
|
|
super().__init__(coordinator, "light")
|
|
|
|
# --- Zustand -------------------------------------------------------
|
|
|
|
def _segments(self) -> list[dict[str, Any]]:
|
|
return self.coordinator.data.segments.get("segments") or []
|
|
|
|
def _einig(self, feld: str) -> Any:
|
|
"""Der Wert, wenn alle Segmente sich einig sind - sonst None.
|
|
|
|
Ein Strip kann segmentweise verschiedene Farben zeigen. Home Assistant
|
|
kennt nur eine je Entitaet; in dem Fall waere jede Angabe gelogen, also
|
|
gibt es keine.
|
|
"""
|
|
segmente = self._segments()
|
|
if not segmente:
|
|
return None
|
|
werte = [seg.get(feld) for seg in segmente]
|
|
return werte[0] if all(w == werte[0] and w is not None for w in werte) else None
|
|
|
|
@property
|
|
def is_on(self) -> bool:
|
|
return bool(self.coordinator.data.state.get("on"))
|
|
|
|
@property
|
|
def brightness(self) -> int | None:
|
|
"""Das Geraet rechnet in Prozent, Home Assistant in 0..255."""
|
|
value = self.coordinator.data.state.get("brightness")
|
|
if value is None:
|
|
return None
|
|
return round(max(0, min(100, int(value))) * 255 / 100)
|
|
|
|
@property
|
|
def color_mode(self) -> ColorMode:
|
|
"""Weiss nur, wenn sich alle Segmente auf 'white' einigen.
|
|
|
|
Sonst RGB: ein Strip mit Effekten oder gemischten Segmenten hat keine
|
|
eine Farbtemperatur, und HA wuerde den Farbkreis sonst ausblenden.
|
|
"""
|
|
return ColorMode.COLOR_TEMP if self._einig("mode") == "white" else ColorMode.RGB
|
|
|
|
@property
|
|
def rgb_color(self) -> tuple[int, int, int] | None:
|
|
farbe = self._einig("color")
|
|
if not isinstance(farbe, dict):
|
|
return None
|
|
try:
|
|
return (int(farbe["r"]), int(farbe["g"]), int(farbe["b"]))
|
|
except KeyError, TypeError, ValueError:
|
|
return None
|
|
|
|
@property
|
|
def color_temp_kelvin(self) -> int | None:
|
|
wert = self._einig("color_temp")
|
|
return int(wert) if isinstance(wert, (int, float)) else None
|
|
|
|
# --- Effekte -------------------------------------------------------
|
|
|
|
@property
|
|
def effect_list(self) -> list[str]:
|
|
"""Die Namen aus der Firmware, nicht die Kennungen.
|
|
|
|
Angezeigt wird "Regenbogen", geschickt wird "rainbow" - eine Liste
|
|
voller englischer Kennungen waere in einer deutschen Oberflaeche
|
|
unnoetig sperrig.
|
|
"""
|
|
return [str(e["name"]) for e in self.coordinator.effects if e.get("name") and e.get("id")]
|
|
|
|
def _effekt_id(self, name: str) -> str | None:
|
|
for e in self.coordinator.effects:
|
|
if e.get("name") == name:
|
|
return str(e["id"])
|
|
return None
|
|
|
|
@property
|
|
def effect(self) -> str | None:
|
|
"""Nur, wenn alle Segmente denselben Effekt fahren."""
|
|
if self._einig("mode") != "effect":
|
|
return None
|
|
kennung = self._einig("effect")
|
|
for e in self.coordinator.effects:
|
|
if e.get("id") == kennung:
|
|
return str(e.get("name") or kennung)
|
|
return str(kennung) if kennung else None
|
|
|
|
@property
|
|
def extra_state_attributes(self) -> dict[str, Any]:
|
|
state = self.coordinator.data.state
|
|
return {
|
|
"segment_count": state.get("segment_count"),
|
|
"pixel_count": state.get("pixel_count"),
|
|
"preset": state.get("preset"),
|
|
"preset_modified": state.get("preset_modified"),
|
|
}
|
|
|
|
# --- Schalten ------------------------------------------------------
|
|
|
|
async def async_turn_on(self, **kwargs: Any) -> None:
|
|
# Erst die Farbe in die Segmente, dann der Hauptschalter: so leuchtet
|
|
# nichts kurz in der alten Farbe auf.
|
|
if ATTR_EFFECT in kwargs:
|
|
kennung = self._effekt_id(kwargs[ATTR_EFFECT])
|
|
if kennung is None:
|
|
raise ServiceValidationError(
|
|
translation_domain=DOMAIN,
|
|
translation_key="unknown_effect",
|
|
translation_placeholders={"effect": str(kwargs[ATTR_EFFECT])},
|
|
)
|
|
await self.coordinator.async_set_all_segments(mode="effect", effect=kennung)
|
|
elif ATTR_RGB_COLOR in kwargs:
|
|
r, g, b = kwargs[ATTR_RGB_COLOR]
|
|
await self.coordinator.async_set_all_segments(
|
|
mode="solid", color={"r": int(r), "g": int(g), "b": int(b)}
|
|
)
|
|
elif ATTR_COLOR_TEMP_KELVIN in kwargs:
|
|
kelvin = max(MIN_KELVIN, min(MAX_KELVIN, int(kwargs[ATTR_COLOR_TEMP_KELVIN])))
|
|
await self.coordinator.async_set_all_segments(mode="white", color_temp=kelvin)
|
|
|
|
fields: dict[str, Any] = {"on": True}
|
|
if ATTR_BRIGHTNESS in kwargs:
|
|
fields["brightness"] = round(int(kwargs[ATTR_BRIGHTNESS]) * 100 / 255)
|
|
if ATTR_TRANSITION in kwargs:
|
|
fields["transition"] = uebergang_ms(kwargs[ATTR_TRANSITION])
|
|
await self.coordinator.async_set_state(**fields)
|
|
|
|
async def async_turn_off(self, **kwargs: Any) -> None:
|
|
fields: dict[str, Any] = {"on": False}
|
|
if ATTR_TRANSITION in kwargs:
|
|
fields["transition"] = uebergang_ms(kwargs[ATTR_TRANSITION])
|
|
await self.coordinator.async_set_state(**fields)
|