From 06dd04e8fe6626b5a743b8d5a1f241588f27be8d Mon Sep 17 00:00:00 2001 From: Michael Sauer Date: Wed, 16 Sep 2026 17:13:31 +0200 Subject: [PATCH] docs: README, Abnahmeprotokoll, Brand-Bilder; Gitea statt GitHub/HACS --- .github/workflows/ci.yml | 29 ------ README.md | 94 +++++++++++++++++++ custom_components/ninlight/brand/icon.png | Bin 0 -> 1486 bytes custom_components/ninlight/brand/icon@2x.png | Bin 0 -> 3229 bytes custom_components/ninlight/brand/logo.png | Bin 0 -> 1486 bytes custom_components/ninlight/brand/logo@2x.png | Bin 0 -> 3229 bytes custom_components/ninlight/manifest.json | 4 +- docs/acceptance.md | 17 ++++ hacs.json | 5 - scripts/check.sh | 9 ++ scripts/check_translations.py | 35 +++++++ scripts/make_brand.py | 39 ++++++++ tests_unit/test_manifest.py | 6 ++ 13 files changed, 202 insertions(+), 36 deletions(-) delete mode 100644 .github/workflows/ci.yml create mode 100644 README.md create mode 100644 custom_components/ninlight/brand/icon.png create mode 100644 custom_components/ninlight/brand/icon@2x.png create mode 100644 custom_components/ninlight/brand/logo.png create mode 100644 custom_components/ninlight/brand/logo@2x.png create mode 100644 docs/acceptance.md delete mode 100644 hacs.json create mode 100755 scripts/check.sh create mode 100644 scripts/check_translations.py create mode 100644 scripts/make_brand.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index 1b1efef..0000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,29 +0,0 @@ -name: CI - -on: - push: - pull_request: - -jobs: - checks: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v5 - - uses: astral-sh/setup-uv@v5 - with: - cache-dependency-glob: "requirements_dev.txt" - - run: uv venv --python 3.14 .venv && uv pip install --python .venv/bin/python -r requirements_dev.txt - - run: .venv/bin/ruff check . && .venv/bin/ruff format --check . - - run: .venv/bin/pytest -q --cov --cov-report=term-missing - hassfest: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v5 - - uses: home-assistant/actions/hassfest@master - hacs: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v5 - - uses: hacs/action@main - with: - category: integration diff --git a/README.md b/README.md new file mode 100644 index 0000000..d05c1cf --- /dev/null +++ b/README.md @@ -0,0 +1,94 @@ +# ninlight für Home Assistant + +Lokale Home-Assistant-Integration für Lampen mit der [ninlight-Firmware](https://gitea.zukunpht.de/ninharp/ninlight-w600) +(migrierte Klyqa-Lampen auf WinnerMicro W600: GU10, E27, E14, G95). Keine Cloud, kein Konto – +HA spricht direkt mit der REST-API der Lampe auf Port 3333. + +## Voraussetzungen + +- Home Assistant 2026.9 oder neuer +- Lampen mit ninlight-Firmware im selben Netz + +## Installation + +Das Repo ist privat und nicht über HACS verfügbar. Den Ordner `custom_components/ninlight` nach +`/custom_components/ninlight` der Home-Assistant-Instanz kopieren (z. B. per +`/usr/bin/scp -r` oder über das Samba-/SSH-Add-on) und Home Assistant neu starten. Für ein Update +denselben Ordner ersetzen und erneut neu starten. + +## Einrichtung + +Lampen werden per mDNS (`_ninlight._tcp`) gefunden und unter *Einstellungen → Geräte & Dienste* +angeboten. Alternativ „Integration hinzufügen → ninlight" und IP-Adresse oder +`ninlight-xxxx.local` eingeben. + +Das Token-Feld ist mit dem **öffentlichen Vorgabe-Token** `aabbccddeeff0011223344` vorbelegt. +Solange eine Lampe dieses Token nutzt, kann jeder in deinem Netz sie steuern und neu flashen; +HA zeigt dafür einen Reparaturhinweis. Eigenes Token auf der Lampe setzen +(`PUT /api/v1/system/settings` mit `{"type":"token","token":"…"}`) und in HA über +*Neu konfigurieren* eintragen. + +Wechselt eine Lampe ihre IP, aktualisiert mDNS den Eintrag selbst (Identität ist die MAC). + +## Entitäten + +| Entität | Art | Hinweis | +|---|---|---| +| Lampe | Licht | An/Aus, Helligkeit, Farbe (RGB-Lampen), Farbtemperatur, Übergang | +| Einblenden, Ausblenden, Standardübergang | Zahl (Konfiguration) | Millisekunden | +| Einschaltverhalten | Auswahl (Konfiguration) | letzter Zustand / ein / aus | +| Bei Änderung einschalten | Schalter (Konfiguration) | | +| Neustart | Taste (Konfiguration) | Werksreset wird bewusst nicht angeboten | +| Signalstärke | Sensor (Diagnose) | | +| IP-Adresse, freier Speicher, letzter Update-Fehler, letzter Start | Sensor (Diagnose) | standardmäßig deaktiviert | +| Firmware | Update | siehe unten | + +## Firmware-Updates + +Home Assistant bietet Updates aus dem Ordner **`/ninlight_firmware/`** an (wird beim +ersten Start angelegt). Dort `*_ota.img`-Dateien ablegen; Dateinamen sind egal. HA liest +Gerätetyp und Version aus dem Trailer des Images, prüft die Prüfsumme und bietet jeder Lampe nur +Images **ihres** Gerätetyps an, die **neuer** sind als ihre Firmware. + +Die Lampe lädt das Image über reines `http://` direkt von Home Assistant, unter einer zufälligen +Adresse, die nur während dieses einen Updates gilt. Erreichen die Lampen die interne URL von HA +nicht über `http://` (z. B. HA hinter HTTPS oder in Docker auf macOS), in den Optionen der +Integration **„Adresse, unter der Lampen Home Assistant erreichen"** setzen, etwa +`http://192.168.1.50:8123`. + +### WLAN-Sicherung – wichtig + +Eine Lampe kennt ihr WLAN entweder aus **gespeicherten Einstellungen** oder nur aus der +**einkompilierten Firmware** (`GET /api/v1/system/settings` → `wifi.source`). Im zweiten Fall +würde ein Image **ohne** WLAN-Zugangsdaten die Lampe nach dem Update aus dem Netz werfen – sie +startet dann im Onboarding-Zugangspunkt. + +Deshalb bietet HA einer solchen Lampe nur Images an, die die SSID ihres aktuellen WLANs enthalten. +Liegt ein neueres Image ohne diese Zugangsdaten im Ordner, erscheint stattdessen ein +Reparaturhinweis. + +**Das Release-Archiv der Firmware enthält Images ohne Zugangsdaten.** Für Lampen, die ihr WLAN +aus der Firmware beziehen, eigene Images **mit** Zugangsdaten bauen (`config.local.cmake` im +Firmware-Repo). + +Grenzen der Prüfung: geprüft wird die SSID, nicht das Passwort; und eine Heim-SSID, die zufällig +einem Text der Firmware gleicht (z. B. `ninlight-recovery`), würde fälschlich als vorhanden +erkannt. + +## Diagnose + +*Gerät → ⋮ → Diagnose herunterladen* liefert Zustand, Systeminfo und passende Firmware-Dateien. +Token, WLAN-Name, MAC und IP sind geschwärzt. + +## Entwicklung + +```bash +uv venv --python /opt/homebrew/bin/python3.14 .venv && uv pip install --python .venv/bin/python -r requirements_dev.txt +scripts/check.sh +``` + +Home Assistant 2026.9 braucht Python ≥ 3.14.2. Vor jedem Push `scripts/check.sh` laufen lassen – +das Repo liegt im Gitea ohne CI. + +`tests_unit/` läuft ohne Home-Assistant-Instanz (HTTP-Client gegen einen lokalen Server, +Firmware-Logik), `tests/` gegen eine Test-Instanz. diff --git a/custom_components/ninlight/brand/icon.png b/custom_components/ninlight/brand/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..334ba1519ec21e13efff8fe88377bb552d81c5cf GIT binary patch literal 1486 zcmcgsZA_C_6h3b){V-5y>qZ4@Ar5Lpr9&cApdi)>ia?ADFcB&sx~W)UXhoo}z{U(J zZ0P*pqd2prDzS(W6_~tGT|uOc#0kbqiwIRZOMw+CrDZKfBkk*_1hQSx$4grMXc{=7hOqQ zbT#d9Z`VopUM?+sgOV5#B)o?6I%Vi{$D9wewc3sU_3TJmYw3y^oEUmO{7iBqf9}iJ z*oVK>-uEC{Q#yqCIAQ!U%4`CDP=wcdWriA4sU`2o*!|O@*D`7`myDGtzcy-UMR!RU zUzzWaxFig}nK-w~o3RB5D_FC)O9ajc18Z-KB~AH2NVf4H8*gz#nZf3AdeH&v&2g0~ z$1F*=Hu->OK(hf|QXTJ1rFUwd;XQvYaB^P_3!IjHXx8szI_3O{V2^Q+(#a_taV z8%>!@d^*R)8S(NUqQw}Dh$=2sm03k6Arx!q>AI(m14BmVNT%4Z432P;6#_{X8-fD1 zE7IDsmO&6FMvGWkScWNuC+{a5p&5PmuL~yK=Q7=$_IsoG2j&gJZ;vY*eO#O zB9d2BS6K_Tf-NoB~;mGl(8Jlha?0y4J z^eX>hZWIwl8W{1?G)+&wMnek~@P-p746Tg?M3*OBhuI04nx%H*aS_z_8 zFTkP!U80J1;am)$(|G8xV;drE?HjN531nXZkl8~IO)qT6nlGNH~Ey0ZT?l{Ta3C zH^mY@E@-X+b(gTG$EMx{O9h=H8L0*~xCZRc zxxp%@qHIPpx4%?D#wwjPjxn^A-SKOlH6Koz)E%lRZllD3U%(klgHlgL&n{LTNYdmU zb?UE3C^7|`pBwQF-W&EY_prf}j_8Bd$RK25voK^(UCKt8i@(wbdd@f3-rxTb{m&+> zy2k0TP@4XxzaugO+o^1R6bS?>qW^RO!id7x%lG0qiD5XCp4Ia*?v-O@_;j=r8TMZA zZPwe9c%w$V)VfEz28}*L5s&BX@d3JY>9?b~Dn1adv5IC)<@0>AQ|>*Ae{XY@rOw&C z9v}6VrcB4+1+-i8_?8sMrb4~*X(8YDwlRiwpj@+ukoL1YwRIqgjNz}AUdns1hKjb2 zmev(oFaKkpF@b4JuI~%K2r+eeq^o^lbmTn$E$7K~cK`uyKpvZ+3cqHeh$%dL=!swF zei_ghCB;yUN#)CN3N{dfX53EOtf8CcDL2o5JNGDAe@>5I7Kf{-D4mf@V%mo2Xl1{J gvu^goO7+h%v$hXbCQRji!gDC2H^%a>M+%Dm0_2rZu>b%7 literal 0 HcmV?d00001 diff --git a/custom_components/ninlight/brand/icon@2x.png b/custom_components/ninlight/brand/icon@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..4ab0f82bc1795d0927f4f4f7612705a6f16d602c GIT binary patch literal 3229 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 literal 0 HcmV?d00001 diff --git a/custom_components/ninlight/brand/logo@2x.png b/custom_components/ninlight/brand/logo@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..4ab0f82bc1795d0927f4f4f7612705a6f16d602c GIT binary patch literal 3229 zcmds3YgAKL7T)(Jgai`^4<#rV-q@lT#PSjZ3R*!0U)3-L2_i5qQ34dfig4u-s>Xst zl`!C>9a~?+V8rPNf^rp&O9(=j4up|M5Q$xlMJysA4KOzeMOkC#U;j*gBqwL@v-h`O z-#JG&MSjFES!ehVy z*!yzJjRj^mY=(Smm;KlpSH@6mKM~Mk-Yjk2_m3sH6FMWm;|F(HtW7QWE+BVWll`~p z*Ui`1NtC6xcL|R6q-XRWTZj2kfz?4@G+AGW2-g7qV0+K=C>Il65DLT9jRR~asv?&R zJtD?*dOqy`2ylk{)ipN??-LZfcRg%kdqS5b@Qo^`rtUz@TEHt(G=ml-zCQwPwJ+O4 zsdyR+tK%Pv#Vl-`2HC|Y@&Z4Eofg2yrA@0HAc_ofby*=~qS(qTEfu*$hH&rPotEe$ zDr`y7+B%UhFu_@__A6)d2%T@c)%B;DypK`LrCZ(oXD081>F1LqyfbC0Ctr2!U%m*> zhO%<}$x3WKv7~wD#ia5VyTOJwX?tH4h2u^vTb1q9m||HA4IX;(ZB9Leq$gzTqFifJ zp(?AO!nydm6{N2Bu8vT#0h%C)!r<84u_K{8P=cAHBEpX5Ok*GbAt-jd{ugXAx;Q2R_~xO;()hr1 z8iIGz7qLn*@7I$7$TWEH#DgSFJe+Qd;P>g)EOBPbMKZW~ZsRTPbU)7oU^;_uOD|b= z@A?o6!OzppMdf3y$%eD5pAI9+WV=yw1aF`-xW!|#Lk2+~{~D;S)EwLhBl!-9s`jRz zGl2a%7%6WcamASr*u$G9?t5!{_rQZMTM?pN( zPicCk&6n65vw!QoVD?$lSUm=S*(1!sVK#@YUZc+&HErBXWT|z~yVND}bH3J0WNmAq zw}$pil9ZIo26MXb7Cck4IYHSadPBOj@#4RjMluRZ3{dncLxf6{TKGmKM5j`eUp4Z1Wo!!w_WY(I(60rK_ znL7Qtt4pIUra6Y5YR&C&?SO?Lt-7-QFZDTI-evEd3Zfo@Zp;o%(}T+8Pq)^iL*!IW;*aZQK6X*9I}Gh&g2mLejP6Q2E21I?}yr{#|CpfM!}^Gd4~1W zv;E*-ot10Au5OJ*s@wgandvi&S@+oC*Q--aY0$@U<^^_sFw?M&t-WY<^JmIH6f?6~ z$O%%3X4*8TrI#dY)eL!|*jZvoOEOzM4JmL-^Fc->JN=44v0GpwZ1*49hGJJ)9vlx< zS%x8|s8%QlYzty(eYY6 zsT%AHB0UdhUbS|E=Pe^=Wn=AsAwz=~`Mvbst3C+Mq=!btybuNhhaP$o?eaUz1o~b6 z?!=nZwNJJJz15A(Ejl4K_d|GGfvY>;&g*>y@1RE|DK&|^7b6@-30XwQZHS9!q2PTl z{+Si?5PTbb$6BRkw<{rERzha!|6I`SC&(~|=`1_3O+x+l<=3?nj1CL9JZ{$&w zLz?)+*+GZX&sf%7j7CGlmMW~EYhI(Xeg;bih(ELdOZet{bs{Sc1BcB@%CB>)&`7 zvxx+3^?01vgnPGL93=D4zWY~fM~YTufsV-7Xg(&h#OHYlD80?Av2`W#eavSDAN#ZDkM@)UD?_ z6o&qUwvvgC);%y*78|Q@9eg3CelUT zFM?(&bZ{80Y<%&E1`?a6a%~9Uao$Cg7hVCxo6 set[str]: + out: set[str] = set() + for key, value in data.items(): + out |= keys(value, f"{prefix}{key}.") if isinstance(value, dict) else {f"{prefix}{key}"} + return out + + +def main() -> int: + strings = json.loads((BASE / "strings.json").read_text(encoding="utf-8")) + en = json.loads((BASE / "translations" / "en.json").read_text(encoding="utf-8")) + de = json.loads((BASE / "translations" / "de.json").read_text(encoding="utf-8")) + if strings != en: + print("translations/en.json weicht von strings.json ab") + return 1 + diff = sorted(keys(strings) ^ keys(de)) + if diff: + print("de.json und strings.json haben unterschiedliche Schluessel:", diff) + return 1 + print("Uebersetzungen vollstaendig") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/make_brand.py b/scripts/make_brand.py new file mode 100644 index 0000000..1e9b7c3 --- /dev/null +++ b/scripts/make_brand.py @@ -0,0 +1,39 @@ +"""Generate the brand images HA serves from custom_components/ninlight/brand/ (HA >= 2026.3).""" + +from __future__ import annotations + +from pathlib import Path + +from PIL import Image, ImageDraw + +OUT = Path(__file__).parents[1] / "custom_components" / "ninlight" / "brand" +AMBER = (255, 176, 46, 255) +DARK = (38, 38, 46, 255) + + +def bulb(size: int) -> Image.Image: + image = Image.new("RGBA", (size, size), (0, 0, 0, 0)) + draw = ImageDraw.Draw(image) + s = size / 256 + draw.ellipse((56 * s, 24 * s, 200 * s, 168 * s), fill=AMBER) + draw.rounded_rectangle((92 * s, 150 * s, 164 * s, 204 * s), radius=10 * s, fill=AMBER) + for y in (206, 222): + draw.rounded_rectangle((96 * s, y * s, 160 * s, (y + 10) * s), radius=5 * s, fill=DARK) + draw.rounded_rectangle((112 * s, 236 * s, 144 * s, 246 * s), radius=5 * s, fill=DARK) + return image + + +def main() -> None: + OUT.mkdir(parents=True, exist_ok=True) + for name, size in ( + ("icon.png", 256), + ("icon@2x.png", 512), + ("logo.png", 256), + ("logo@2x.png", 512), + ): + bulb(size).save(OUT / name) + print(OUT / name) + + +if __name__ == "__main__": + main() diff --git a/tests_unit/test_manifest.py b/tests_unit/test_manifest.py index 5a169a1..e70bed2 100644 --- a/tests_unit/test_manifest.py +++ b/tests_unit/test_manifest.py @@ -18,3 +18,9 @@ def test_manifest_matches_constants() -> None: assert manifest["config_flow"] is True assert manifest["requirements"] == [] assert manifest["dependencies"] == ["http"] + + +def test_manifest_points_to_gitea() -> None: + manifest = json.loads(MANIFEST.read_text(encoding="utf-8")) + assert manifest["documentation"] == "https://gitea.zukunpht.de/ninharp/ha-ninlight" + assert manifest["issue_tracker"] == "https://gitea.zukunpht.de/ninharp/ha-ninlight/issues"