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:
@@ -50,6 +50,10 @@ Ohne mDNS geht es auch von Hand über Adresse und Token.
|
|||||||
| `select` | aufgeschaltete Szene (acht Plätze in der Firmware) |
|
| `select` | aufgeschaltete Szene (acht Plätze in der Firmware) |
|
||||||
| `button` | Neustart |
|
| `button` | Neustart |
|
||||||
| `sensor` | WLAN-Signal, Laufzeit, freier Speicher, Leistungsbegrenzung |
|
| `sensor` | WLAN-Signal, Laufzeit, freier Speicher, Leistungsbegrenzung |
|
||||||
|
| `switch` | sACN-Empfang an/aus |
|
||||||
|
| `number` | sACN-Universe und -Startslot |
|
||||||
|
| `select` | Verhalten nach Stromende (`release` / `hold`) |
|
||||||
|
| `binary_sensor` | sACN aktiv, Multicast beigetreten (letzterer standardmäßig aus) |
|
||||||
|
|
||||||
Die Diagnose-Sensoren sind standardmäßig abgeschaltet; die
|
Die Diagnose-Sensoren sind standardmäßig abgeschaltet; die
|
||||||
Leistungsbegrenzung ist an, weil sie verrät, ob die Strombegrenzung gerade
|
Leistungsbegrenzung ist an, weil sie verrät, ob die Strombegrenzung gerade
|
||||||
@@ -64,7 +68,30 @@ eine Farbe, die stimmen würde. Die Segmente stellt man über Szenen.
|
|||||||
- Einzelne Segmente als eigene Entitäten
|
- Einzelne Segmente als eigene Entitäten
|
||||||
- Firmware-Aktualisierung über eine `update`-Entität (die Firmware kann OTA
|
- Firmware-Aktualisierung über eine `update`-Entität (die Firmware kann OTA
|
||||||
mit Rückfallebene, die Integration nutzt es noch nicht)
|
mit Rückfallebene, die Integration nutzt es noch nicht)
|
||||||
- sACN/E1.31 — in der Firmware geplant, siehe ninstrip-Repo
|
|
||||||
|
## sACN / E1.31
|
||||||
|
|
||||||
|
Aufbau wie ha-ninlight: die Einstellungen kommen aus `system/settings`, der
|
||||||
|
Zustand aus `system/info`, und nach jedem Schreiben wird **zurückgelesen**
|
||||||
|
statt angenommen — das Gerät prüft selbst, und eine Entität, die das
|
||||||
|
Gewünschte statt des Gespeicherten zeigt, lügt genau dann, wenn beides
|
||||||
|
auseinanderläuft.
|
||||||
|
|
||||||
|
Zwei Unterschiede zu ninlight:
|
||||||
|
|
||||||
|
- **Kein Layout-Feld.** ninstrip nutzt fest drei Kanäle je Pixel (R, G, B), so
|
||||||
|
wie LedFx, xLights und Resolume senden.
|
||||||
|
- **Der Startslot gilt in jedem Universe**, nicht nur im ersten — ein Strip
|
||||||
|
verteilt sich über mehrere. Die Obergrenze ist deshalb `513 − 3 = 510`:
|
||||||
|
dahinter passt kein ganzes Pixel mehr hinein.
|
||||||
|
|
||||||
|
Jede Änderung über Home Assistant holt einem laufenden oder gehaltenen Strom
|
||||||
|
die Kontrolle ab — das erledigt die Firmware.
|
||||||
|
|
||||||
|
**Multicast über VLAN-Grenzen:** „beigetreten" heißt nur, dass das Gerät der
|
||||||
|
Gruppe beigetreten ist. Ob das Netz sie weiterreicht, steht auf einem anderen
|
||||||
|
Blatt; ohne Multicast-Router kommt sie über eine VLAN-Grenze nie an. Unicast
|
||||||
|
geht dann trotzdem.
|
||||||
|
|
||||||
## Entwicklung
|
## Entwicklung
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ PLATFORMS: list[Platform] = [
|
|||||||
Platform.SELECT,
|
Platform.SELECT,
|
||||||
Platform.SENSOR,
|
Platform.SENSOR,
|
||||||
Platform.BUTTON,
|
Platform.BUTTON,
|
||||||
|
Platform.SWITCH,
|
||||||
|
Platform.NUMBER,
|
||||||
|
Platform.BINARY_SENSOR,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -113,6 +113,10 @@ class NinstripClient:
|
|||||||
async def apply_preset(self, index: int) -> Any:
|
async def apply_preset(self, index: int) -> Any:
|
||||||
return await self._request("PUT", f"device/presets/{index}/apply")
|
return await self._request("PUT", f"device/presets/{index}/apply")
|
||||||
|
|
||||||
|
async def set_sacn(self, **fields: Any) -> None:
|
||||||
|
"""sACN-Einstellungen aendern. Weggelassene Felder behalten ihren Wert."""
|
||||||
|
await self._request("PUT", "system/settings", {"type": "sacn", **fields})
|
||||||
|
|
||||||
async def reboot(self) -> None:
|
async def reboot(self) -> None:
|
||||||
await self._request("PUT", "system/command", {"command": {"type": "reboot"}})
|
await self._request("PUT", "system/command", {"command": {"type": "reboot"}})
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
"""Diagnose-Binaersensoren eines ninstrip-Geraets: Zustand des sACN-Empfangs."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from homeassistant.components.binary_sensor import BinarySensorEntity
|
||||||
|
from homeassistant.const import EntityCategory
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
|
|
||||||
|
from .coordinator import NinstripConfigEntry, NinstripCoordinator
|
||||||
|
from .entity import NinstripEntity
|
||||||
|
|
||||||
|
PARALLEL_UPDATES = 0
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entry: NinstripConfigEntry,
|
||||||
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
coordinator = entry.runtime_data
|
||||||
|
async_add_entities(
|
||||||
|
[NinstripSacnActive(coordinator), NinstripSacnMulticast(coordinator)]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class _NinstripSacnInfo(NinstripEntity, BinarySensorEntity):
|
||||||
|
"""Basis fuer die beiden Merker unter system/info -> sacn."""
|
||||||
|
|
||||||
|
_attr_entity_category = EntityCategory.DIAGNOSTIC
|
||||||
|
_field: str
|
||||||
|
|
||||||
|
@property
|
||||||
|
def available(self) -> bool:
|
||||||
|
return super().available and self.coordinator.data.sacn_info is not None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_on(self) -> bool:
|
||||||
|
return bool((self.coordinator.data.sacn_info or {}).get(self._field))
|
||||||
|
|
||||||
|
|
||||||
|
class NinstripSacnActive(_NinstripSacnInfo):
|
||||||
|
"""An, solange ein Sender das Bild bestimmt - auch in der Haltephase."""
|
||||||
|
|
||||||
|
_attr_translation_key = "sacn_active"
|
||||||
|
_field = "active"
|
||||||
|
|
||||||
|
def __init__(self, coordinator: NinstripCoordinator) -> None:
|
||||||
|
super().__init__(coordinator, "sacn_active")
|
||||||
|
|
||||||
|
|
||||||
|
class NinstripSacnMulticast(_NinstripSacnInfo):
|
||||||
|
"""An, wenn der Beitritt zu allen Multicast-Gruppen gelang.
|
||||||
|
|
||||||
|
Standardmaessig aus, wie bei ha-ninlight: im Normalbetrieb steht er auf
|
||||||
|
"an" und verdient seinen Platz erst, wenn Multicast nicht ankommt. Bei
|
||||||
|
ninlight hat eine falsche Gruppenadresse zwei Abende Suche gekostet - hier
|
||||||
|
zeigt es sich.
|
||||||
|
|
||||||
|
Achtung: "an" heisst nur, dass das GERAET beigetreten ist. Ob das Netz
|
||||||
|
die Gruppe weiterreicht, steht auf einem anderen Blatt - ueber eine
|
||||||
|
VLAN-Grenze kommt Multicast ohne Multicast-Router nie an.
|
||||||
|
"""
|
||||||
|
|
||||||
|
_attr_translation_key = "sacn_multicast"
|
||||||
|
_attr_entity_registry_enabled_default = False
|
||||||
|
_field = "multicast"
|
||||||
|
|
||||||
|
def __init__(self, coordinator: NinstripCoordinator) -> None:
|
||||||
|
super().__init__(coordinator, "sacn_multicast")
|
||||||
@@ -35,6 +35,22 @@ class NinstripData:
|
|||||||
state: dict[str, Any]
|
state: dict[str, Any]
|
||||||
info: dict[str, Any]
|
info: dict[str, Any]
|
||||||
presets: 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]):
|
class NinstripCoordinator(DataUpdateCoordinator[NinstripData]):
|
||||||
@@ -69,6 +85,9 @@ class NinstripCoordinator(DataUpdateCoordinator[NinstripData]):
|
|||||||
state = await self.client.get_state()
|
state = await self.client.get_state()
|
||||||
info = await self.client.get_info()
|
info = await self.client.get_info()
|
||||||
presets = await self.client.get_presets()
|
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:
|
except NinstripAuthError as err:
|
||||||
raise ConfigEntryAuthFailed(f"{self.client.host}: Token abgelehnt") from err
|
raise ConfigEntryAuthFailed(f"{self.client.host}: Token abgelehnt") from err
|
||||||
except NinstripError as err:
|
except NinstripError as err:
|
||||||
@@ -84,7 +103,7 @@ class NinstripCoordinator(DataUpdateCoordinator[NinstripData]):
|
|||||||
return self.data
|
return self.data
|
||||||
raise UpdateFailed(f"{self.client.host}: {err}") from err
|
raise UpdateFailed(f"{self.client.host}: {err}") from err
|
||||||
self._consecutive_poll_failures = 0
|
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:
|
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."""
|
"""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."""
|
"""Schreibt und uebernimmt die vollstaendige Antwort des Geraets direkt."""
|
||||||
state = await self.async_call(lambda client: client.set_state(**fields))
|
state = await self.async_call(lambda client: client.set_state(**fields))
|
||||||
self._consecutive_poll_failures = 0
|
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:
|
async def async_after_change(self) -> None:
|
||||||
"""Nach einem Eingriff, der den ganzen Stand aendern kann (Szene, Segmente)."""
|
"""Nach einem Eingriff, der den ganzen Stand aendern kann (Szene, Segmente)."""
|
||||||
|
|||||||
@@ -52,3 +52,22 @@ class NinstripEntity(CoordinatorEntity[NinstripCoordinator]):
|
|||||||
name=device_name(info),
|
name=device_name(info),
|
||||||
sw_version=info["fw_version"],
|
sw_version=info["fw_version"],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class NinstripSacnEntity(NinstripEntity):
|
||||||
|
"""Basis fuer Entitaeten, die an den sACN-Einstellungen haengen.
|
||||||
|
|
||||||
|
Firmware von vor sACN antwortet ohne sacn-Block. Die Entitaeten entstehen
|
||||||
|
trotzdem - so werden sie nach einem Update beim naechsten Abfragen
|
||||||
|
lebendig, ohne die Integration neu zu laden -, melden sich aber bis dahin
|
||||||
|
nicht verfuegbar. Wie ha-ninlight.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def available(self) -> bool:
|
||||||
|
return super().available and self.coordinator.data.sacn is not None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sacn(self) -> dict[str, Any]:
|
||||||
|
"""Die sACN-Einstellungen; leer, solange das Geraet keine meldet."""
|
||||||
|
return self.coordinator.data.sacn or {}
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
"""sACN-Zahlenfelder eines ninstrip-Geraets: Universe und Startslot."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from homeassistant.components.number import NumberEntity, NumberMode
|
||||||
|
from homeassistant.const import EntityCategory
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
|
|
||||||
|
from .coordinator import NinstripConfigEntry, NinstripCoordinator
|
||||||
|
from .entity import NinstripSacnEntity
|
||||||
|
|
||||||
|
PARALLEL_UPDATES = 0
|
||||||
|
|
||||||
|
# Kanaele je Pixel, falls das Geraet sie nicht meldet. Die Firmware stellt fest
|
||||||
|
# drei ein (R, G, B) - so senden LedFx, xLights und Resolume.
|
||||||
|
_KANAELE_VORGABE = 3
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entry: NinstripConfigEntry,
|
||||||
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
coordinator = entry.runtime_data
|
||||||
|
async_add_entities(
|
||||||
|
[NinstripSacnUniverse(coordinator), NinstripSacnStartSlot(coordinator)]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class NinstripSacnUniverse(NinstripSacnEntity, NumberEntity):
|
||||||
|
"""Das erste Universe. Braucht der Strip mehr, folgen die naechsten."""
|
||||||
|
|
||||||
|
_attr_entity_category = EntityCategory.CONFIG
|
||||||
|
_attr_translation_key = "sacn_universe"
|
||||||
|
_attr_native_min_value = 1
|
||||||
|
_attr_native_max_value = 63_999
|
||||||
|
_attr_native_step = 1
|
||||||
|
_attr_mode = NumberMode.BOX
|
||||||
|
|
||||||
|
def __init__(self, coordinator: NinstripCoordinator) -> None:
|
||||||
|
super().__init__(coordinator, "sacn_universe")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def native_value(self) -> float | None:
|
||||||
|
value = self.sacn.get("universe")
|
||||||
|
return float(value) if isinstance(value, (int, float)) else None
|
||||||
|
|
||||||
|
async def async_set_native_value(self, value: float) -> None:
|
||||||
|
await self.coordinator.async_set_sacn(universe=int(value))
|
||||||
|
|
||||||
|
|
||||||
|
class NinstripSacnStartSlot(NinstripSacnEntity, NumberEntity):
|
||||||
|
"""Der erste Slot - er gilt in JEDEM belegten Universe, nicht nur im ersten.
|
||||||
|
|
||||||
|
Anders als bei ninlight, wo eine Lampe ein einzelner Slotsatz ist: ein
|
||||||
|
Strip verteilt sich ueber mehrere Universes, und in jedem beginnt er an
|
||||||
|
derselben Stelle.
|
||||||
|
"""
|
||||||
|
|
||||||
|
_attr_entity_category = EntityCategory.CONFIG
|
||||||
|
_attr_translation_key = "sacn_start_slot"
|
||||||
|
_attr_native_min_value = 1
|
||||||
|
_attr_native_step = 1
|
||||||
|
_attr_mode = NumberMode.BOX
|
||||||
|
|
||||||
|
def __init__(self, coordinator: NinstripCoordinator) -> None:
|
||||||
|
super().__init__(coordinator, "sacn_start_slot")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def native_max_value(self) -> float:
|
||||||
|
"""Der hoechste Startslot, hinter dem noch ein ganzes Pixel Platz hat.
|
||||||
|
|
||||||
|
Bei drei Kanaelen sind das 510. Ein festes 512 wuerde Werte anbieten,
|
||||||
|
bei denen kein einziges Pixel mehr ins Universe passt.
|
||||||
|
"""
|
||||||
|
kanaele = self.sacn.get("channels_per_pixel")
|
||||||
|
if not isinstance(kanaele, int) or not 1 <= kanaele <= 512:
|
||||||
|
kanaele = _KANAELE_VORGABE
|
||||||
|
return 513 - kanaele
|
||||||
|
|
||||||
|
@property
|
||||||
|
def native_value(self) -> float | None:
|
||||||
|
value = self.sacn.get("start_slot")
|
||||||
|
return float(value) if isinstance(value, (int, float)) else None
|
||||||
|
|
||||||
|
async def async_set_native_value(self, value: float) -> None:
|
||||||
|
await self.coordinator.async_set_sacn(start_slot=int(value))
|
||||||
@@ -3,23 +3,30 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from homeassistant.components.select import SelectEntity
|
from homeassistant.components.select import SelectEntity
|
||||||
|
from homeassistant.const import EntityCategory
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
|
|
||||||
from .coordinator import NinstripConfigEntry, NinstripCoordinator
|
from .coordinator import NinstripConfigEntry, NinstripCoordinator
|
||||||
from .entity import NinstripEntity
|
from .entity import NinstripEntity, NinstripSacnEntity
|
||||||
|
|
||||||
# Wird angezeigt, wenn keine Szene aufgeschaltet ist oder jemand die Segmente
|
# Wird angezeigt, wenn keine Szene aufgeschaltet ist oder jemand die Segmente
|
||||||
# seit dem Aufschalten veraendert hat.
|
# seit dem Aufschalten veraendert hat.
|
||||||
NONE_OPTION = "—"
|
NONE_OPTION = "—"
|
||||||
|
|
||||||
|
# Was das Geraet 2,5 s nach dem letzten sACN-Paket tut. Werte wie ha-ninlight.
|
||||||
|
ON_TIMEOUT_OPTIONS = ["release", "hold"]
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
entry: NinstripConfigEntry,
|
entry: NinstripConfigEntry,
|
||||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
async_add_entities([NinstripPresetSelect(entry.runtime_data)])
|
coordinator = entry.runtime_data
|
||||||
|
async_add_entities(
|
||||||
|
[NinstripPresetSelect(coordinator), NinstripSacnOnTimeoutSelect(coordinator)]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class NinstripPresetSelect(NinstripEntity, SelectEntity):
|
class NinstripPresetSelect(NinstripEntity, SelectEntity):
|
||||||
@@ -65,3 +72,27 @@ class NinstripPresetSelect(NinstripEntity, SelectEntity):
|
|||||||
await self.coordinator.async_call(lambda c: c.apply_preset(index))
|
await self.coordinator.async_call(lambda c: c.apply_preset(index))
|
||||||
await self.coordinator.async_after_change()
|
await self.coordinator.async_after_change()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
|
class NinstripSacnOnTimeoutSelect(NinstripSacnEntity, SelectEntity):
|
||||||
|
"""Was nach dem Ende eines sACN-Stroms passiert.
|
||||||
|
|
||||||
|
release: zurueck zum vorherigen Bild. hold: das letzte Bild bleibt stehen,
|
||||||
|
bis ein Bedienbefehl kommt - jede Aenderung ueber Home Assistant holt die
|
||||||
|
Kontrolle zurueck.
|
||||||
|
"""
|
||||||
|
|
||||||
|
_attr_entity_category = EntityCategory.CONFIG
|
||||||
|
_attr_translation_key = "sacn_on_timeout"
|
||||||
|
_attr_options = ON_TIMEOUT_OPTIONS
|
||||||
|
|
||||||
|
def __init__(self, coordinator: NinstripCoordinator) -> None:
|
||||||
|
super().__init__(coordinator, "sacn_on_timeout")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current_option(self) -> str | None:
|
||||||
|
value = self.sacn.get("on_timeout")
|
||||||
|
return value if value in ON_TIMEOUT_OPTIONS else None
|
||||||
|
|
||||||
|
async def async_select_option(self, option: str) -> None:
|
||||||
|
await self.coordinator.async_set_sacn(on_timeout=option)
|
||||||
|
|||||||
@@ -49,6 +49,13 @@
|
|||||||
"select": {
|
"select": {
|
||||||
"preset": {
|
"preset": {
|
||||||
"name": "Szene"
|
"name": "Szene"
|
||||||
|
},
|
||||||
|
"sacn_on_timeout": {
|
||||||
|
"name": "sACN nach Stromende",
|
||||||
|
"state": {
|
||||||
|
"release": "Zurück zum vorherigen Bild",
|
||||||
|
"hold": "Letztes Bild halten"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"button": {
|
"button": {
|
||||||
@@ -69,6 +76,27 @@
|
|||||||
"power_scale": {
|
"power_scale": {
|
||||||
"name": "Leistungsbegrenzung"
|
"name": "Leistungsbegrenzung"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"switch": {
|
||||||
|
"sacn_enabled": {
|
||||||
|
"name": "sACN-Empfang"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"number": {
|
||||||
|
"sacn_universe": {
|
||||||
|
"name": "sACN-Universe"
|
||||||
|
},
|
||||||
|
"sacn_start_slot": {
|
||||||
|
"name": "sACN-Startslot"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"binary_sensor": {
|
||||||
|
"sacn_active": {
|
||||||
|
"name": "sACN aktiv"
|
||||||
|
},
|
||||||
|
"sacn_multicast": {
|
||||||
|
"name": "sACN-Multicast beigetreten"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"exceptions": {
|
"exceptions": {
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
"""sACN-Schalter eines ninstrip-Geraets."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from homeassistant.components.switch import SwitchEntity
|
||||||
|
from homeassistant.const import EntityCategory
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
|
|
||||||
|
from .coordinator import NinstripConfigEntry, NinstripCoordinator
|
||||||
|
from .entity import NinstripSacnEntity
|
||||||
|
|
||||||
|
PARALLEL_UPDATES = 0
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entry: NinstripConfigEntry,
|
||||||
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
async_add_entities([NinstripSacnSwitch(entry.runtime_data)])
|
||||||
|
|
||||||
|
|
||||||
|
class NinstripSacnSwitch(NinstripSacnEntity, SwitchEntity):
|
||||||
|
"""Ob das Geraet ueberhaupt auf sACN/E1.31 hoert.
|
||||||
|
|
||||||
|
Abschalten holt dem Strom auch die Kontrolle ab - bei on_timeout "hold"
|
||||||
|
bliebe sonst das letzte Bild stehen. Die Firmware erledigt das selbst.
|
||||||
|
"""
|
||||||
|
|
||||||
|
_attr_entity_category = EntityCategory.CONFIG
|
||||||
|
_attr_translation_key = "sacn_enabled"
|
||||||
|
|
||||||
|
def __init__(self, coordinator: NinstripCoordinator) -> None:
|
||||||
|
super().__init__(coordinator, "sacn_enabled")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_on(self) -> bool:
|
||||||
|
return bool(self.sacn.get("enabled"))
|
||||||
|
|
||||||
|
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||||
|
await self.coordinator.async_set_sacn(enabled=True)
|
||||||
|
|
||||||
|
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||||
|
await self.coordinator.async_set_sacn(enabled=False)
|
||||||
@@ -49,6 +49,13 @@
|
|||||||
"select": {
|
"select": {
|
||||||
"preset": {
|
"preset": {
|
||||||
"name": "Szene"
|
"name": "Szene"
|
||||||
|
},
|
||||||
|
"sacn_on_timeout": {
|
||||||
|
"name": "sACN nach Stromende",
|
||||||
|
"state": {
|
||||||
|
"release": "Zurück zum vorherigen Bild",
|
||||||
|
"hold": "Letztes Bild halten"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"button": {
|
"button": {
|
||||||
@@ -69,6 +76,27 @@
|
|||||||
"power_scale": {
|
"power_scale": {
|
||||||
"name": "Leistungsbegrenzung"
|
"name": "Leistungsbegrenzung"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"switch": {
|
||||||
|
"sacn_enabled": {
|
||||||
|
"name": "sACN-Empfang"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"number": {
|
||||||
|
"sacn_universe": {
|
||||||
|
"name": "sACN-Universe"
|
||||||
|
},
|
||||||
|
"sacn_start_slot": {
|
||||||
|
"name": "sACN-Startslot"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"binary_sensor": {
|
||||||
|
"sacn_active": {
|
||||||
|
"name": "sACN aktiv"
|
||||||
|
},
|
||||||
|
"sacn_multicast": {
|
||||||
|
"name": "sACN-Multicast beigetreten"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"exceptions": {
|
"exceptions": {
|
||||||
|
|||||||
@@ -49,6 +49,13 @@
|
|||||||
"select": {
|
"select": {
|
||||||
"preset": {
|
"preset": {
|
||||||
"name": "Scene"
|
"name": "Scene"
|
||||||
|
},
|
||||||
|
"sacn_on_timeout": {
|
||||||
|
"name": "sACN on stream end",
|
||||||
|
"state": {
|
||||||
|
"release": "Return to previous image",
|
||||||
|
"hold": "Hold last image"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"button": {
|
"button": {
|
||||||
@@ -69,6 +76,27 @@
|
|||||||
"power_scale": {
|
"power_scale": {
|
||||||
"name": "Power limiting"
|
"name": "Power limiting"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"switch": {
|
||||||
|
"sacn_enabled": {
|
||||||
|
"name": "sACN receiver"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"number": {
|
||||||
|
"sacn_universe": {
|
||||||
|
"name": "sACN universe"
|
||||||
|
},
|
||||||
|
"sacn_start_slot": {
|
||||||
|
"name": "sACN start slot"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"binary_sensor": {
|
||||||
|
"sacn_active": {
|
||||||
|
"name": "sACN active"
|
||||||
|
},
|
||||||
|
"sacn_multicast": {
|
||||||
|
"name": "sACN multicast joined"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"exceptions": {
|
"exceptions": {
|
||||||
|
|||||||
Reference in New Issue
Block a user