feat(update): Firmware-Update aus dem Ordner mit Einmal-URLs und WLAN-Sicherung

This commit is contained in:
Michael Sauer
2026-09-16 16:45:24 +02:00
parent cc318c9d25
commit ccc31d3286
9 changed files with 878 additions and 10 deletions
+74
View File
@@ -0,0 +1,74 @@
"""Serve firmware images to lamps under one-time URLs."""
from __future__ import annotations
from collections.abc import Mapping
from pathlib import Path
import secrets
from typing import Any
from aiohttp import web
from homeassistant.components.http import HomeAssistantView
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.network import NoURLAvailableError, get_url
from .const import CONF_LAMP_BASE_URL, DOMAIN, FIRMWARE_URL_PREFIX
class OtaRegistry:
"""Random keys, each bound to exactly one file, valid only while its update runs."""
def __init__(self) -> None:
self._paths: dict[str, Path] = {}
def create(self, path: Path) -> str:
key = secrets.token_urlsafe(32)
self._paths[key] = path
return key
def revoke(self, key: str) -> None:
self._paths.pop(key, None)
def get(self, key: str) -> Path | None:
return self._paths.get(key)
def __len__(self) -> int:
return len(self._paths)
class NinlightFirmwareView(HomeAssistantView):
"""No HA login: the lamp cannot authenticate. The unguessable key is the protection."""
url = FIRMWARE_URL_PREFIX + "{key}"
name = "api:ninlight:firmware"
requires_auth = False
def __init__(self, hass: HomeAssistant, registry: OtaRegistry) -> None:
self._hass = hass
self._registry = registry
async def get(self, request: web.Request, key: str) -> web.Response:
path = self._registry.get(key)
if path is None:
return web.Response(status=404)
try:
body = await self._hass.async_add_executor_job(path.read_bytes)
except OSError:
return web.Response(status=404)
return web.Response(body=body, content_type="application/octet-stream")
def lamp_base_url(hass: HomeAssistant, options: Mapping[str, Any]) -> str:
"""The http:// address lamps use to reach Home Assistant."""
base = options.get(CONF_LAMP_BASE_URL)
if not base:
try:
base = get_url(hass, allow_external=False, allow_cloud=False, prefer_external=False)
except NoURLAvailableError as err:
raise HomeAssistantError(
translation_domain=DOMAIN, translation_key="no_http_url"
) from err
if not str(base).startswith("http://"):
raise HomeAssistantError(translation_domain=DOMAIN, translation_key="no_http_url")
return str(base)