Files
ha-ninlight/tests/conftest.py
T
Michael Sauer ac60428fdb feat: sACN-Einstellungen und -Status als Entitaeten
Die Firmware kann seit dieser Woche sACN/E1.31. Einstellen liess sich das nur
per REST - jetzt steht es in Home Assistant: Schalter fuer den Empfang, Zahlen
fuer Universe und Startslot, Auswahlen fuer das Verhalten nach dem Timeout und
die Slotbelegung, dazu zwei Diagnose-Binaersensoren.

Die sACN-Einstellungen liegen hinter system/settings, nicht in device/state -
der Koordinator liest sie deshalb jetzt mit, eine dritte Anfrage je Zyklus. Der
Schreibweg liest die Einstellungen danach frisch zurueck statt den gesetzten
Wert anzunehmen: die Lampe validiert selbst (die Startslot-Grenze haengt an der
Slotbelegung), und eine Entitaet, die das Gewuenschte statt des Gespeicherten
zeigt, luegt in genau dem Moment, in dem es darauf ankommt.

Aeltere Firmware antwortet ohne den sacn-Block. Die Entitaeten entstehen
trotzdem und melden sich als nicht verfuegbar - so werden sie nach einem Update
der Lampe beim naechsten Poll von selbst lebendig, ohne Neuladen.

Die Obergrenze des Startslots kommt aus dem "slots"-Feld der Lampe, nicht aus
einer festen 512: sie haengt an Board UND Slotbelegung, und eine feste Grenze
wuerde Werte anbieten, die die Lampe dann ablehnt.

Der Multicast-Sensor ist ab Werk deaktiviert wie die uebrigen Diagnosewerte. Er
meldet im Normalbetrieb schlicht "beigetreten" - aber eine falsche
Gruppenadresse hat zwei Abende gekostet, und genau dort waere sie aufgefallen.

test_update.py misst seine Zusage jetzt um den Rescan herum statt nach dem
Install: dort pollt der Koordinator ohnehin wieder und liest seitdem auch die
Einstellungen mit.
2026-09-21 23:35:54 +02:00

119 lines
3.6 KiB
Python

"""Fixtures for tests that run against a Home Assistant instance."""
from __future__ import annotations
from collections.abc import Generator
from copy import deepcopy
from pathlib import Path
from typing import Any
from unittest.mock import AsyncMock, MagicMock, patch
from homeassistant.const import CONF_HOST, CONF_TOKEN
from homeassistant.core import HomeAssistant
import pytest
from pytest_homeassistant_custom_component.common import MockConfigEntry
from custom_components.ninlight.api import NinlightClient
from custom_components.ninlight.const import DOMAIN
from tests_unit.samples import (
HOST_RGB,
INFO_RGB,
MAC_RGB,
SETTINGS_COMPILED,
STATE_RGB,
TOKEN,
)
CLIENT_PATCH_TARGETS = [
"custom_components.ninlight.NinlightClient",
"custom_components.ninlight.config_flow.NinlightClient",
]
@pytest.fixture(autouse=True)
def auto_enable_custom_integrations(enable_custom_integrations: None) -> Generator[None]:
"""Load custom_components/ in every Home Assistant test."""
yield
@pytest.fixture
def lamp_state() -> dict[str, Any]:
return deepcopy(STATE_RGB)
@pytest.fixture
def lamp_info() -> dict[str, Any]:
return deepcopy(INFO_RGB)
@pytest.fixture
def lamp_settings() -> dict[str, Any]:
return deepcopy(SETTINGS_COMPILED)
@pytest.fixture
def mock_client(
lamp_state: dict[str, Any], lamp_info: dict[str, Any], lamp_settings: dict[str, Any]
) -> Generator[MagicMock]:
"""A lamp whose answers follow the mutable lamp_* fixtures."""
client = MagicMock(spec=NinlightClient)
client.host = HOST_RGB
client.get_state = AsyncMock(side_effect=lambda: deepcopy(lamp_state))
client.get_info = AsyncMock(side_effect=lambda: deepcopy(lamp_info))
client.get_settings = AsyncMock(side_effect=lambda: deepcopy(lamp_settings))
async def set_state(**fields: Any) -> dict[str, Any]:
lamp_state.update(fields)
return deepcopy(lamp_state)
client.set_state = AsyncMock(side_effect=set_state)
async def set_sacn(**fields: Any) -> None:
# Like the lamp: it stores what it accepts, and the caller reads the block back.
lamp_settings.setdefault("sacn", {}).update(fields)
client.set_sacn = AsyncMock(side_effect=set_sacn)
client.reboot = AsyncMock(return_value=None)
client.fw_update = AsyncMock(return_value=None)
patches = [patch(target, return_value=client) for target in CLIENT_PATCH_TARGETS]
for p in patches:
p.start()
yield client
for p in patches:
p.stop()
@pytest.fixture
def mock_config_entry() -> MockConfigEntry:
return MockConfigEntry(
domain=DOMAIN,
title="Klyqa GU10 RGB CW/WW 06b8",
unique_id=MAC_RGB,
data={CONF_HOST: HOST_RGB, CONF_TOKEN: TOKEN},
)
@pytest.fixture
async def init_integration(
hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_client: MagicMock
) -> MockConfigEntry:
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()
return mock_config_entry
@pytest.fixture
def mock_setup_entry() -> Generator[AsyncMock]:
"""Flow tests must not set the entry up for real."""
with patch("custom_components.ninlight.async_setup_entry", return_value=True) as mock:
yield mock
@pytest.fixture(autouse=True)
def firmware_dir(tmp_path: Path) -> Generator[Path]:
"""Every Home Assistant test gets its own, initially empty firmware folder."""
directory = tmp_path / "ninlight_firmware"
with patch("custom_components.ninlight.shared.firmware_directory", return_value=directory):
yield directory