Files
ha-ninstrip/README.md
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

157 lines
7.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 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.