Files
ha-ninstrip/custom_components/ninstrip/number.py
T
Michael Sauer 9eb7adcdd7 feat: nachgezogen, was ha-ninlight laengst hat
Michaels Befund: die Integration ist nicht dasselbe wie die von ninlight.
Verglichen und die Luecken geschlossen - alle Felder gibt es in der
Firmware schon, sie waren nur nicht bedienbar:

* number: fade_in, fade_out, transition_default in Millisekunden, mit den
  Grenzen der Firmware (10 s, 10 s, 60 s).
* select: Einschaltverhalten (last, on, off).
* switch: bei Aenderung einschalten.
* light: Uebergangszeit beim Ein- und Ausschalten. HA rechnet in Sekunden,
  das Geraet in Millisekunden; laengeres als 60 s wird gekappt, sonst
  wiese das Geraet die ganze Anfrage mit 400 ab. Ohne Angabe wird kein
  Feld geschickt, sonst ueberschriebe jeder Schaltbefehl die Vorgabe.
* sensor: letzter Update-Fehler und Startzeit (Zeitstempel statt einer
  Laufzeit, die jede Abfrage weiterlaeuft), beide ab Werk aus.
* diagnostics.py: Diagnosedaten zum Herunterladen, ohne Token, Adressen,
  MAC und SSID.
* config_flow: Neu-Konfigurieren von Adresse und Token, mit Gegenprobe der
  MAC - ein Tippfehler in der Adresse soll nicht still den falschen
  Eintrag umbiegen.
* Hinweis in den Reparaturen, wenn ein Geraet noch auf dem oeffentlichen
  Werks-Token steht.

Nicht uebernommen: die Auswahl sacn_layout. ninstrip ist immer rgb, ein
Strip hat keinen Masterkanal - eine Auswahl mit einer Option waere nur
Zierde.

241 Tests, 97 %. Die Kappung der Uebergangszeit per Mutation gegengeprueft.
2026-09-23 00:35:12 +02:00

144 lines
5.0 KiB
Python

"""Zahlenfelder eines ninstrip-Geraets: Uebergangszeiten und sACN."""
from __future__ import annotations
from dataclasses import dataclass
from homeassistant.components.number import (
NumberDeviceClass,
NumberEntity,
NumberEntityDescription,
NumberMode,
)
from homeassistant.const import EntityCategory, UnitOfTime
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import MAX_FADE_MS, MAX_TRANSITION_MS
from .coordinator import NinstripConfigEntry, NinstripCoordinator
from .entity import NinstripEntity, 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
@dataclass(frozen=True, kw_only=True)
class NinstripZeitDescription(NumberEntityDescription):
"""Ein Millisekundenfeld aus device/state."""
entity_category: EntityCategory = EntityCategory.CONFIG
device_class: NumberDeviceClass = NumberDeviceClass.DURATION
native_unit_of_measurement: str = UnitOfTime.MILLISECONDS
native_min_value: float = 0
native_step: float = 50
mode: NumberMode = NumberMode.BOX
# Grenzen wie in der Firmware (nl_api_device.c). Feldnamen wie in ninlight.
ZEITEN = (
NinstripZeitDescription(key="fade_in", translation_key="fade_in", native_max_value=MAX_FADE_MS),
NinstripZeitDescription(
key="fade_out", translation_key="fade_out", native_max_value=MAX_FADE_MS
),
NinstripZeitDescription(
key="transition_default",
translation_key="transition_default",
native_max_value=MAX_TRANSITION_MS,
),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: NinstripConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
coordinator = entry.runtime_data
entitaeten: list[NumberEntity] = [NinstripZeit(coordinator, d) for d in ZEITEN]
entitaeten.append(NinstripSacnUniverse(coordinator))
entitaeten.append(NinstripSacnStartSlot(coordinator))
async_add_entities(entitaeten)
class NinstripZeit(NinstripEntity, NumberEntity):
"""Ein Millisekundenfeld: Einblenden, Ausblenden, Vorgabe fuer Uebergaenge."""
entity_description: NinstripZeitDescription
def __init__(
self, coordinator: NinstripCoordinator, description: NinstripZeitDescription
) -> None:
super().__init__(coordinator, description.key)
self.entity_description = description
@property
def native_value(self) -> float | None:
wert = self.coordinator.data.state.get(self.entity_description.key)
return float(wert) if isinstance(wert, (int, float)) else None
async def async_set_native_value(self, value: float) -> None:
await self.coordinator.async_set_state(**{self.entity_description.key: int(value)})
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))