Aufgebaut wie ha-ninlight: die Abbilder liegen unter ninstrip_firmware im Konfigurationsverzeichnis, Home Assistant liefert sie selbst unter einer einmaligen Adresse aus, und zwar genau die Bytes, die vorher geprueft wurden. Dazu firmware.py (Anhaenger, Versionsreihenfolge, WLAN-Pruefung, Katalog), http.py, urls.py und shared.py. Drei Unterschiede zu ninlight, alle aus der Firmware heraus: * Verglichen wird device_type - die Platine -, nicht product. Letzteres ist bei allen Platinen 'ninstrip'; die Abbilder unterscheiden sich aber hart, die K1Y invertiert das Datensignal und die KL-RGBC3 nicht. Firmware ohne dieses Feld bekommt gar kein Angebot. * Die WLAN-Pruefung sucht die SSID unverpackt: ein ESP32-Abbild ist nicht gzip-verpackt wie das des W600. An einem mit sdkconfig.local gebauten Abbild gegengeprueft - die SSID steht genau einmal NUL-gerahmt drin. * Der Fehlerschluessel heisst device_url_too_long statt lamp_url_too_long. Dazu das Testgeruest, das es hier bisher gar nicht gab: 204 Tests in tests_unit/ (ohne HA-Instanz) und tests/ (mit), Abdeckung 97 %, dazu scripts/check.sh als Ersatz fuer die fehlende CI. Die Nutzdaten in tests_unit/samples.py sind an der Stehlampe abgefragt. Beim Gegenpruefen per Mutation fiel auf, dass die Platinenpruefung unmittelbar vor dem Senden ungeprueft war - der Test dazu ist jetzt da und faellt, wenn man die Pruefung entfernt.
87 lines
3.1 KiB
Python
87 lines
3.1 KiB
Python
"""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))
|