/** * Home Automation API Client * * Provides a unified interface to interact with the backend API. * All functions use the global window.API_BASE configuration. */ class HomeAutomationClient { /** * Get layout info for a specific device * @param {string} deviceId - Device ID * @returns {Promise} Layout info */ async getDeviceLayout(deviceId) { return await this.fetch(this.api(`/devices/${deviceId}/layout`)); } constructor() { this.eventSource = null; this.eventListeners = []; } /** * Helper to construct full API URLs * Reads window.API_BASE at runtime to support dynamic configuration * @param {string} path - API path (e.g., '/devices') * @returns {string} Full URL */ api(path) { const baseUrl = window.API_BASE || ''; return `${baseUrl}${path}`; } /** * Generic fetch wrapper with error handling * @param {string} url - URL to fetch * @param {object} options - Fetch options * @returns {Promise} Response data */ async fetch(url, options = {}) { try { const response = await fetch(url, { ...options, headers: { 'Content-Type': 'application/json', ...options.headers } }); if (!response.ok) { throw new Error(`API error: ${response.status} ${response.statusText}`); } return await response.json(); } catch (error) { console.error('API request failed:', error); throw error; } } /** * Get layout data (rooms and their devices) * @returns {Promise<{rooms: Array<{name: string, devices: string[]}>}>} */ async getLayout() { return await this.fetch(this.api('/layout')); } /** * Get all devices with their features * @returns {Promise>} */ async getDevices() { return await this.fetch(this.api('/devices')); } async getDevice(deviceId) { return await this.fetch(this.api(`/devices/${deviceId}`)); } /** * Get current state of a specific device * @param {string} deviceId - Device ID * @returns {Promise} Device state */ async getDeviceState(deviceId) { return await this.fetch(this.api(`/devices/${deviceId}/state`)); } /** * Get all device states at once * @returns {Promise} Map of device_id to state */ async getAllStates() { return await this.fetch(this.api('/devices/states')); } /** * Send a command to a device * @param {string} deviceId - Device ID * @param {string} type - Device type (light, thermostat, etc.) * @param {object} payload - Command payload * @returns {Promise} */ async setDeviceState(deviceId, type, payload) { await fetch(this.api(`/devices/${deviceId}/set`), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ type, payload }) }); } /** * Get room information for a device * @param {string} deviceId - Device ID * @returns {Promise<{room: string}>} */ async getDeviceRoom(deviceId) { return await this.fetch(this.api(`/devices/${deviceId}/room`)); } /** * Get all available scenes * @returns {Promise>} */ async getScenes() { return await this.fetch(this.api('/scenes')); } /** * Activate a scene * @param {string} sceneId - Scene ID * @returns {Promise} */ async activateScene(sceneId) { await fetch(this.api(`/scenes/${sceneId}/activate`), { method: 'POST' }); } /** * Connect to realtime event stream (SSE) * @param {Function} onEvent - Callback function(event) * @param {Function} onError - Error callback (optional) * @returns {EventSource} EventSource instance */ connectRealtime(onEvent, onError = null) { if (this.eventSource) { this.eventSource.close(); } this.eventSource = new EventSource(this.api('/realtime')); this.eventSource.onmessage = (event) => { try { const data = JSON.parse(event.data); // Normalize event format: convert API format to unified format const normalizedEvent = { device_id: data.device_id, type: data.type, state: data.payload || data.state // Support both formats }; onEvent(normalizedEvent); // Notify all registered listeners this.eventListeners.forEach(listener => { if (!listener.deviceId || listener.deviceId === normalizedEvent.device_id) { listener.callback(normalizedEvent); } }); } catch (error) { console.error('Failed to parse SSE event:', error); } }; this.eventSource.onerror = (error) => { console.error('SSE connection error:', error); if (onError) { onError(error); } // Auto-reconnect after 5 seconds setTimeout(() => { if (this.eventSource) { this.eventSource.close(); this.connectRealtime(onEvent, onError); } }, 5000); }; return this.eventSource; } /** * Register a listener for specific device updates * @param {string|null} deviceId - Device ID or null for all devices * @param {Function} callback - Callback function(event) * @returns {Function} Unsubscribe function */ onDeviceUpdate(deviceId, callback) { const listener = { deviceId, callback }; this.eventListeners.push(listener); // Return unsubscribe function return () => { const index = this.eventListeners.indexOf(listener); if (index > -1) { this.eventListeners.splice(index, 1); } }; } /** * Disconnect from realtime stream */ disconnectRealtime() { if (this.eventSource) { this.eventSource.close(); this.eventSource = null; } this.eventListeners = []; } /** * Helper: Get device by ID from devices array * @param {Array} devices - Devices array * @param {string} deviceId - Device ID to find * @returns {object|null} Device object or null */ findDevice(devices, deviceId) { return devices.find(d => d.device_id === deviceId) || null; } /** * Helper: Get room by name from layout * @param {object} layout - Layout object * @param {string} roomName - Room name to find * @returns {object|null} Room object or null */ findRoom(layout, roomName) { return layout.rooms.find(r => r.name === roomName) || null; } /** * Helper: Get devices for a specific room * @param {object} layout - Layout object * @param {Array} devices - Devices array * @param {string} roomName - Room name * @returns {Array} Array of device objects */ getDevicesForRoom(layout, devices, roomName) { const room = this.findRoom(layout, roomName); if (!room) return []; const deviceMap = {}; devices.forEach(d => deviceMap[d.device_id] = d); // Extract device IDs from room.devices (they are objects with device_id property) const deviceIds = room.devices.map(d => d.device_id || d); return deviceIds .map(id => deviceMap[id]) .filter(d => d != null); } } // Create global instance window.apiClient = new HomeAutomationClient(); /** * Convenience function for backward compatibility */ function api(url) { return window.apiClient.api(url); }