feat: sACN-Einstellungen und -Status als Entitaeten
Die Firmware kann sACN/E1.31 - jetzt auch in Home Assistant, aufgebaut wie in
ha-ninlight: Schalter fuer den Empfang, Zahlen fuer Universe und Startslot,
Auswahl fuer das Verhalten nach Stromende, zwei Diagnose-Binaersensoren.
Die Einstellungen liegen hinter system/settings, der Koordinator liest sie
deshalb mit - eine vierte Anfrage je Zyklus. Nach dem Schreiben wird
zurueckgelesen statt angenommen, wie in ha-ninlight.
Zwei Unterschiede zu ninlight:
- Kein Layout-Feld: ninstrip nutzt fest drei Kanaele je Pixel.
- Der Startslot gilt in JEDEM belegten Universe. Die Obergrenze ist deshalb
513 minus Kanaele je Pixel, also 510 - ein festes 512 boete Werte an, bei
denen kein einziges Pixel mehr ins Universe passt.
Aeltere Firmware ohne sacn-Block: die Entitaeten entstehen trotzdem und
melden sich nicht verfuegbar, bis ein Update kommt.
In ha-dev an der Stehlampe geprueft: Universe, Startslot, on_timeout und der
Schalter kommen jeweils auf dem Geraet an; der Aktiv-Sensor steht waehrend
eines Stroms auf an, danach auf aus. 511 als Startslot weist Home Assistant
selbst ab ("outside valid range 1 - 510"). Einstellungen der Lampe danach
zurueckgestellt.
This commit is contained in:
@@ -35,6 +35,22 @@ class NinstripData:
|
||||
state: dict[str, Any]
|
||||
info: dict[str, Any]
|
||||
presets: dict[str, Any]
|
||||
# Nur, was das Geraet unter system/settings zeigt. Aeltere Firmware traegt
|
||||
# dort keinen sacn-Block - die Entitaeten melden sich dann nicht
|
||||
# verfuegbar, statt zu verschwinden.
|
||||
settings: dict[str, Any]
|
||||
|
||||
@property
|
||||
def sacn(self) -> dict[str, Any] | None:
|
||||
"""sACN-Einstellungen, oder None bei Firmware ohne sACN."""
|
||||
block = self.settings.get("sacn")
|
||||
return block if isinstance(block, dict) else None
|
||||
|
||||
@property
|
||||
def sacn_info(self) -> dict[str, Any] | None:
|
||||
"""sACN-Zustand aus system/info, oder None bei Firmware ohne sACN."""
|
||||
block = self.info.get("sacn")
|
||||
return block if isinstance(block, dict) else None
|
||||
|
||||
|
||||
class NinstripCoordinator(DataUpdateCoordinator[NinstripData]):
|
||||
@@ -69,6 +85,9 @@ class NinstripCoordinator(DataUpdateCoordinator[NinstripData]):
|
||||
state = await self.client.get_state()
|
||||
info = await self.client.get_info()
|
||||
presets = await self.client.get_presets()
|
||||
# Die sACN-Einstellungen liegen hinter system/settings, nicht in
|
||||
# device/state - eine vierte Anfrage je Zyklus. Wie ha-ninlight.
|
||||
settings = await self.client.get_settings()
|
||||
except NinstripAuthError as err:
|
||||
raise ConfigEntryAuthFailed(f"{self.client.host}: Token abgelehnt") from err
|
||||
except NinstripError as err:
|
||||
@@ -84,7 +103,7 @@ class NinstripCoordinator(DataUpdateCoordinator[NinstripData]):
|
||||
return self.data
|
||||
raise UpdateFailed(f"{self.client.host}: {err}") from err
|
||||
self._consecutive_poll_failures = 0
|
||||
return NinstripData(state, info, presets)
|
||||
return NinstripData(state, info, presets, settings)
|
||||
|
||||
async def async_call[T](self, action: Callable[[NinstripClient], Awaitable[T]]) -> T:
|
||||
"""Fuehrt eine Anfrage unter der Sperre aus und uebersetzt Fehler fuer die Oberflaeche."""
|
||||
@@ -115,7 +134,28 @@ class NinstripCoordinator(DataUpdateCoordinator[NinstripData]):
|
||||
"""Schreibt und uebernimmt die vollstaendige Antwort des Geraets direkt."""
|
||||
state = await self.async_call(lambda client: client.set_state(**fields))
|
||||
self._consecutive_poll_failures = 0
|
||||
self.async_set_updated_data(NinstripData(state, self.data.info, self.data.presets))
|
||||
self.async_set_updated_data(
|
||||
NinstripData(state, self.data.info, self.data.presets, self.data.settings)
|
||||
)
|
||||
|
||||
async def async_set_sacn(self, **fields: Any) -> None:
|
||||
"""sACN-Einstellungen schreiben und die Antwort des Geraets uebernehmen.
|
||||
|
||||
Das Geraet antwortet nur mit {"status": "ok"}, also wird zurueckgelesen
|
||||
statt angenommen: es prueft selbst, und eine Entitaet, die das
|
||||
Gewuenschte statt des Gespeicherten zeigt, luegt genau dann, wenn
|
||||
beides auseinanderlaeuft. Wie ha-ninlight.
|
||||
"""
|
||||
|
||||
async def schreiben_und_lesen(client: NinstripClient) -> dict[str, Any]:
|
||||
await client.set_sacn(**fields)
|
||||
return await client.get_settings()
|
||||
|
||||
settings = await self.async_call(schreiben_und_lesen)
|
||||
self._consecutive_poll_failures = 0
|
||||
self.async_set_updated_data(
|
||||
NinstripData(self.data.state, self.data.info, self.data.presets, settings)
|
||||
)
|
||||
|
||||
async def async_after_change(self) -> None:
|
||||
"""Nach einem Eingriff, der den ganzen Stand aendern kann (Szene, Segmente)."""
|
||||
|
||||
Reference in New Issue
Block a user