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.
119 lines
3.6 KiB
Python
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
|