Add "Mein Zimmer" room-control page (Home Assistant, proxied through the backend)

Implements the room-control page from the design mockup: a scenes row above cards
for shutters, color lamps, and brightness-only lamps, all driven by a new
`general.ha` config section (server URL, token, ordered device/scene lists).

The backend proxies every Home Assistant call server-side (GET/POST /api/ha/...)
rather than the browser calling Home Assistant directly, so the long-lived token
never leaves the LAN device and Home Assistant's own CORS settings don't need to
know about musicmouse at all. Card kind (shutter/color/brightness-only) is
inferred at runtime from what Home Assistant reports about each entity, not
configured explicitly.

Also stops tracking python-backend/config.yml, which had drifted into the repo
despite its own header saying it shouldn't be - it now carries real credentials
locally and needs to stay untracked.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-27 23:45:29 +02:00
parent edb6e5e027
commit a8ed350aec
27 changed files with 1437 additions and 163 deletions

View File

@@ -36,6 +36,8 @@ __all__ = [
"FigureColors",
"FigureConfig",
"GeneralConfig",
"HaConfig",
"HaDeviceConfig",
"LibraryConfig",
"MqttConfig",
"WebConfig",
@@ -173,6 +175,36 @@ class WebConfig(_Strict):
return None if folder is None else _resolve_folder(folder, info, must_exist=False)
class HaDeviceConfig(_Strict):
"""One Home Assistant entity to expose to the room-control page ("Mein Zimmer")."""
entity_id: str
name: str | None = None
class HaConfig(_Strict):
"""Home Assistant integration for the room-control page ("Mein Zimmer").
The backend never calls Home Assistant itself - it only hands the browser the
server URL, the token, and these two ordered lists. Control happens directly from
the browser to Home Assistant's own REST API, so this token grants full HA control
to anything on the LAN that can reach musicmouse. See config.yml.example.
"""
url: str
token: str
#: Order is preserved and drives the device card grid on the room page.
devices: list[HaDeviceConfig] = Field(default_factory=list)
#: Order is preserved and drives the scene pill row on the room page.
scenes: list[HaDeviceConfig] = Field(default_factory=list)
@model_validator(mode="after")
def _check_something_configured(self) -> Self:
if not self.devices and not self.scenes:
raise ValueError("configure at least one device or scene, or omit the ha section")
return self
class GeneralConfig(_Strict):
library: LibraryConfig
@@ -188,6 +220,7 @@ class GeneralConfig(_Strict):
mqtt: MqttConfig | None = None
web: WebConfig | None = None
ha: HaConfig | None = None
min_volume: int = Field(default=0, ge=0, le=200)
max_volume: int = Field(default=100, ge=0, le=200)