feat(light): Farbe und Farbtemperatur ueber alle Segmente

In Home Assistant liessen sich nur Szenen und Helligkeit stellen, keine
Farben - device/state traegt naemlich keine. Die steht je Segment, und die
Segmente hat der Koordinator bisher gar nicht gelesen.

Jetzt holt er device/segments mit (fuenfte Anfrage je Zyklus), und die
Lichtentitaet bietet rgb_color und color_temp_kelvin an:

* Setzen schreibt in ALLE Segmente, aber nur die Farbfelder - Grenzen,
  Namen und Helligkeit bleiben stehen. Die Einteilung eines Strips ist von
  Hand gemacht, die reisst eine Farbwahl nicht ein. Geschrieben wird die
  ganze Liste auf einmal, danach zurueckgelesen statt angenommen.
* Angezeigt wird nur, worauf sich alle Segmente einigen. Zwei Farben und
  eine Entitaet - da waere jede Angabe gelogen, also gibt es keine.
* Farbtemperatur nur, wenn alle auf "white" stehen; sonst RGB, sonst
  blendete HA den Farbkreis aus, sobald ein Segment einen Effekt faehrt.
* Die Farbe geht VOR dem Einschalten raus, sonst leuchtet der Strip kurz
  in der alten auf.

Die Spanne 2700-6500 K ist die, die die Firmware auf die Weisskanaele
umrechnet (nl_ct.h); ohne Weisskanaele naehert sie sie in RGB an.

Dazu: das Markenbild ist jetzt dasselbe wie bei ninlight - Michaels
Wunsch, die beiden Integrationen gehoeren zusammen. Das Skript fuer ein
eigenes Motiv bleibt als Ausgangspunkt liegen.
This commit is contained in:
Michael Sauer
2026-09-23 00:20:23 +02:00
parent 2c340ea11f
commit c40c68b537
11 changed files with 361 additions and 22 deletions
+76 -7
View File
@@ -1,22 +1,35 @@
"""Hauptschalter und Helligkeit eines ninstrip.
"""Hauptschalter, Helligkeit und Farbe eines ninstrip.
Bewusst eine Entitaet je Geraet, nicht je Segment: device/state ist der
Hauptschalter und traegt keine Farbe - bei mehreren Segmenten gaebe es keine
eine Farbe, die stimmen wuerde. Die Segmente selbst werden ueber Szenen
gestellt; einzelne Segmententitaeten koennen spaeter dazukommen.
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, ColorMode, LightEntity
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
ATTR_COLOR_TEMP_KELVIN,
ATTR_RGB_COLOR,
ColorMode,
LightEntity,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
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
async def async_setup_entry(
hass: HomeAssistant,
@@ -30,12 +43,31 @@ class NinstripLight(NinstripEntity, LightEntity):
"""Der Strip als Ganzes."""
_attr_name = None
_attr_color_mode = ColorMode.BRIGHTNESS
_attr_supported_color_modes = {ColorMode.BRIGHTNESS}
_attr_supported_color_modes = {ColorMode.RGB, ColorMode.COLOR_TEMP}
_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"))
@@ -48,6 +80,30 @@ class NinstripLight(NinstripEntity, LightEntity):
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
@property
def extra_state_attributes(self) -> dict[str, Any]:
state = self.coordinator.data.state
@@ -58,7 +114,20 @@ class NinstripLight(NinstripEntity, LightEntity):
"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_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)