Files
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

87 lines
2.4 KiB
Python

"""Ein echter lokaler aiohttp-Server spielt das Geraet; kein Home Assistant im Spiel."""
from __future__ import annotations
import asyncio
from collections.abc import AsyncGenerator, Awaitable, Callable
from dataclasses import dataclass, field
import json
from typing import Any
import aiohttp
from aiohttp import web
from aiohttp.test_utils import TestServer
import pytest
@dataclass
class Call:
"""Eine Anfrage, die das gespielte Geraet bekommen hat."""
method: str
path: str
headers: dict[str, str]
body: Any
@dataclass
class FakeStrip:
"""Vorgegebene Antworten je (METHODE, Pfad); merkt sich jeden Aufruf."""
host: str
port: int
routes: dict[tuple[str, str], tuple[int, str, float]] = field(default_factory=dict)
calls: list[Call] = field(default_factory=list)
def reply(
self,
method: str,
path: str,
status: int = 200,
payload: Any = None,
*,
raw: str | None = None,
delay: float = 0.0,
) -> None:
body = raw if raw is not None else json.dumps(payload)
self.routes[(method.upper(), path)] = (status, body, delay)
@pytest.fixture(autouse=True)
def _enable_sockets(socket_enabled: None) -> None:
"""pytest-homeassistant-custom-component sperrt Sockets; hier wird localhost gebraucht."""
@pytest.fixture
async def strip(aiohttp_server: Callable[..., Awaitable[TestServer]]) -> FakeStrip:
holder: list[FakeStrip] = []
async def handler(request: web.Request) -> web.Response:
fake = holder[0]
text = await request.text()
try:
body: Any = json.loads(text) if text else None
except ValueError:
body = text
fake.calls.append(Call(request.method, request.path, dict(request.headers), body))
route = fake.routes.get((request.method, request.path))
if route is None:
return web.Response(status=404, text="no route")
status, text_out, delay = route
if delay:
await asyncio.sleep(delay)
return web.Response(status=status, text=text_out)
app = web.Application()
app.router.add_route("*", "/{tail:.*}", handler)
server = await aiohttp_server(app)
fake = FakeStrip(server.host, server.port)
holder.append(fake)
return fake
@pytest.fixture
async def session() -> AsyncGenerator[aiohttp.ClientSession]:
async with aiohttp.ClientSession() as client_session:
yield client_session