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.
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 3.2 KiB |
@@ -35,6 +35,10 @@ class NinstripData:
|
||||
state: dict[str, Any]
|
||||
info: dict[str, Any]
|
||||
presets: dict[str, Any]
|
||||
# Farbe und Farbtemperatur stehen NICHT in device/state, sondern je
|
||||
# Segment. Ohne diese Liste koennte die Lichtentitaet keine Farbe zeigen
|
||||
# und keine setzen.
|
||||
segments: dict[str, Any]
|
||||
# Nur, was das Geraet unter system/settings zeigt. Aeltere Firmware traegt
|
||||
# dort keinen sacn-Block - die Entitaeten melden sich dann nicht
|
||||
# verfuegbar, statt zu verschwinden.
|
||||
@@ -85,6 +89,7 @@ class NinstripCoordinator(DataUpdateCoordinator[NinstripData]):
|
||||
state = await self.client.get_state()
|
||||
info = await self.client.get_info()
|
||||
presets = await self.client.get_presets()
|
||||
segments = await self.client.get_segments()
|
||||
# Die sACN-Einstellungen liegen hinter system/settings, nicht in
|
||||
# device/state - eine vierte Anfrage je Zyklus. Wie ha-ninlight.
|
||||
settings = await self.client.get_settings()
|
||||
@@ -103,7 +108,7 @@ class NinstripCoordinator(DataUpdateCoordinator[NinstripData]):
|
||||
return self.data
|
||||
raise UpdateFailed(f"{self.client.host}: {err}") from err
|
||||
self._consecutive_poll_failures = 0
|
||||
return NinstripData(state, info, presets, settings)
|
||||
return NinstripData(state, info, presets, segments, settings)
|
||||
|
||||
async def async_call[T](self, action: Callable[[NinstripClient], Awaitable[T]]) -> T:
|
||||
"""Fuehrt eine Anfrage unter der Sperre aus und uebersetzt Fehler fuer die Oberflaeche."""
|
||||
@@ -135,7 +140,9 @@ class NinstripCoordinator(DataUpdateCoordinator[NinstripData]):
|
||||
state = await self.async_call(lambda client: client.set_state(**fields))
|
||||
self._consecutive_poll_failures = 0
|
||||
self.async_set_updated_data(
|
||||
NinstripData(state, self.data.info, self.data.presets, self.data.settings)
|
||||
NinstripData(
|
||||
state, self.data.info, self.data.presets, self.data.segments, self.data.settings
|
||||
)
|
||||
)
|
||||
|
||||
async def async_set_sacn(self, **fields: Any) -> None:
|
||||
@@ -154,7 +161,43 @@ class NinstripCoordinator(DataUpdateCoordinator[NinstripData]):
|
||||
settings = await self.async_call(schreiben_und_lesen)
|
||||
self._consecutive_poll_failures = 0
|
||||
self.async_set_updated_data(
|
||||
NinstripData(self.data.state, self.data.info, self.data.presets, settings)
|
||||
NinstripData(
|
||||
self.data.state,
|
||||
self.data.info,
|
||||
self.data.presets,
|
||||
self.data.segments,
|
||||
settings,
|
||||
)
|
||||
)
|
||||
|
||||
async def async_set_all_segments(self, **fields: Any) -> None:
|
||||
"""Schreibt dieselben Felder in JEDES Segment und liest zurueck.
|
||||
|
||||
Die Grenzen und Namen der Segmente bleiben dabei stehen - gesetzt wird
|
||||
nur, was uebergeben wird. Eine Farbe ueber die Lichtentitaet soll die
|
||||
muehsam von Hand gemachte Einteilung des Strips nicht einreissen.
|
||||
|
||||
Geschrieben wird die ganze Liste auf einmal: das Geraet nimmt sie als
|
||||
Ganzes an oder gar nicht, und es ist eine Anfrage statt einer je
|
||||
Segment. Danach wird zurueckgelesen statt angenommen, wie bei sACN.
|
||||
"""
|
||||
|
||||
async def schreiben_und_lesen(client: NinstripClient) -> dict[str, Any]:
|
||||
alt = self.data.segments.get("segments") or []
|
||||
neu = [{**seg, **fields} for seg in alt]
|
||||
await client.put_segments(neu)
|
||||
return await client.get_segments()
|
||||
|
||||
segments = await self.async_call(schreiben_und_lesen)
|
||||
self._consecutive_poll_failures = 0
|
||||
self.async_set_updated_data(
|
||||
NinstripData(
|
||||
self.data.state,
|
||||
self.data.info,
|
||||
self.data.presets,
|
||||
segments,
|
||||
self.data.settings,
|
||||
)
|
||||
)
|
||||
|
||||
async def async_after_change(self) -> None:
|
||||
|
||||
@@ -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)
|
||||
|
||||