# 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: `), 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 und Helligkeit des ganzen Strips | | `select` | aufgeschaltete Szene (acht Plätze in der Firmware) | | `button` | Neustart | | `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) | | `update` | Firmware-Aktualisierung aus dem Abbild-Ordner | Die Diagnose-Sensoren sind standardmäßig abgeschaltet; die Leistungsbegrenzung ist an, weil sie verrät, ob die Strombegrenzung gerade herunterregelt (100 % heißt: tut sie nicht). **Eine Lichtentität je Gerät, nicht je Segment.** `device/state` ist der Hauptschalter und trägt keine Farbe — bei mehreren Segmenten gäbe es keine eine Farbe, die stimmen würde. Die Segmente stellt man über Szenen. ## Noch nicht drin - Einzelne Segmente als eigene Entitäten ## 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. ### 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.