Files
ha-ninstrip/custom_components/ninstrip/number.py
T
Michael Sauer 2c77b2d325 feat: update-Entitaet fuer die Firmware und das Testgeruest
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.
2026-09-22 23:10:07 +02:00

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))