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:
@@ -0,0 +1,75 @@
|
||||
"""Was sich alle ninstrip-Eintraege teilen: der Abbild-Katalog und die einmaligen Schluessel."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
|
||||
from .const import CATALOG_SCAN_INTERVAL, DOMAIN, FIRMWARE_DIR_NAME, SIGNAL_CATALOG_UPDATED
|
||||
from .firmware import FirmwareCatalog
|
||||
from .http import NinstripFirmwareView, OtaRegistry
|
||||
|
||||
|
||||
def firmware_directory(hass: HomeAssistant) -> Path:
|
||||
return Path(hass.config.path(FIRMWARE_DIR_NAME))
|
||||
|
||||
|
||||
@dataclass
|
||||
class NinstripShared:
|
||||
"""Einmal je Home Assistant."""
|
||||
|
||||
catalog: FirmwareCatalog
|
||||
registry: OtaRegistry
|
||||
scan_lock: asyncio.Lock = field(default_factory=asyncio.Lock)
|
||||
unsub_scan: CALLBACK_TYPE | None = None
|
||||
|
||||
|
||||
DATA_SHARED: HassKey[NinstripShared] = HassKey(DOMAIN)
|
||||
|
||||
|
||||
@callback
|
||||
def async_get_shared(hass: HomeAssistant) -> NinstripShared:
|
||||
"""Beim ersten Zugriff anlegen und die Ausgabe genau einmal anmelden."""
|
||||
shared = hass.data.get(DATA_SHARED)
|
||||
if shared is None:
|
||||
shared = NinstripShared(FirmwareCatalog(firmware_directory(hass)), OtaRegistry())
|
||||
hass.data[DATA_SHARED] = shared
|
||||
hass.http.register_view(NinstripFirmwareView(hass, shared.registry))
|
||||
return shared
|
||||
|
||||
|
||||
async def async_scan_catalog(hass: HomeAssistant, shared: NinstripShared) -> None:
|
||||
async with shared.scan_lock:
|
||||
await hass.async_add_executor_job(shared.catalog.scan)
|
||||
async_dispatcher_send(hass, SIGNAL_CATALOG_UPDATED)
|
||||
|
||||
|
||||
async def async_start_catalog(hass: HomeAssistant, shared: NinstripShared) -> None:
|
||||
if shared.unsub_scan is not None:
|
||||
return
|
||||
|
||||
async def _turnus(_now: datetime) -> None:
|
||||
await async_scan_catalog(hass, shared)
|
||||
|
||||
shared.unsub_scan = async_track_time_interval(
|
||||
hass,
|
||||
_turnus,
|
||||
CATALOG_SCAN_INTERVAL,
|
||||
name="ninstrip firmware catalog",
|
||||
cancel_on_shutdown=True,
|
||||
)
|
||||
await async_scan_catalog(hass, shared)
|
||||
|
||||
|
||||
@callback
|
||||
def async_stop_catalog(shared: NinstripShared) -> None:
|
||||
if shared.unsub_scan is not None:
|
||||
shared.unsub_scan()
|
||||
shared.unsub_scan = None
|
||||
Reference in New Issue
Block a user