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.
@@ -46,7 +46,7 @@ Ohne mDNS geht es auch von Hand über Adresse und Token.
|
||||
|
||||
| Entität | |
|
||||
|---|---|
|
||||
| `light` | Hauptschalter und Helligkeit des ganzen Strips |
|
||||
| `light` | Hauptschalter, Helligkeit, Farbe und Farbtemperatur des ganzen Strips |
|
||||
| `select` | aufgeschaltete Szene (acht Plätze in der Firmware) |
|
||||
| `button` | Neustart |
|
||||
| `sensor` | IP-Adresse, WLAN-Signal, Laufzeit, freier Speicher, Leistungsbegrenzung |
|
||||
@@ -63,8 +63,19 @@ Signal, Laufzeit und freier Speicher sind aus; die braucht man erst im
|
||||
Fehlerfall.
|
||||
|
||||
**Eine Lichtentität je Gerät, nicht je Segment.** `device/state` ist der
|
||||
Hauptschalter und trägt keine Farbe — bei mehreren Segmenten gäbe es keine
|
||||
eine Farbe, die stimmen würde. Die Segmente stellt man über Szenen.
|
||||
Hauptschalter und trägt keine Farbe — die steht je **Segment**. Die
|
||||
Lichtentität schreibt eine gewählte Farbe deshalb in **alle** Segmente und
|
||||
lässt deren Grenzen, Namen und Helligkeit stehen; die mühsam von Hand gemachte
|
||||
Einteilung bleibt also erhalten.
|
||||
|
||||
Zeigen kann sie nur **eine** Farbe. Sind sich die Segmente nicht einig, meldet
|
||||
sie gar keine, statt sich eine auszusuchen — jede Angabe wäre dann gelogen.
|
||||
Farbtemperatur meldet sie nur, wenn **alle** Segmente auf `white` stehen;
|
||||
sobald eines einen Effekt fährt, ist es RGB. Die Spanne ist 2700–6500 K, das
|
||||
rechnet die Firmware auf die Weißkanäle um (`nl_ct.h`); Strips ohne
|
||||
Weißkanäle bekommen dieselbe Spanne als RGB-Näherung.
|
||||
|
||||
Für einzelne Segmente gibt es noch keine Entitäten.
|
||||
|
||||
## Noch nicht drin
|
||||
|
||||
@@ -138,11 +149,15 @@ eingebunden, der Quellcode bleibt in diesem Repo.
|
||||
|
||||
### Markenbilder
|
||||
|
||||
`scripts/make_brand.py` zeichnet die vier PNGs unter
|
||||
`custom_components/ninstrip/brand/`. Ab Home Assistant 2026.3 liefert HA sie
|
||||
für eine eigene Integration von dort aus; ohne sie bleibt in der
|
||||
Geräteübersicht ein Platzhalter. Das Motiv ist ein Strip mit fünf leuchtenden
|
||||
Pixeln — ninlight hat an derselben Stelle eine Glühbirne.
|
||||
Die vier PNGs unter `custom_components/ninstrip/brand/` sind **dieselben wie
|
||||
in ha-ninlight**: die beiden Integrationen gehören zusammen, und zwei
|
||||
verschiedene Motive in derselben Geräteübersicht stiften nur Verwirrung. Ab
|
||||
Home Assistant 2026.3 liefert HA sie für eine eigene Integration von dort
|
||||
aus; ohne sie bleibt ein Platzhalter.
|
||||
|
||||
`scripts/make_brand.py` zeichnet ein eigenes Motiv (ein Band mit fünf
|
||||
leuchtenden Pixeln) und liegt als Ausgangspunkt bereit, falls doch einmal
|
||||
eines gewünscht ist — in Gebrauch ist es nicht.
|
||||
|
||||
### Tests
|
||||
|
||||
|
||||
|
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)
|
||||
|
||||
@@ -2,9 +2,13 @@
|
||||
|
||||
Ab Home Assistant 2026.3 nimmt HA fuer eine eigene Integration die Bilder aus
|
||||
diesem Ordner - ohne sie bleibt in der Geraeteuebersicht ein Platzhalter.
|
||||
ha-ninlight macht es genauso (dort eine Gluehbirne), hier ist es ein Strip:
|
||||
ein dunkles Band mit fuenf leuchtenden Pixeln in Regenbogenfarben, weil genau
|
||||
das den Unterschied zu ninlight ausmacht - viele Pixel statt einer Lampe.
|
||||
**Nicht mehr in Gebrauch.** Michael wollte am 23.09.2026 dasselbe Bild wie
|
||||
bei ninlight - die beiden Integrationen gehoeren zusammen, und zwei
|
||||
verschiedene Motive in derselben Geraeteuebersicht stiften nur Verwirrung.
|
||||
Die Bilder in brand/ sind deshalb aus ha-ninlight uebernommen.
|
||||
|
||||
Das Skript bleibt als Ausgangspunkt liegen, falls doch einmal ein eigenes
|
||||
Motiv gewuenscht ist: ein dunkles Band mit fuenf leuchtenden Pixeln.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -20,6 +20,7 @@ from tests_unit.samples import (
|
||||
INFO_K1Y,
|
||||
MAC_K1Y,
|
||||
PRESETS_K1Y,
|
||||
SEGMENTS_K1Y,
|
||||
SETTINGS_COMPILED,
|
||||
STATE_K1Y,
|
||||
TOKEN,
|
||||
@@ -57,12 +58,18 @@ def strip_presets() -> dict[str, Any]:
|
||||
return deepcopy(PRESETS_K1Y)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def strip_segments() -> dict[str, Any]:
|
||||
return deepcopy(SEGMENTS_K1Y)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_client(
|
||||
strip_state: dict[str, Any],
|
||||
strip_info: dict[str, Any],
|
||||
strip_settings: dict[str, Any],
|
||||
strip_presets: dict[str, Any],
|
||||
strip_segments: dict[str, Any],
|
||||
) -> Generator[MagicMock]:
|
||||
"""Ein Geraet, dessen Antworten den veraenderbaren strip_*-Fixtures folgen."""
|
||||
client = MagicMock(spec=NinstripClient)
|
||||
@@ -71,6 +78,14 @@ def mock_client(
|
||||
client.get_info = AsyncMock(side_effect=lambda: deepcopy(strip_info))
|
||||
client.get_settings = AsyncMock(side_effect=lambda: deepcopy(strip_settings))
|
||||
client.get_presets = AsyncMock(side_effect=lambda: deepcopy(strip_presets))
|
||||
client.get_segments = AsyncMock(side_effect=lambda: deepcopy(strip_segments))
|
||||
|
||||
async def put_segments(segments: list[dict[str, Any]]) -> None:
|
||||
# Wie das Geraet: es nimmt die Liste als Ganzes an und antwortet nur
|
||||
# mit "ok"; der Aufrufer liest danach zurueck.
|
||||
strip_segments["segments"] = deepcopy(segments)
|
||||
|
||||
client.put_segments = AsyncMock(side_effect=put_segments)
|
||||
|
||||
async def set_state(**fields: Any) -> dict[str, Any]:
|
||||
strip_state.update(fields)
|
||||
|
||||
@@ -35,7 +35,9 @@ async def test_zustand_und_helligkeit_aus_prozent(
|
||||
assert state.state == STATE_ON
|
||||
# Das Geraet rechnet in Prozent: 100 % sind 255.
|
||||
assert state.attributes[ATTR_BRIGHTNESS] == 255
|
||||
assert state.attributes["color_mode"] == ColorMode.BRIGHTNESS
|
||||
# Alle Segmente stehen auf weiss, also meldet die Entitaet Farbtemperatur.
|
||||
assert state.attributes["color_mode"] == ColorMode.COLOR_TEMP
|
||||
assert state.attributes["color_temp_kelvin"] == 2700
|
||||
assert state.attributes["pixel_count"] == 60
|
||||
assert state.attributes["segment_count"] == 1
|
||||
assert state.attributes["preset"] == 2
|
||||
@@ -123,3 +125,178 @@ async def test_die_antwort_des_geraets_zaehlt_nicht_die_anfrage(
|
||||
blocking=True,
|
||||
)
|
||||
assert hass.states.get(light_id(hass)).attributes[ATTR_BRIGHTNESS] == 51
|
||||
|
||||
|
||||
# --- Farbe und Farbtemperatur ------------------------------------------
|
||||
|
||||
|
||||
async def test_farbe_aus_den_segmenten(
|
||||
hass: HomeAssistant,
|
||||
init_integration: MockConfigEntry,
|
||||
strip_segments: dict[str, Any],
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Die Farbe steht nicht in device/state, sondern je Segment."""
|
||||
strip_segments["segments"][0].update({"mode": "solid", "color": {"r": 12, "g": 200, "b": 90}})
|
||||
await abfragen(hass, freezer)
|
||||
state = hass.states.get(light_id(hass))
|
||||
assert state.attributes["color_mode"] == ColorMode.RGB
|
||||
assert state.attributes["rgb_color"] == (12, 200, 90)
|
||||
|
||||
|
||||
async def test_farbe_setzen_schreibt_in_alle_segmente(
|
||||
hass: HomeAssistant, init_integration: MockConfigEntry, mock_client: MagicMock
|
||||
) -> None:
|
||||
await hass.services.async_call(
|
||||
"light",
|
||||
"turn_on",
|
||||
{ATTR_ENTITY_ID: light_id(hass), "rgb_color": (255, 40, 0)},
|
||||
blocking=True,
|
||||
)
|
||||
(geschrieben,) = mock_client.put_segments.await_args.args
|
||||
assert [(s["mode"], s["color"]) for s in geschrieben] == [
|
||||
("solid", {"r": 255, "g": 40, "b": 0})
|
||||
]
|
||||
assert hass.states.get(light_id(hass)).attributes["rgb_color"] == (255, 40, 0)
|
||||
|
||||
|
||||
async def test_farbe_setzen_laesst_die_einteilung_stehen(
|
||||
hass: HomeAssistant,
|
||||
init_integration: MockConfigEntry,
|
||||
mock_client: MagicMock,
|
||||
strip_segments: dict[str, Any],
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Grenzen, Namen und Helligkeit der Segmente bleiben unangetastet.
|
||||
|
||||
Sonst risse eine Farbe ueber die Lichtentitaet die ganze Einteilung des
|
||||
Strips ein - und die ist muehsam von Hand gemacht.
|
||||
"""
|
||||
strip_segments["segments"] = [
|
||||
{
|
||||
"id": 0,
|
||||
"name": "Regal",
|
||||
"start": 0,
|
||||
"end": 19,
|
||||
"mode": "white",
|
||||
"color_temp": 2700,
|
||||
"brightness": 80,
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Kante",
|
||||
"start": 20,
|
||||
"end": 59,
|
||||
"mode": "white",
|
||||
"color_temp": 4000,
|
||||
"brightness": 55,
|
||||
},
|
||||
]
|
||||
await abfragen(hass, freezer)
|
||||
await hass.services.async_call(
|
||||
"light",
|
||||
"turn_on",
|
||||
{ATTR_ENTITY_ID: light_id(hass), "rgb_color": (0, 0, 255)},
|
||||
blocking=True,
|
||||
)
|
||||
(geschrieben,) = mock_client.put_segments.await_args.args
|
||||
assert [(s["name"], s["start"], s["end"], s["brightness"]) for s in geschrieben] == [
|
||||
("Regal", 0, 19, 80),
|
||||
("Kante", 20, 59, 55),
|
||||
]
|
||||
assert all(s["mode"] == "solid" for s in geschrieben)
|
||||
|
||||
|
||||
async def test_uneinige_segmente_zeigen_keine_farbe(
|
||||
hass: HomeAssistant,
|
||||
init_integration: MockConfigEntry,
|
||||
strip_segments: dict[str, Any],
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Zwei Farben, eine Entitaet - dann waere jede Angabe gelogen."""
|
||||
strip_segments["segments"] = [
|
||||
{
|
||||
"id": 0,
|
||||
"start": 0,
|
||||
"end": 29,
|
||||
"mode": "solid",
|
||||
"color": {"r": 255, "g": 0, "b": 0},
|
||||
"brightness": 80,
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"start": 30,
|
||||
"end": 59,
|
||||
"mode": "solid",
|
||||
"color": {"r": 0, "g": 0, "b": 255},
|
||||
"brightness": 80,
|
||||
},
|
||||
]
|
||||
await abfragen(hass, freezer)
|
||||
state = hass.states.get(light_id(hass))
|
||||
assert state.attributes["color_mode"] == ColorMode.RGB
|
||||
assert state.attributes["rgb_color"] is None
|
||||
|
||||
|
||||
async def test_effektsegmente_melden_keine_farbtemperatur(
|
||||
hass: HomeAssistant,
|
||||
init_integration: MockConfigEntry,
|
||||
strip_segments: dict[str, Any],
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
strip_segments["segments"][0].update({"mode": "effect", "effect": "rainbow"})
|
||||
strip_segments["segments"][0].pop("color_temp")
|
||||
await abfragen(hass, freezer)
|
||||
state = hass.states.get(light_id(hass))
|
||||
assert state.attributes["color_mode"] == ColorMode.RGB
|
||||
assert state.attributes["color_temp_kelvin"] is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("gewuenscht", "erwartet"), [(2700, 2700), (4000, 4000), (6500, 6500)])
|
||||
async def test_farbtemperatur_setzen(
|
||||
hass: HomeAssistant,
|
||||
init_integration: MockConfigEntry,
|
||||
mock_client: MagicMock,
|
||||
gewuenscht: int,
|
||||
erwartet: int,
|
||||
) -> None:
|
||||
await hass.services.async_call(
|
||||
"light",
|
||||
"turn_on",
|
||||
{ATTR_ENTITY_ID: light_id(hass), "color_temp_kelvin": gewuenscht},
|
||||
blocking=True,
|
||||
)
|
||||
(geschrieben,) = mock_client.put_segments.await_args.args
|
||||
assert [(s["mode"], s["color_temp"]) for s in geschrieben] == [("white", erwartet)]
|
||||
|
||||
|
||||
async def test_farbe_wird_vor_dem_einschalten_gesetzt(
|
||||
hass: HomeAssistant, init_integration: MockConfigEntry, mock_client: MagicMock
|
||||
) -> None:
|
||||
"""Sonst leuchtet der Strip kurz in der alten Farbe auf."""
|
||||
reihenfolge: list[str] = []
|
||||
mock_client.put_segments.side_effect = lambda segments: reihenfolge.append("segmente")
|
||||
alt = mock_client.set_state.side_effect
|
||||
|
||||
async def merken(**fields: Any) -> dict[str, Any]:
|
||||
reihenfolge.append("state")
|
||||
return await alt(**fields)
|
||||
|
||||
mock_client.set_state.side_effect = merken
|
||||
await hass.services.async_call(
|
||||
"light",
|
||||
"turn_on",
|
||||
{ATTR_ENTITY_ID: light_id(hass), "rgb_color": (10, 20, 30), ATTR_BRIGHTNESS: 128},
|
||||
blocking=True,
|
||||
)
|
||||
assert reihenfolge == ["segmente", "state"]
|
||||
mock_client.set_state.assert_awaited_once_with(on=True, brightness=50)
|
||||
|
||||
|
||||
async def test_ohne_farbe_werden_die_segmente_nicht_angefasst(
|
||||
hass: HomeAssistant, init_integration: MockConfigEntry, mock_client: MagicMock
|
||||
) -> None:
|
||||
await hass.services.async_call(
|
||||
"light", "turn_on", {ATTR_ENTITY_ID: light_id(hass)}, blocking=True
|
||||
)
|
||||
mock_client.put_segments.assert_not_awaited()
|
||||
|
||||
@@ -95,3 +95,19 @@ SETTINGS_STORED: dict[str, Any] = {
|
||||
**SETTINGS_COMPILED,
|
||||
"wifi": {"ssid": "ninIOT", "source": "stored"},
|
||||
}
|
||||
|
||||
# Ein Segment ueber den ganzen Strip, warmweiss - so kommt ein Geraet aus dem
|
||||
# Werkszustand. Farbe und Farbtemperatur stehen nur hier, nicht in state.
|
||||
SEGMENTS_K1Y: dict[str, Any] = {
|
||||
"segments": [
|
||||
{
|
||||
"id": 0,
|
||||
"name": None,
|
||||
"start": 0,
|
||||
"end": 59,
|
||||
"mode": "white",
|
||||
"color_temp": 2700,
|
||||
"brightness": 80,
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||