/** * HOMECORE API client. * * REST: fetch-based, bearer token auth. Base URL defaults to window.location.origin * so the Vite dev-server proxy handles the `/api` → `:8123` rewrite. * WS: native WebSocket, mirrors HA's ws handshake protocol (auth_required → auth → auth_ok). */ import type { ApiConfig, ServiceDomainView, StateView, WsAuthOk, WsAuthRequired, WsServerMessage, } from './types.js'; export interface ClientOptions { baseUrl?: string; token: string; } export class HomecoreClient { private readonly base: string; private readonly token: string; constructor(options: ClientOptions) { this.base = options.baseUrl ?? ''; this.token = options.token; } // ── REST helpers ──────────────────────────────────────────────────────────── private headers(): HeadersInit { return { 'Authorization': `Bearer ${this.token}`, 'Content-Type': 'application/json', }; } private async get(path: string): Promise { const resp = await fetch(`${this.base}${path}`, { method: 'GET', headers: this.headers(), }); if (!resp.ok) { throw new Error(`GET ${path} → ${resp.status} ${resp.statusText}`); } return resp.json() as Promise; } private async post(path: string, body: unknown): Promise { const resp = await fetch(`${this.base}${path}`, { method: 'POST', headers: this.headers(), body: JSON.stringify(body), }); if (!resp.ok) { throw new Error(`POST ${path} → ${resp.status} ${resp.statusText}`); } return resp.json() as Promise; } // ── REST endpoints (mirrors rest.rs) ───────────────────────────────────── getConfig(): Promise { return this.get('/api/config'); } getStates(): Promise { return this.get('/api/states'); } getState(entityId: string): Promise { return this.get(`/api/states/${encodeURIComponent(entityId)}`); } setState(entityId: string, state: string, attributes?: Record): Promise { return this.post(`/api/states/${encodeURIComponent(entityId)}`, { state, attributes: attributes ?? {}, }); } getServices(): Promise { return this.get('/api/services'); } callService(domain: string, service: string, data?: unknown): Promise { return this.post(`/api/services/${domain}/${service}`, data ?? {}); } // ── WebSocket ──────────────────────────────────────────────────────────── /** * Open an authenticated WebSocket connection. * Resolves once `auth_ok` is received; rejects on auth failure or network error. * Returns the live socket; caller is responsible for `.close()`. */ openWebSocket(wsBase?: string): Promise { const resolved = wsBase ?? this.base.replace(/^http/, 'ws'); const origin = resolved || window.location.origin.replace(/^http/, 'ws'); const url = `${origin}/api/websocket`; return new Promise((resolve, reject) => { const ws = new WebSocket(url); ws.onmessage = (evt: MessageEvent) => { const msg = JSON.parse(evt.data) as WsServerMessage; if ((msg as WsAuthRequired).type === 'auth_required') { ws.send(JSON.stringify({ type: 'auth', access_token: this.token })); return; } if ((msg as WsAuthOk).type === 'auth_ok') { ws.onmessage = null; resolve(ws); return; } if (msg.type === 'auth_invalid') { ws.close(); reject(new Error(`WS auth_invalid`)); } }; ws.onerror = () => reject(new Error('WebSocket connection error')); ws.onclose = () => reject(new Error('WebSocket closed before auth_ok')); }); } }