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.
This commit is contained in:
Michael Sauer
2026-09-22 23:10:07 +02:00
parent d0a9487805
commit 2c77b2d325
44 changed files with 3600 additions and 43 deletions
@@ -40,9 +40,17 @@
"init": {
"title": "Settings",
"data": {
"scan_interval": "Polling interval (seconds)"
"scan_interval": "Polling interval (seconds)",
"device_base_url": "Address devices use to reach Home Assistant"
},
"data_description": {
"device_base_url": "Only needed for firmware updates if devices cannot reach Home Assistant's internal URL over plain http, e.g. http://192.168.1.50:8123"
}
}
},
"error": {
"invalid_base_url": "Must be a plain http:// address with host and optional port, without path.",
"base_url_too_long": "Address too long – the device accepts at most 255 characters for the whole firmware URL."
}
},
"entity": {
@@ -97,6 +105,11 @@
"sacn_multicast": {
"name": "sACN multicast joined"
}
},
"update": {
"firmware": {
"name": "Firmware"
}
}
},
"exceptions": {
@@ -111,6 +124,36 @@
},
"cannot_connect": {
"message": "The device is unreachable."
},
"update_busy": {
"message": "An update is already running on the device."
},
"no_update": {
"message": "No firmware update is available."
},
"no_http_url": {
"message": "Devices can only reach Home Assistant over plain http. Set the address in the integration options."
},
"device_url_too_long": {
"message": "The firmware URL is longer than the 255 characters the device accepts."
},
"image_invalid": {
"message": "The firmware file changed or is no longer valid."
},
"wifi_guard": {
"message": "This firmware does not contain the Wi-Fi credentials for {ssid}; the device would lose its network connection."
},
"update_failed": {
"message": "The update failed: {reason}"
},
"update_timeout": {
"message": "The device did not report the new firmware within 12 minutes."
}
},
"issues": {
"wifi_guard": {
"title": "Firmware for {name} not offered",
"description": "The firmware folder contains {version} for {device_type}, but without Wi-Fi credentials for {ssid}. The device only knows its Wi-Fi from its firmware and would lose its network connection after this update, so the update is not offered. A device that was put on the network through onboarding instead gets the update without this reservation."
}
}
}