feat(light): Effekte zur Auswahl

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>
This commit is contained in:
Michael Sauer
2026-09-23 11:15:36 +02:00
co-authored by Claude Opus 5
parent 9eb7adcdd7
commit ffcbbdfe4a
10 changed files with 235 additions and 7 deletions
+2
View File
@@ -16,6 +16,7 @@ from pytest_homeassistant_custom_component.common import MockConfigEntry
from custom_components.ninstrip.api import NinstripClient
from custom_components.ninstrip.const import DOMAIN
from tests_unit.samples import (
EFFECTS_K1Y,
HOST_K1Y,
INFO_K1Y,
MAC_K1Y,
@@ -79,6 +80,7 @@ def mock_client(
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))
client.get_effects = AsyncMock(side_effect=lambda: deepcopy(EFFECTS_K1Y))
async def put_segments(segments: list[dict[str, Any]]) -> None:
# Wie das Geraet: es nimmt die Liste als Ganzes an und antwortet nur
+136
View File
@@ -0,0 +1,136 @@
"""Effekte: Liste, Anzeige, Setzen - und was passiert, wenn sie fehlen."""
from __future__ import annotations
from typing import Any
from unittest.mock import MagicMock
from freezegun.api import FrozenDateTimeFactory
from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ServiceValidationError
import pytest
from pytest_homeassistant_custom_component.common import MockConfigEntry
from tests.helpers import abfragen, entity_id_for
def light_id(hass: HomeAssistant) -> str:
return entity_id_for(hass, "light", "light")
async def test_die_liste_zeigt_namen_nicht_kennungen(
hass: HomeAssistant, init_integration: MockConfigEntry
) -> None:
state = hass.states.get(light_id(hass))
assert state.attributes["effect_list"] == ["Regenbogen", "Feuer", "Lauflicht"]
async def test_die_liste_wird_nur_einmal_geholt(
hass: HomeAssistant,
init_integration: MockConfigEntry,
mock_client: MagicMock,
freezer: FrozenDateTimeFactory,
) -> None:
"""Sie steht fest in der Firmware - eine sechste Anfrage je Zyklus waere
Verschwendung."""
vorher = mock_client.get_effects.await_count
await abfragen(hass, freezer)
await abfragen(hass, freezer)
assert mock_client.get_effects.await_count == vorher
async def test_nach_einem_firmware_update_wird_sie_neu_geholt(
hass: HomeAssistant,
init_integration: MockConfigEntry,
mock_client: MagicMock,
strip_info: dict[str, Any],
freezer: FrozenDateTimeFactory,
) -> None:
"""Eine neue Firmware kann neue Effekte mitbringen."""
vorher = mock_client.get_effects.await_count
strip_info["fw_version"] = "0.2.0"
await abfragen(hass, freezer)
assert mock_client.get_effects.await_count == vorher + 1
async def test_laufender_effekt_wird_mit_namen_angezeigt(
hass: HomeAssistant,
init_integration: MockConfigEntry,
strip_segments: dict[str, Any],
freezer: FrozenDateTimeFactory,
) -> None:
strip_segments["segments"][0].update({"mode": "effect", "effect": "fire"})
await abfragen(hass, freezer)
assert hass.states.get(light_id(hass)).attributes["effect"] == "Feuer"
async def test_unbekannte_kennung_wird_roh_gezeigt(
hass: HomeAssistant,
init_integration: MockConfigEntry,
strip_segments: dict[str, Any],
freezer: FrozenDateTimeFactory,
) -> None:
"""Neuere Firmware, aeltere Liste - dann lieber die Kennung als nichts."""
strip_segments["segments"][0].update({"mode": "effect", "effect": "meteor"})
await abfragen(hass, freezer)
assert hass.states.get(light_id(hass)).attributes["effect"] == "meteor"
async def test_ohne_effektmodus_kein_effekt(
hass: HomeAssistant, init_integration: MockConfigEntry
) -> None:
assert hass.states.get(light_id(hass)).attributes["effect"] is None
async def test_uneinige_segmente_zeigen_keinen_effekt(
hass: HomeAssistant,
init_integration: MockConfigEntry,
strip_segments: dict[str, Any],
freezer: FrozenDateTimeFactory,
) -> None:
strip_segments["segments"] = [
{"id": 0, "start": 0, "end": 29, "mode": "effect", "effect": "fire", "brightness": 80},
{"id": 1, "start": 30, "end": 59, "mode": "effect", "effect": "chase", "brightness": 80},
]
await abfragen(hass, freezer)
assert hass.states.get(light_id(hass)).attributes["effect"] is None
async def test_effekt_setzen_schreibt_die_kennung(
hass: HomeAssistant, init_integration: MockConfigEntry, mock_client: MagicMock
) -> None:
await hass.services.async_call(
"light",
"turn_on",
{ATTR_ENTITY_ID: light_id(hass), "effect": "Lauflicht"},
blocking=True,
)
(geschrieben,) = mock_client.put_segments.await_args.args
assert [(s["mode"], s["effect"]) for s in geschrieben] == [("effect", "chase")]
assert hass.states.get(light_id(hass)).attributes["effect"] == "Lauflicht"
async def test_unbekannter_effekt_wird_abgelehnt(
hass: HomeAssistant, init_integration: MockConfigEntry, mock_client: MagicMock
) -> None:
with pytest.raises(ServiceValidationError):
await hass.services.async_call(
"light",
"turn_on",
{ATTR_ENTITY_ID: light_id(hass), "effect": "Diskokugel"},
blocking=True,
)
mock_client.put_segments.assert_not_awaited()
async def test_firmware_ohne_effektliste_bietet_keine_an(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_client: MagicMock,
) -> None:
mock_client.get_effects.side_effect = lambda: {"palettes": []}
mock_config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
assert hass.states.get(light_id(hass)).attributes["effect_list"] == []
+7 -3
View File
@@ -138,8 +138,12 @@ async def test_ohne_uebergang_wird_kein_feld_geschickt(
mock_client.set_state.assert_awaited_once_with(on=False)
async def test_die_lichtentitaet_meldet_uebergaenge(
async def test_die_lichtentitaet_meldet_uebergaenge_und_effekte(
hass: HomeAssistant, init_integration: MockConfigEntry
) -> None:
state = hass.states.get(entity_id_for(hass, "light", "light"))
assert state.attributes["supported_features"] == 32 # LightEntityFeature.TRANSITION
from homeassistant.components.light import LightEntityFeature
merkmale = hass.states.get(entity_id_for(hass, "light", "light")).attributes[
"supported_features"
]
assert merkmale == LightEntityFeature.TRANSITION | LightEntityFeature.EFFECT