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.
204 lines
9.5 KiB
Markdown
204 lines
9.5 KiB
Markdown
# ha-ninstrip
|
||
|
||
Home-Assistant-Integration für **ninstrip** — ESP32-C3-Firmware für
|
||
IC-getriebene LED-Strips. Schwesterprojekt zu
|
||
[ha-ninlight](https://gitea.zukunpht.de/ninharp/ha-ninlight).
|
||
|
||
## Warum eine eigene Integration
|
||
|
||
ninlight und ninstrip teilen sich das API-Gerüst (Port 3333, `/api/v1`,
|
||
`Authorization: <token>`), beschreiben aber verschiedene Geräte: eine Lampe
|
||
mit einem Licht gegen einen Strip mit Segmenten und Szenen. Beide Modelle in
|
||
eine Integration zu zwingen hieße, in Coordinator und Entitäten dauernd zu
|
||
verzweigen — und bei jedem Fehler beide Produkte zu gefährden. Dazu kommt,
|
||
dass sich die Geräte unter verschiedenen mDNS-Diensten melden
|
||
(`_ninlight._tcp` gegen `_ninstrip._tcp`) und ohnehin getrennt gefunden
|
||
werden.
|
||
|
||
Das Gerüst ist aber bewusst **eins zu eins von ninlight übernommen**: gleiche
|
||
Fehlerklassen, gleicher Coordinator-Aufbau, gleicher Einrichtungsablauf. Wer
|
||
dort etwas korrigiert, findet es hier an derselben Stelle.
|
||
|
||
## Einrichtung
|
||
|
||
Das Gerät meldet sich per mDNS und wird von Home Assistant vorgeschlagen.
|
||
Nötig ist nur das **API-Token**. Im Auslieferungszustand und nach jedem
|
||
Werksreset antwortet das Gerät mit dem öffentlichen Werks-Token
|
||
`aabbccddeeff0011223344` — dasselbe wie bei ninlight —, und der Dialog belegt
|
||
es vor. Wer ein eigenes gesetzt hat, trägt es dort ein.
|
||
|
||
### Vertrauensgrenze
|
||
|
||
Das Werks-Token steht öffentlich im Repo. **Wer im selben WLAN ist und es
|
||
kennt, kann das Gerät steuern, Einstellungen ändern und Firmware aufspielen.**
|
||
Das ist eine bewusste Entscheidung, gleich wie bei ninlight: Ziel ist, die
|
||
Geräte einfach nutzbar zu halten, nicht ein gehärtetes Sicherheitsmodell.
|
||
|
||
Wer das nicht will, setzt ein eigenes Token — über die Firmware-API mit
|
||
`PUT system/settings {"type": "token", "token": "..."}` — und trägt es hier
|
||
ein. Für einen gehärteten Bau lässt sich das Werks-Token in der Firmware auch
|
||
ganz abschalten (`NL_DEFAULT_TOKEN` leer); dann würfelt das Gerät beim ersten
|
||
Start und gibt das Token einmalig seriell aus.
|
||
|
||
Ohne mDNS geht es auch von Hand über Adresse und Token.
|
||
|
||
## Entitäten
|
||
|
||
| Entität | |
|
||
|---|---|
|
||
| `light` | Hauptschalter, Helligkeit, Farbe, Farbtemperatur, Übergangszeit |
|
||
| `select` | aufgeschaltete Szene (acht Plätze in der Firmware) |
|
||
| `select` | Einschaltverhalten (`last` / `on` / `off`) |
|
||
| `number` | Einblend-, Ausblend- und Übergangszeit in ms |
|
||
| `switch` | bei Änderung einschalten |
|
||
| `button` | Neustart |
|
||
| `sensor` | IP-Adresse, WLAN-Signal, Laufzeit, freier Speicher, Leistungsbegrenzung, letzter Update-Fehler, Startzeit |
|
||
| `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) |
|
||
| `update` | Firmware-Aktualisierung aus dem Abbild-Ordner |
|
||
|
||
Standardmäßig an sind die **IP-Adresse** — sie ist das, was man im Alltag
|
||
sucht — und die **Leistungsbegrenzung**, weil sie verrät, ob die
|
||
Strombegrenzung gerade herunterregelt (100 % heißt: tut sie nicht). WLAN-
|
||
Signal, Laufzeit und freier Speicher sind aus; die braucht man erst im
|
||
Fehlerfall.
|
||
|
||
**Eine Lichtentität je Gerät, nicht je Segment.** `device/state` ist der
|
||
Hauptschalter und trägt keine Farbe — die steht je **Segment**. Die
|
||
Lichtentität schreibt eine gewählte Farbe deshalb in **alle** Segmente und
|
||
lässt deren Grenzen, Namen und Helligkeit stehen; die mühsam von Hand gemachte
|
||
Einteilung bleibt also erhalten.
|
||
|
||
Zeigen kann sie nur **eine** Farbe. Sind sich die Segmente nicht einig, meldet
|
||
sie gar keine, statt sich eine auszusuchen — jede Angabe wäre dann gelogen.
|
||
Farbtemperatur meldet sie nur, wenn **alle** Segmente auf `white` stehen;
|
||
sobald eines einen Effekt fährt, ist es RGB. Die Spanne ist 2700–6500 K, das
|
||
rechnet die Firmware auf die Weißkanäle um (`nl_ct.h`); Strips ohne
|
||
Weißkanäle bekommen dieselbe Spanne als RGB-Näherung.
|
||
|
||
Für einzelne Segmente gibt es noch keine Entitäten.
|
||
|
||
## Unterschiede zu ha-ninlight
|
||
|
||
Gleich sind: Einrichtung samt Neu-Konfigurieren und erneuter Anmeldung,
|
||
Diagnosedaten, Firmware-Aktualisierung, sACN, die Zeiten, das
|
||
Einschaltverhalten, „bei Änderung einschalten" und der Hinweis, wenn ein Gerät
|
||
noch auf dem Werks-Token steht.
|
||
|
||
Verschieden ist, was die Geräte verschieden macht:
|
||
|
||
- **Kein `sacn_layout`.** ninlight kann `dimmer` (Masterkanal plus Farben) und
|
||
`rgb`; ein Strip hat keinen Masterkanal, ninstrip ist immer `rgb`. Die
|
||
Firmware meldet das Feld trotzdem, damit dieselben Werkzeuge beide bedienen.
|
||
- **Farbe kommt aus den Segmenten**, bei ninlight aus `device/state`.
|
||
- **Szenen statt Farbmodi je Lampe**: acht Plätze in der Firmware, als
|
||
Auswahl.
|
||
- **Leistungsbegrenzung als Sensor** — bei einer Lampe gibt es nichts zu
|
||
begrenzen.
|
||
|
||
## Noch nicht drin
|
||
|
||
- Einzelne Segmente als eigene Entitäten
|
||
- Effekte (die Firmware kann sie je Segment, `device/effects` listet sie)
|
||
|
||
## Firmware-Aktualisierung
|
||
|
||
Aufbau wie ha-ninlight. Die Abbilder liegen im Ordner **`ninstrip_firmware`**
|
||
unterhalb des Konfigurationsverzeichnisses; `tools/release_images.sh` der
|
||
Firmware baut sie. Alle fünf Minuten wird der Ordner durchgesehen.
|
||
|
||
Home Assistant liefert das Abbild selbst aus, unter einer **einmaligen
|
||
Adresse**, die nur gilt, solange die Aktualisierung läuft. Das Gerät kann sich
|
||
bei Home Assistant nicht anmelden — der unratbare Schlüssel ist der Schutz.
|
||
Ausgeliefert werden genau die Bytes, die vorher geprüft wurden; wird die Datei
|
||
mittendrin überschrieben, ändert das nichts mehr an dem, was ankommt.
|
||
|
||
Drei Dinge entscheiden, ob ein Abbild angeboten wird:
|
||
|
||
- **Die Platine muss passen.** Verglichen wird `device_type` aus
|
||
`system/info` mit dem Typ im NINL-Anhänger des Abbildes
|
||
(`ninstrip-rgbc3`, `ninstrip-k1y`). `product` ist bei allen Platinen
|
||
`ninstrip` und taugt dafür nicht: ein K1Y-Abbild invertiert das
|
||
Datensignal, ein KL-RGBC3-Abbild nicht — die falsche Wahl lässt den Strip
|
||
dunkel. Firmware vor `v0.1.0` meldet kein `device_type` und bekommt
|
||
deshalb gar kein Angebot.
|
||
- **Die Version muss vergleichbar und neuer sein.** Gerechnet wird nach
|
||
`git describe`, nicht mit der Versionslogik von Home Assistant: die hält
|
||
`0.1.0-12-g71a57` und `0.1.0-47-g71a57` für denselben Versionskern.
|
||
Firmware vor dem Tag `v0.1.0` meldete nur einen Hash — daran lässt sich
|
||
keine Reihenfolge ablesen, also gibt es auch kein Angebot.
|
||
- **Die WLAN-Zugangsdaten müssen drin sein**, solange das Gerät sein WLAN
|
||
nur aus der Firmware kennt (`wifi.source` = `compiled`). Sonst verlöre es
|
||
nach dem Update seine Netzverbindung. Fehlen sie, erscheint ein Hinweis in
|
||
den Reparaturen statt eines Angebots. Wer das Gerät über das Onboarding ins
|
||
WLAN gebracht hat (`stored`), bekommt das Update ohne diesen Vorbehalt.
|
||
|
||
Die Geräte laden nur über **schlichtes http**. Erreichen sie die interne
|
||
Adresse von Home Assistant nicht so, trägt man in den Einstellungen der
|
||
Integration eine eigene ein (`http://…`, höchstens so lang, dass die ganze
|
||
Firmware-Adresse unter 255 Zeichen bleibt — mehr nimmt das Gerät nicht an).
|
||
|
||
## 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
|
||
|
||
Die Integration wird in der gemeinsamen Dev-Instanz `ha-dev` per Mount
|
||
eingebunden, der Quellcode bleibt in diesem Repo.
|
||
|
||
### Markenbilder
|
||
|
||
Die vier PNGs unter `custom_components/ninstrip/brand/` sind **dieselben wie
|
||
in ha-ninlight**: die beiden Integrationen gehören zusammen, und zwei
|
||
verschiedene Motive in derselben Geräteübersicht stiften nur Verwirrung. Ab
|
||
Home Assistant 2026.3 liefert HA sie für eine eigene Integration von dort
|
||
aus; ohne sie bleibt ein Platzhalter.
|
||
|
||
`scripts/make_brand.py` zeichnet ein eigenes Motiv (ein Band mit fünf
|
||
leuchtenden Pixeln) und liegt als Ausgangspunkt bereit, falls doch einmal
|
||
eines gewünscht ist — in Gebrauch ist es nicht.
|
||
|
||
### Tests
|
||
|
||
```
|
||
uv venv -p /opt/homebrew/opt/python@3.14/bin/python3.14 .venv
|
||
uv pip install --python .venv/bin/python -r requirements_dev.txt
|
||
./scripts/check.sh
|
||
```
|
||
|
||
`scripts/check.sh` ersetzt die CI — das Repo liegt in einem privaten Gitea
|
||
ohne Actions — und läuft über ruff, die Tests samt Abdeckung (mindestens 95 %)
|
||
und die Übersetzungen.
|
||
|
||
Die Tests liegen in zwei Ordnern: **`tests_unit/`** kommt ohne
|
||
Home-Assistant-Instanz aus (API-Client gegen einen echten lokalen Server,
|
||
Anhänger, Versionsreihenfolge, Adressen), **`tests/`** fährt eine
|
||
Home-Assistant-Instanz hoch. Die Nutzdaten in `tests_unit/samples.py` sind an
|
||
echten Geräten abgefragt, keine erfundenen.
|
||
|
||
Die Python-Version ist nicht beliebig: Home Assistant 2026.9 verlangt
|
||
mindestens 3.14.2, und auf PyPI liegt diese HA-Version nur für uv bereit.
|