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:
@@ -1,6 +1,6 @@
|
||||
/** Every call the UI makes. Commands are fire-and-forget: the websocket reports back. */
|
||||
|
||||
import type { Album, PlayerState, Settings } from "./types";
|
||||
import type { Album, HaConfig, HaEntityState, PlayerState, Settings } from "./types";
|
||||
|
||||
async function request<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
const response = await fetch(`/api${path}`, {
|
||||
@@ -20,6 +20,33 @@ function post(path: string, body?: unknown): Promise<void> {
|
||||
});
|
||||
}
|
||||
|
||||
/** `null` means the room-control page isn't configured, not an error - unlike
|
||||
* `request()`, a 404 here is expected and shouldn't throw. */
|
||||
async function fetchHaConfig(): Promise<HaConfig | null> {
|
||||
const response = await fetch("/api/ha");
|
||||
if (response.status === 404) return null;
|
||||
if (!response.ok) throw new Error(`GET /ha failed: ${response.status}`);
|
||||
return (await response.json()) as HaConfig;
|
||||
}
|
||||
|
||||
/** `null` covers both "unknown to Home Assistant" and "Home Assistant unreachable
|
||||
* right now" (the backend answers the latter with a 502) - the room page treats a
|
||||
* device with no state the same way either way, rather than crashing on a poll. */
|
||||
async function fetchHaState(entityId: string): Promise<HaEntityState | null> {
|
||||
const response = await fetch(`/api/ha/states/${entityId}`);
|
||||
if (!response.ok) return null;
|
||||
return (await response.json()) as HaEntityState;
|
||||
}
|
||||
|
||||
async function fetchHaStates(entityIds: string[]): Promise<Record<string, HaEntityState>> {
|
||||
const results = await Promise.all(entityIds.map(fetchHaState));
|
||||
const byId: Record<string, HaEntityState> = {};
|
||||
results.forEach((state, index) => {
|
||||
if (state) byId[entityIds[index]!] = state;
|
||||
});
|
||||
return byId;
|
||||
}
|
||||
|
||||
export const api = {
|
||||
library: () => request<{ albums: Album[] }>("/library").then((body) => body.albums),
|
||||
state: () => request<PlayerState>("/state"),
|
||||
@@ -38,6 +65,11 @@ export const api = {
|
||||
settings: () => request<Settings>("/settings"),
|
||||
saveSettings: (settings: Settings) =>
|
||||
request<Settings>("/settings", { method: "PUT", body: JSON.stringify(settings) }),
|
||||
|
||||
haConfig: fetchHaConfig,
|
||||
haStates: fetchHaStates,
|
||||
haCallService: (domain: string, service: string, body: Record<string, unknown>) =>
|
||||
post(`/ha/services/${domain}/${service}`, body),
|
||||
};
|
||||
|
||||
export const coverUrl = (albumId: string) => `/api/albums/${albumId}/cover`;
|
||||
|
||||
@@ -63,3 +63,23 @@ export type ServerMessage =
|
||||
| { type: "state"; state: PlayerState }
|
||||
| { type: "position"; position: number; duration: number }
|
||||
| { type: "library" };
|
||||
|
||||
export interface HaDevice {
|
||||
entity_id: string;
|
||||
name: string | null;
|
||||
}
|
||||
|
||||
/** No url/token here - the backend proxies Home Assistant calls and keeps the token to
|
||||
* itself, so the browser only ever learns which entities exist. */
|
||||
export interface HaConfig {
|
||||
devices: HaDevice[];
|
||||
scenes: HaDevice[];
|
||||
}
|
||||
|
||||
/** A Home Assistant entity's `state`/`attributes`, relayed byte-for-byte through
|
||||
* `GET /api/ha/states/{entity_id}`. */
|
||||
export interface HaEntityState {
|
||||
entity_id: string;
|
||||
state: string;
|
||||
attributes: Record<string, unknown>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user