279 lines
8.2 KiB
JavaScript
279 lines
8.2 KiB
JavaScript
/**
|
|
* 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<object>} 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<any>} 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<Array<{device_id: string, name: string, type: string, features: object}>>}
|
|
*/
|
|
async getDevices() {
|
|
return await this.fetch(this.api('/devices'));
|
|
}
|
|
|
|
async getDevice() {
|
|
return await this.fetch(this.api(`/devices/${deviceId}`));
|
|
}
|
|
|
|
/**
|
|
* Get current state of a specific device
|
|
* @param {string} deviceId - Device ID
|
|
* @returns {Promise<object>} Device state
|
|
*/
|
|
async getDeviceState(deviceId) {
|
|
return await this.fetch(this.api(`/devices/${deviceId}/state`));
|
|
}
|
|
|
|
/**
|
|
* Get all device states at once
|
|
* @returns {Promise<object>} 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<void>}
|
|
*/
|
|
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<Array<{scene_id: string, name: string, devices: object}>>}
|
|
*/
|
|
async getScenes() {
|
|
return await this.fetch(this.api('/scenes'));
|
|
}
|
|
|
|
/**
|
|
* Activate a scene
|
|
* @param {string} sceneId - Scene ID
|
|
* @returns {Promise<void>}
|
|
*/
|
|
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);
|
|
}
|