Files
ha-ninlight/custom_components/ninlight/__init__.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

100 lines
3.3 KiB
Python

"""The ninlight integration."""
from __future__ import annotations
from homeassistant.const import CONF_HOST, CONF_TOKEN, Platform
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import (
config_validation as cv,
)
from homeassistant.helpers import (
device_registry as dr,
)
from homeassistant.helpers import (
issue_registry as ir,
)
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.device_registry import format_mac
from homeassistant.helpers.typing import ConfigType
from .api import NinlightClient
from .const import DEFAULT_TOKEN, DOMAIN
from .coordinator import NinlightConfigEntry, NinlightCoordinator
from .shared import async_get_shared, async_start_catalog, async_stop_catalog
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
PLATFORMS: list[Platform] = [
Platform.BINARY_SENSOR,
Platform.BUTTON,
Platform.LIGHT,
Platform.NUMBER,
Platform.SELECT,
Platform.SENSOR,
Platform.SWITCH,
Platform.UPDATE,
]
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
async_get_shared(hass)
return True
async def async_setup_entry(hass: HomeAssistant, entry: NinlightConfigEntry) -> bool:
client = NinlightClient(
async_get_clientsession(hass), entry.data[CONF_HOST], entry.data[CONF_TOKEN]
)
coordinator = NinlightCoordinator(hass, entry, client)
await coordinator.async_config_entry_first_refresh()
entry.runtime_data = coordinator
_async_update_default_token_issue(hass, entry)
@callback
def _sync_firmware_version() -> None:
info = coordinator.data.info
registry = dr.async_get(hass)
device = registry.async_get_device_by_identifier(
(DOMAIN, format_mac(info["mac"])), entry.entry_id
)
if device is not None and device.sw_version != info["fw_version"]:
registry.async_update_device(device.id, sw_version=info["fw_version"])
entry.async_on_unload(coordinator.async_add_listener(_sync_firmware_version))
await async_start_catalog(hass, async_get_shared(hass))
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: NinlightConfigEntry) -> bool:
unloaded = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unloaded and not [
other
for other in hass.config_entries.async_loaded_entries(DOMAIN)
if other.entry_id != entry.entry_id
]:
async_stop_catalog(async_get_shared(hass))
return unloaded
async def async_remove_entry(hass: HomeAssistant, entry: NinlightConfigEntry) -> None:
ir.async_delete_issue(hass, DOMAIN, f"default_token_{entry.entry_id}")
ir.async_delete_issue(hass, DOMAIN, f"wifi_guard_{entry.entry_id}")
@callback
def _async_update_default_token_issue(hass: HomeAssistant, entry: NinlightConfigEntry) -> None:
issue_id = f"default_token_{entry.entry_id}"
if entry.data[CONF_TOKEN] == DEFAULT_TOKEN:
ir.async_create_issue(
hass,
DOMAIN,
issue_id,
is_fixable=False,
severity=ir.IssueSeverity.WARNING,
translation_key="default_token",
translation_placeholders={"name": entry.title},
)
else:
ir.async_delete_issue(hass, DOMAIN, issue_id)