Files
ha-ninstrip/tests/test_zustandsfelder.py
T
Michael Sauer 9eb7adcdd7 feat: nachgezogen, was ha-ninlight laengst hat
Michaels Befund: die Integration ist nicht dasselbe wie die von ninlight.
Verglichen und die Luecken geschlossen - alle Felder gibt es in der
Firmware schon, sie waren nur nicht bedienbar:

* number: fade_in, fade_out, transition_default in Millisekunden, mit den
  Grenzen der Firmware (10 s, 10 s, 60 s).
* select: Einschaltverhalten (last, on, off).
* switch: bei Aenderung einschalten.
* light: Uebergangszeit beim Ein- und Ausschalten. HA rechnet in Sekunden,
  das Geraet in Millisekunden; laengeres als 60 s wird gekappt, sonst
  wiese das Geraet die ganze Anfrage mit 400 ab. Ohne Angabe wird kein
  Feld geschickt, sonst ueberschriebe jeder Schaltbefehl die Vorgabe.
* sensor: letzter Update-Fehler und Startzeit (Zeitstempel statt einer
  Laufzeit, die jede Abfrage weiterlaeuft), beide ab Werk aus.
* diagnostics.py: Diagnosedaten zum Herunterladen, ohne Token, Adressen,
  MAC und SSID.
* config_flow: Neu-Konfigurieren von Adresse und Token, mit Gegenprobe der
  MAC - ein Tippfehler in der Adresse soll nicht still den falschen
  Eintrag umbiegen.
* Hinweis in den Reparaturen, wenn ein Geraet noch auf dem oeffentlichen
  Werks-Token steht.

Nicht uebernommen: die Auswahl sacn_layout. ninstrip ist immer rgb, ein
Strip hat keinen Masterkanal - eine Auswahl mit einer Option waere nur
Zierde.

241 Tests, 97 %. Die Kappung der Uebergangszeit per Mutation gegengeprueft.
2026-09-23 00:35:12 +02:00

146 lines
5.6 KiB
Python

"""Die Felder aus device/state, die ninlight schon lange hat: Zeiten,
Einschaltverhalten, Aufwecken - und die Uebergangszeit beim Schalten."""
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, STATE_OFF, STATE_ON
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
@pytest.mark.parametrize(
("key", "wert", "grenze"),
[("fade_in", 500, 10_000), ("fade_out", 500, 10_000), ("transition_default", 300, 60_000)],
)
async def test_zeiten_zeigen_und_setzen(
hass: HomeAssistant,
init_integration: MockConfigEntry,
mock_client: MagicMock,
key: str,
wert: int,
grenze: int,
) -> None:
entity_id = entity_id_for(hass, "number", key)
state = hass.states.get(entity_id)
assert state.state == f"{float(wert)}"
# Die Obergrenzen sind die der Firmware (nl_api_device.c); alles darueber
# wiese sie mit 400 ab.
assert state.attributes["max"] == grenze
assert state.attributes["unit_of_measurement"] == "ms"
await hass.services.async_call(
"number", "set_value", {ATTR_ENTITY_ID: entity_id, "value": 1200}, blocking=True
)
mock_client.set_state.assert_awaited_once_with(**{key: 1200})
@pytest.mark.parametrize("key", ["fade_in", "fade_out", "transition_default"])
async def test_zeiten_ueber_der_grenze_werden_abgelehnt(
hass: HomeAssistant, init_integration: MockConfigEntry, mock_client: MagicMock, key: str
) -> None:
with pytest.raises(ServiceValidationError):
await hass.services.async_call(
"number",
"set_value",
{ATTR_ENTITY_ID: entity_id_for(hass, "number", key), "value": 60_001},
blocking=True,
)
mock_client.set_state.assert_not_awaited()
async def test_einschaltverhalten(
hass: HomeAssistant, init_integration: MockConfigEntry, mock_client: MagicMock
) -> None:
entity_id = entity_id_for(hass, "select", "power_on_behaviour")
state = hass.states.get(entity_id)
assert state.state == "last"
assert state.attributes["options"] == ["last", "on", "off"]
await hass.services.async_call(
"select", "select_option", {ATTR_ENTITY_ID: entity_id, "option": "off"}, blocking=True
)
mock_client.set_state.assert_awaited_once_with(power_on_behaviour="off")
assert hass.states.get(entity_id).state == "off"
async def test_unbekanntes_einschaltverhalten_zeigt_nichts(
hass: HomeAssistant,
init_integration: MockConfigEntry,
strip_state: dict[str, Any],
freezer: FrozenDateTimeFactory,
) -> None:
# Gemeint ist das Verhalten, nicht die Zeile Code: Home Assistant selbst
# zeigt "unknown", sobald der Wert nicht in den Optionen steht. Die Pruefung
# in der Entitaet ist derselbe Riemen doppelt - wie in ha-ninlight.
strip_state["power_on_behaviour"] = "irgendwas"
await abfragen(hass, freezer)
assert hass.states.get(entity_id_for(hass, "select", "power_on_behaviour")).state == "unknown"
async def test_aufwecken_bei_aenderung(
hass: HomeAssistant, init_integration: MockConfigEntry, mock_client: MagicMock
) -> None:
entity_id = entity_id_for(hass, "switch", "wake_on_change")
assert hass.states.get(entity_id).state == STATE_OFF
await hass.services.async_call("switch", "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True)
mock_client.set_state.assert_awaited_once_with(wake_on_change=True)
assert hass.states.get(entity_id).state == STATE_ON
await hass.services.async_call("switch", "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True)
mock_client.set_state.assert_awaited_with(wake_on_change=False)
@pytest.mark.parametrize(
("sekunden", "millisekunden"), [(0, 0), (2.5, 2500), (0.04, 40), (120, 60_000)]
)
async def test_uebergangszeit_beim_einschalten(
hass: HomeAssistant,
init_integration: MockConfigEntry,
mock_client: MagicMock,
sekunden: float,
millisekunden: int,
) -> None:
"""HA rechnet in Sekunden, das Geraet in Millisekunden - und laengeres als
60 s weist es mit 400 ab, deshalb wird gekappt statt durchgereicht."""
await hass.services.async_call(
"light",
"turn_on",
{ATTR_ENTITY_ID: entity_id_for(hass, "light", "light"), "transition": sekunden},
blocking=True,
)
mock_client.set_state.assert_awaited_once_with(on=True, transition=millisekunden)
async def test_uebergangszeit_beim_ausschalten(
hass: HomeAssistant, init_integration: MockConfigEntry, mock_client: MagicMock
) -> None:
await hass.services.async_call(
"light",
"turn_off",
{ATTR_ENTITY_ID: entity_id_for(hass, "light", "light"), "transition": 1.5},
blocking=True,
)
mock_client.set_state.assert_awaited_once_with(on=False, transition=1500)
async def test_ohne_uebergang_wird_kein_feld_geschickt(
hass: HomeAssistant, init_integration: MockConfigEntry, mock_client: MagicMock
) -> None:
"""Sonst ueberschriebe jeder Schaltbefehl die Vorgabe des Geraets."""
await hass.services.async_call(
"light", "turn_off", {ATTR_ENTITY_ID: entity_id_for(hass, "light", "light")}, blocking=True
)
mock_client.set_state.assert_awaited_once_with(on=False)
async def test_die_lichtentitaet_meldet_uebergaenge(
hass: HomeAssistant, init_integration: MockConfigEntry
) -> None:
state = hass.states.get(entity_id_for(hass, "light", "light"))
assert state.attributes["supported_features"] == 32 # LightEntityFeature.TRANSITION