new ui 6
This commit is contained in:
301
apps/ui/static/README.md
Normal file
301
apps/ui/static/README.md
Normal file
@@ -0,0 +1,301 @@
|
|||||||
|
# Home Automation API Client
|
||||||
|
|
||||||
|
Wiederverwendbare JavaScript-API-Client-Bibliothek für das Home Automation UI.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Füge die folgenden Script-Tags in deine HTML-Seiten ein:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<script src="/static/types.js"></script>
|
||||||
|
<script src="/static/api-client.js"></script>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Konfiguration
|
||||||
|
|
||||||
|
Der API-Client nutzt `window.API_BASE`, das vom Backend gesetzt wird:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
window.API_BASE = '{{ api_base }}'; // Jinja2 template
|
||||||
|
```
|
||||||
|
|
||||||
|
## Verwendung
|
||||||
|
|
||||||
|
### Globale Instanz
|
||||||
|
|
||||||
|
Der API-Client erstellt automatisch eine globale Instanz `window.apiClient`:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Layout abrufen
|
||||||
|
const layout = await window.apiClient.getLayout();
|
||||||
|
|
||||||
|
// Geräte abrufen
|
||||||
|
const devices = await window.apiClient.getDevices();
|
||||||
|
|
||||||
|
// Gerätestatus abrufen
|
||||||
|
const state = await window.apiClient.getDeviceState('kitchen_light');
|
||||||
|
|
||||||
|
// Gerätesteuerung
|
||||||
|
await window.apiClient.setDeviceState('kitchen_light', 'light', {
|
||||||
|
power: true,
|
||||||
|
brightness: 80
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Verfügbare Methoden
|
||||||
|
|
||||||
|
#### `getLayout(): Promise<Layout>`
|
||||||
|
Lädt die Layout-Daten (Räume und ihre Geräte).
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const layout = await window.apiClient.getLayout();
|
||||||
|
// { rooms: [{name: "Küche", devices: ["kitchen_light", ...]}, ...] }
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `getDevices(): Promise<Device[]>`
|
||||||
|
Lädt alle Geräte mit ihren Features.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const devices = await window.apiClient.getDevices();
|
||||||
|
// [{device_id: "...", name: "...", type: "light", features: {...}}, ...]
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `getDeviceState(deviceId): Promise<DeviceState>`
|
||||||
|
Lädt den aktuellen Status eines Geräts.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const state = await window.apiClient.getDeviceState('kitchen_light');
|
||||||
|
// {power: true, brightness: 80, ...}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `getAllStates(): Promise<Object>`
|
||||||
|
Lädt alle Gerätestatus auf einmal.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const states = await window.apiClient.getAllStates();
|
||||||
|
// {"kitchen_light": {power: true, ...}, "thermostat_1": {...}, ...}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `setDeviceState(deviceId, type, payload): Promise<void>`
|
||||||
|
Sendet einen Befehl an ein Gerät.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Licht einschalten
|
||||||
|
await window.apiClient.setDeviceState('kitchen_light', 'light', {
|
||||||
|
power: true,
|
||||||
|
brightness: 80
|
||||||
|
});
|
||||||
|
|
||||||
|
// Thermostat einstellen
|
||||||
|
await window.apiClient.setDeviceState('thermostat_1', 'thermostat', {
|
||||||
|
target_temp: 22.5
|
||||||
|
});
|
||||||
|
|
||||||
|
// Rollladen steuern
|
||||||
|
await window.apiClient.setDeviceState('cover_1', 'cover', {
|
||||||
|
position: 50
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `getDeviceRoom(deviceId): Promise<{room: string}>`
|
||||||
|
Ermittelt den Raum eines Geräts.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const { room } = await window.apiClient.getDeviceRoom('kitchen_light');
|
||||||
|
// {room: "Küche"}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `getScenes(): Promise<Scene[]>`
|
||||||
|
Lädt alle verfügbaren Szenen.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const scenes = await window.apiClient.getScenes();
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `activateScene(sceneId): Promise<void>`
|
||||||
|
Aktiviert eine Szene.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
await window.apiClient.activateScene('evening');
|
||||||
|
```
|
||||||
|
|
||||||
|
### Realtime-Updates (SSE)
|
||||||
|
|
||||||
|
#### `connectRealtime(onEvent, onError): EventSource`
|
||||||
|
Verbindet sich mit dem SSE-Stream für Live-Updates.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
window.apiClient.connectRealtime(
|
||||||
|
(event) => {
|
||||||
|
console.log('Update:', event.device_id, event.state);
|
||||||
|
// event = {device_id: "...", type: "state", state: {...}}
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
console.error('Connection error:', error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `onDeviceUpdate(deviceId, callback): Function`
|
||||||
|
Registriert einen Listener für spezifische Geräte-Updates.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Für ein bestimmtes Gerät
|
||||||
|
const unsubscribe = window.apiClient.onDeviceUpdate('kitchen_light', (event) => {
|
||||||
|
console.log('Kitchen light changed:', event.state);
|
||||||
|
updateUI(event.state);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Für alle Geräte
|
||||||
|
const unsubscribeAll = window.apiClient.onDeviceUpdate(null, (event) => {
|
||||||
|
console.log('Any device changed:', event.device_id, event.state);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Später: Listener entfernen
|
||||||
|
unsubscribe();
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `disconnectRealtime(): void`
|
||||||
|
Trennt die SSE-Verbindung und entfernt alle Listener.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
window.apiClient.disconnectRealtime();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Helper-Methoden
|
||||||
|
|
||||||
|
#### `findDevice(devices, deviceId): Device|null`
|
||||||
|
Findet ein Gerät in einem Array.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const devices = await window.apiClient.getDevices();
|
||||||
|
const device = window.apiClient.findDevice(devices, 'kitchen_light');
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `findRoom(layout, roomName): Room|null`
|
||||||
|
Findet einen Raum im Layout.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const layout = await window.apiClient.getLayout();
|
||||||
|
const room = window.apiClient.findRoom(layout, 'Küche');
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `getDevicesForRoom(layout, devices, roomName): Device[]`
|
||||||
|
Gibt alle Geräte eines Raums zurück.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const layout = await window.apiClient.getLayout();
|
||||||
|
const devices = await window.apiClient.getDevices();
|
||||||
|
const kitchenDevices = window.apiClient.getDevicesForRoom(layout, devices, 'Küche');
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `api(path): string`
|
||||||
|
Konstruiert eine vollständige API-URL.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const url = window.apiClient.api('/devices');
|
||||||
|
// "http://172.19.1.11:8001/devices"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Backward Compatibility
|
||||||
|
|
||||||
|
Die globale `api()` Funktion ist weiterhin verfügbar:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function api(url) {
|
||||||
|
return window.apiClient.api(url);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Typen (JSDoc)
|
||||||
|
|
||||||
|
Die Datei `types.js` enthält JSDoc-Definitionen für alle API-Typen:
|
||||||
|
|
||||||
|
- `Room` - Raum mit Geräten
|
||||||
|
- `Layout` - Layout-Struktur
|
||||||
|
- `Device` - Gerätedaten
|
||||||
|
- `DeviceFeatures` - Geräte-Features
|
||||||
|
- `DeviceState` - Gerätestatus (Light, Thermostat, Contact, etc.)
|
||||||
|
- `RealtimeEvent` - SSE-Event-Format
|
||||||
|
- `Scene` - Szenen-Definition
|
||||||
|
- `*Payload` - Command-Payloads für verschiedene Gerätetypen
|
||||||
|
|
||||||
|
Diese ermöglichen IDE-Autocomplete und Type-Checking in modernen Editoren (VS Code, WebStorm).
|
||||||
|
|
||||||
|
## Beispiel: Vollständige Seite
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>My Page</title>
|
||||||
|
<script src="/static/types.js"></script>
|
||||||
|
<script src="/static/api-client.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="status"></div>
|
||||||
|
<button id="toggle">Toggle Light</button>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
window.API_BASE = 'http://172.19.1.11:8001';
|
||||||
|
const deviceId = 'kitchen_light';
|
||||||
|
|
||||||
|
async function init() {
|
||||||
|
// Load initial state
|
||||||
|
const state = await window.apiClient.getDeviceState(deviceId);
|
||||||
|
updateUI(state);
|
||||||
|
|
||||||
|
// Listen for updates
|
||||||
|
window.apiClient.onDeviceUpdate(deviceId, (event) => {
|
||||||
|
updateUI(event.state);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Connect to realtime
|
||||||
|
window.apiClient.connectRealtime((event) => {
|
||||||
|
console.log('Event:', event);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle button clicks
|
||||||
|
document.getElementById('toggle').onclick = async () => {
|
||||||
|
const currentState = await window.apiClient.getDeviceState(deviceId);
|
||||||
|
await window.apiClient.setDeviceState(deviceId, 'light', {
|
||||||
|
power: !currentState.power
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateUI(state) {
|
||||||
|
document.getElementById('status').textContent =
|
||||||
|
state.power ? 'ON' : 'OFF';
|
||||||
|
}
|
||||||
|
|
||||||
|
init();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
Alle API-Methoden werfen Exceptions bei Fehlern:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
try {
|
||||||
|
const state = await window.apiClient.getDeviceState('invalid_id');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('API error:', error);
|
||||||
|
showErrorMessage(error.message);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Auto-Reconnect
|
||||||
|
|
||||||
|
Der SSE-Client versucht automatisch, nach 5 Sekunden wieder zu verbinden, wenn die Verbindung abbricht.
|
||||||
|
|
||||||
|
## Verwendete Technologien
|
||||||
|
|
||||||
|
- **Fetch API** - Für HTTP-Requests
|
||||||
|
- **EventSource** - Für Server-Sent Events
|
||||||
|
- **JSDoc** - Für Type Definitions
|
||||||
|
- **ES6+** - Modern JavaScript (Class, async/await, etc.)
|
||||||
262
apps/ui/static/api-client.js
Normal file
262
apps/ui/static/api-client.js
Normal file
@@ -0,0 +1,262 @@
|
|||||||
|
/**
|
||||||
|
* 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 {
|
||||||
|
constructor() {
|
||||||
|
this.baseUrl = window.API_BASE || '';
|
||||||
|
this.eventSource = null;
|
||||||
|
this.eventListeners = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to construct full API URLs
|
||||||
|
* @param {string} path - API path (e.g., '/devices')
|
||||||
|
* @returns {string} Full URL
|
||||||
|
*/
|
||||||
|
api(path) {
|
||||||
|
return `${this.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'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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);
|
||||||
|
|
||||||
|
return room.devices
|
||||||
|
.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);
|
||||||
|
}
|
||||||
166
apps/ui/static/types.js
Normal file
166
apps/ui/static/types.js
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
/**
|
||||||
|
* Type definitions for Home Automation API
|
||||||
|
*
|
||||||
|
* These are JSDoc type definitions that provide IDE autocomplete
|
||||||
|
* and type checking in JavaScript files.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} Room
|
||||||
|
* @property {string} name - Room name (e.g., "Küche", "Wohnzimmer")
|
||||||
|
* @property {string[]} devices - Array of device IDs in this room
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} Layout
|
||||||
|
* @property {Room[]} rooms - Array of rooms with their devices
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} DeviceFeatures
|
||||||
|
* @property {boolean} [dimmable] - Light: supports brightness control
|
||||||
|
* @property {boolean} [color_hsb] - Light: supports HSB color control
|
||||||
|
* @property {boolean} [color_temp] - Light: supports color temperature
|
||||||
|
* @property {number} [min_temp] - Thermostat: minimum temperature
|
||||||
|
* @property {number} [max_temp] - Thermostat: maximum temperature
|
||||||
|
* @property {number} [step] - Thermostat: temperature step size
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} Device
|
||||||
|
* @property {string} device_id - Unique device identifier
|
||||||
|
* @property {string} name - Human-readable device name
|
||||||
|
* @property {string} type - Device type: light, thermostat, contact, temp_humidity_sensor, relay, outlet, cover
|
||||||
|
* @property {DeviceFeatures} features - Device-specific features
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} LightState
|
||||||
|
* @property {boolean} power - On/off state
|
||||||
|
* @property {number} [brightness] - Brightness 0-100 (if dimmable)
|
||||||
|
* @property {Object} [color_hsb] - HSB color (if color_hsb)
|
||||||
|
* @property {number} color_hsb.hue - Hue 0-360
|
||||||
|
* @property {number} color_hsb.saturation - Saturation 0-100
|
||||||
|
* @property {number} color_hsb.brightness - Brightness 0-100
|
||||||
|
* @property {number} [color_temp] - Color temperature in mireds (if color_temp)
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} ThermostatState
|
||||||
|
* @property {number} current_temp - Current temperature in °C
|
||||||
|
* @property {number} target_temp - Target temperature in °C
|
||||||
|
* @property {string} mode - Operating mode: heat, cool, auto, off
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} ContactState
|
||||||
|
* @property {boolean} open - true if open, false if closed
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} TempHumidityState
|
||||||
|
* @property {number} temperature - Temperature in °C
|
||||||
|
* @property {number} humidity - Relative humidity 0-100%
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} RelayState
|
||||||
|
* @property {boolean} power - On/off state
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} OutletState
|
||||||
|
* @property {boolean} power - On/off state
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} CoverState
|
||||||
|
* @property {number} position - Position 0-100 (0=closed, 100=open)
|
||||||
|
* @property {string} state - State: open, closed, opening, closing, stopped
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {LightState|ThermostatState|ContactState|TempHumidityState|RelayState|OutletState|CoverState} DeviceState
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} RealtimeEvent
|
||||||
|
* @property {string} device_id - Device that changed
|
||||||
|
* @property {string} type - Device type
|
||||||
|
* @property {DeviceState} state - New device state
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} Scene
|
||||||
|
* @property {string} scene_id - Unique scene identifier
|
||||||
|
* @property {string} name - Human-readable scene name
|
||||||
|
* @property {Object<string, Object>} devices - Map of device_id to desired state
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} LightPayload
|
||||||
|
* @property {boolean} [power] - Turn on/off
|
||||||
|
* @property {number} [brightness] - Set brightness 0-100
|
||||||
|
* @property {Object} [color_hsb] - Set HSB color
|
||||||
|
* @property {number} color_hsb.hue - Hue 0-360
|
||||||
|
* @property {number} color_hsb.saturation - Saturation 0-100
|
||||||
|
* @property {number} color_hsb.brightness - Brightness 0-100
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} ThermostatPayload
|
||||||
|
* @property {number} [target_temp] - Set target temperature
|
||||||
|
* @property {string} [mode] - Set mode: heat, cool, auto, off
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} RelayPayload
|
||||||
|
* @property {boolean} power - Turn on/off
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} OutletPayload
|
||||||
|
* @property {boolean} power - Turn on/off
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} CoverPayload
|
||||||
|
* @property {number} [position] - Set position 0-100
|
||||||
|
* @property {string} [action] - Action: open, close, stop
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Example usage:
|
||||||
|
*
|
||||||
|
* // Get layout data
|
||||||
|
* const layout = await window.apiClient.getLayout();
|
||||||
|
* // layout is typed as Layout
|
||||||
|
*
|
||||||
|
* // Get devices
|
||||||
|
* const devices = await window.apiClient.getDevices();
|
||||||
|
* // devices is typed as Device[]
|
||||||
|
*
|
||||||
|
* // Get device state
|
||||||
|
* const state = await window.apiClient.getDeviceState('kitchen_light');
|
||||||
|
* // state is typed as DeviceState
|
||||||
|
*
|
||||||
|
* // Set device state
|
||||||
|
* await window.apiClient.setDeviceState('kitchen_light', 'light', {
|
||||||
|
* power: true,
|
||||||
|
* brightness: 80
|
||||||
|
* });
|
||||||
|
*
|
||||||
|
* // Connect to realtime events
|
||||||
|
* window.apiClient.connectRealtime((event) => {
|
||||||
|
* console.log('Device update:', event.device_id, event.state);
|
||||||
|
* });
|
||||||
|
*
|
||||||
|
* // Listen to specific device
|
||||||
|
* const unsubscribe = window.apiClient.onDeviceUpdate('kitchen_light', (event) => {
|
||||||
|
* console.log('Kitchen light changed:', event.state);
|
||||||
|
* });
|
||||||
|
*
|
||||||
|
* // Later: cleanup
|
||||||
|
* unsubscribe();
|
||||||
|
* window.apiClient.disconnectRealtime();
|
||||||
|
*/
|
||||||
@@ -4,6 +4,8 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Gerät - Home Automation</title>
|
<title>Gerät - Home Automation</title>
|
||||||
|
<script src="/static/types.js"></script>
|
||||||
|
<script src="/static/api-client.js"></script>
|
||||||
<style>
|
<style>
|
||||||
* {
|
* {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -290,11 +292,6 @@
|
|||||||
// API configuration from backend
|
// API configuration from backend
|
||||||
window.API_BASE = '{{ api_base }}';
|
window.API_BASE = '{{ api_base }}';
|
||||||
|
|
||||||
// Helper function to construct API URLs
|
|
||||||
function api(url) {
|
|
||||||
return `${window.API_BASE}${url}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get device ID from URL
|
// Get device ID from URL
|
||||||
const pathParts = window.location.pathname.split('/');
|
const pathParts = window.location.pathname.split('/');
|
||||||
const deviceId = pathParts[pathParts.length - 1];
|
const deviceId = pathParts[pathParts.length - 1];
|
||||||
@@ -324,35 +321,25 @@
|
|||||||
const errorContainer = document.getElementById('error-container');
|
const errorContainer = document.getElementById('error-container');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Load device info
|
// Load device info using API client
|
||||||
const devicesResponse = await fetch(api('/devices'));
|
const devicesData = await window.apiClient.getDevices();
|
||||||
if (!devicesResponse.ok) {
|
deviceData = window.apiClient.findDevice(devicesData, deviceId);
|
||||||
throw new Error(`Devices API error: ${devicesResponse.status}`);
|
|
||||||
}
|
|
||||||
const devicesData = await devicesResponse.json();
|
|
||||||
deviceData = devicesData.find(d => d.device_id === deviceId);
|
|
||||||
|
|
||||||
if (!deviceData) {
|
if (!deviceData) {
|
||||||
throw new Error(`Gerät "${deviceId}" nicht gefunden`);
|
throw new Error(`Gerät "${deviceId}" nicht gefunden`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load layout to get room
|
// Load layout to get room
|
||||||
const layoutResponse = await fetch(api('/layout'));
|
const layoutData = await window.apiClient.getLayout();
|
||||||
if (layoutResponse.ok) {
|
for (const room of layoutData.rooms) {
|
||||||
const layoutData = await layoutResponse.json();
|
if (room.devices.includes(deviceId)) {
|
||||||
for (const room of layoutData.rooms) {
|
roomName = room.name;
|
||||||
if (room.devices.includes(deviceId)) {
|
break;
|
||||||
roomName = room.name;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load device state
|
// Load device state
|
||||||
const stateResponse = await fetch(api(`/devices/${deviceId}/state`));
|
deviceState = await window.apiClient.getDeviceState(deviceId);
|
||||||
if (stateResponse.ok) {
|
|
||||||
deviceState = await stateResponse.json();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update header
|
// Update header
|
||||||
document.getElementById('device-icon').textContent = deviceIcons[deviceData.type] || '📱';
|
document.getElementById('device-icon').textContent = deviceIcons[deviceData.type] || '📱';
|
||||||
@@ -679,18 +666,7 @@
|
|||||||
|
|
||||||
async function sendCommand(payload) {
|
async function sendCommand(payload) {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(api(`/devices/${deviceId}/set`), {
|
await window.apiClient.setDeviceState(deviceId, deviceData.type, payload.payload);
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify(payload)
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`API error: ${response.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
showSuccess('Befehl gesendet');
|
showSuccess('Befehl gesendet');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error sending command:', error);
|
console.error('Error sending command:', error);
|
||||||
@@ -738,31 +714,15 @@
|
|||||||
|
|
||||||
function connectRealtime() {
|
function connectRealtime() {
|
||||||
try {
|
try {
|
||||||
eventSource = new EventSource(api('/realtime'));
|
// Use API client's realtime connection
|
||||||
|
window.apiClient.connectRealtime((event) => {
|
||||||
eventSource.onmessage = (event) => {
|
if (event.device_id === deviceId && event.state) {
|
||||||
try {
|
deviceState = { ...deviceState, ...event.state };
|
||||||
const data = JSON.parse(event.data);
|
updateUI();
|
||||||
|
|
||||||
if (data.device_id === deviceId && data.payload) {
|
|
||||||
deviceState = { ...deviceState, ...data.payload };
|
|
||||||
updateUI();
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.warn('Failed to parse SSE event:', e);
|
|
||||||
}
|
}
|
||||||
};
|
}, (error) => {
|
||||||
|
|
||||||
eventSource.onerror = (error) => {
|
|
||||||
console.error('SSE connection error:', error);
|
console.error('SSE connection error:', error);
|
||||||
setTimeout(() => {
|
});
|
||||||
if (eventSource) {
|
|
||||||
eventSource.close();
|
|
||||||
connectRealtime();
|
|
||||||
}
|
|
||||||
}, 5000);
|
|
||||||
};
|
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to connect to realtime events:', error);
|
console.error('Failed to connect to realtime events:', error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,9 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Raum - Home Automation</title>
|
<title>{{ room_name }} - Home Automation</title>
|
||||||
|
<script src="/static/types.js"></script>
|
||||||
|
<script src="/static/api-client.js"></script>
|
||||||
<style>
|
<style>
|
||||||
* {
|
* {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -207,11 +209,6 @@
|
|||||||
// API configuration from backend
|
// API configuration from backend
|
||||||
window.API_BASE = '{{ api_base }}';
|
window.API_BASE = '{{ api_base }}';
|
||||||
|
|
||||||
// Helper function to construct API URLs
|
|
||||||
function api(url) {
|
|
||||||
return `${window.API_BASE}${url}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get room name from URL
|
// Get room name from URL
|
||||||
const pathParts = window.location.pathname.split('/');
|
const pathParts = window.location.pathname.split('/');
|
||||||
const roomName = decodeURIComponent(pathParts[pathParts.length - 1]);
|
const roomName = decodeURIComponent(pathParts[pathParts.length - 1]);
|
||||||
@@ -242,15 +239,12 @@
|
|||||||
const roomInfoEl = document.getElementById('room-info');
|
const roomInfoEl = document.getElementById('room-info');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Load layout
|
// Load layout and devices using API client
|
||||||
const layoutResponse = await fetch(api('/layout'));
|
const layoutData = await window.apiClient.getLayout();
|
||||||
if (!layoutResponse.ok) {
|
const devicesData = await window.apiClient.getDevices();
|
||||||
throw new Error(`Layout API error: ${layoutResponse.status}`);
|
|
||||||
}
|
|
||||||
const layoutData = await layoutResponse.json();
|
|
||||||
|
|
||||||
// Find the room
|
// Find the room using API client helper
|
||||||
const room = layoutData.rooms.find(r => r.name === roomName);
|
const room = window.apiClient.findRoom(layoutData, roomName);
|
||||||
if (!room) {
|
if (!room) {
|
||||||
throw new Error(`Raum "${roomName}" nicht gefunden`);
|
throw new Error(`Raum "${roomName}" nicht gefunden`);
|
||||||
}
|
}
|
||||||
@@ -259,13 +253,6 @@
|
|||||||
roomNameEl.textContent = room.name;
|
roomNameEl.textContent = room.name;
|
||||||
roomInfoEl.textContent = `${room.devices.length} Gerät${room.devices.length !== 1 ? 'e' : ''}`;
|
roomInfoEl.textContent = `${room.devices.length} Gerät${room.devices.length !== 1 ? 'e' : ''}`;
|
||||||
|
|
||||||
// Load devices
|
|
||||||
const devicesResponse = await fetch(api('/devices'));
|
|
||||||
if (!devicesResponse.ok) {
|
|
||||||
throw new Error(`Devices API error: ${devicesResponse.status}`);
|
|
||||||
}
|
|
||||||
const devicesData = await devicesResponse.json();
|
|
||||||
|
|
||||||
// Create device lookup
|
// Create device lookup
|
||||||
const deviceMap = {};
|
const deviceMap = {};
|
||||||
devicesData.forEach(device => {
|
devicesData.forEach(device => {
|
||||||
@@ -310,14 +297,11 @@
|
|||||||
|
|
||||||
async function loadDeviceStates(deviceIds) {
|
async function loadDeviceStates(deviceIds) {
|
||||||
try {
|
try {
|
||||||
// Load states for all devices
|
// Load states for all devices using API client
|
||||||
const statePromises = deviceIds.map(async deviceId => {
|
const statePromises = deviceIds.map(async deviceId => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(api(`/devices/${deviceId}/state`));
|
const state = await window.apiClient.getDeviceState(deviceId);
|
||||||
if (response.ok) {
|
deviceStates[deviceId] = state;
|
||||||
const state = await response.json();
|
|
||||||
deviceStates[deviceId] = state;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn(`Failed to load state for ${deviceId}:`, e);
|
console.warn(`Failed to load state for ${deviceId}:`, e);
|
||||||
}
|
}
|
||||||
@@ -427,7 +411,7 @@
|
|||||||
|
|
||||||
function connectRealtime() {
|
function connectRealtime() {
|
||||||
try {
|
try {
|
||||||
eventSource = new EventSource(api('/realtime'));
|
eventSource = new EventSource(window.apiClient.api('/realtime'));
|
||||||
|
|
||||||
eventSource.onmessage = (event) => {
|
eventSource.onmessage = (event) => {
|
||||||
try {
|
try {
|
||||||
@@ -441,8 +425,8 @@
|
|||||||
const card = document.querySelector(`[data-device-id="${data.device_id}"]`);
|
const card = document.querySelector(`[data-device-id="${data.device_id}"]`);
|
||||||
if (card) {
|
if (card) {
|
||||||
const stateDiv = card.querySelector('.device-state');
|
const stateDiv = card.querySelector('.device-state');
|
||||||
const devicesResponse = fetch(api('/devices')).then(r => r.json()).then(devices => {
|
window.apiClient.getDevices().then(devices => {
|
||||||
const device = devices.find(d => d.device_id === data.device_id);
|
const device = window.apiClient.findDevice(devices, data.device_id);
|
||||||
if (device) {
|
if (device) {
|
||||||
updateDeviceCardState(stateDiv, device);
|
updateDeviceCardState(stateDiv, device);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Räume - Home Automation</title>
|
<title>Räume - Home Automation</title>
|
||||||
|
<script src="/static/types.js"></script>
|
||||||
|
<script src="/static/api-client.js"></script>
|
||||||
<style>
|
<style>
|
||||||
* {
|
* {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -150,11 +152,6 @@
|
|||||||
// API configuration from backend
|
// API configuration from backend
|
||||||
window.API_BASE = '{{ api_base }}';
|
window.API_BASE = '{{ api_base }}';
|
||||||
|
|
||||||
// Helper function to construct API URLs
|
|
||||||
function api(url) {
|
|
||||||
return `${window.API_BASE}${url}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Room icon mapping
|
// Room icon mapping
|
||||||
const roomIcons = {
|
const roomIcons = {
|
||||||
'wohnzimmer': '🛋️',
|
'wohnzimmer': '🛋️',
|
||||||
@@ -193,19 +190,9 @@
|
|||||||
const errorContainer = document.getElementById('error-container');
|
const errorContainer = document.getElementById('error-container');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Load layout
|
// Load layout and devices using API client
|
||||||
const layoutResponse = await fetch(api('/layout'));
|
const layoutData = await window.apiClient.getLayout();
|
||||||
if (!layoutResponse.ok) {
|
const devicesData = await window.apiClient.getDevices();
|
||||||
throw new Error(`Layout API error: ${layoutResponse.status}`);
|
|
||||||
}
|
|
||||||
const layoutData = await layoutResponse.json();
|
|
||||||
|
|
||||||
// Load devices for feature checks
|
|
||||||
const devicesResponse = await fetch(api('/devices'));
|
|
||||||
if (!devicesResponse.ok) {
|
|
||||||
throw new Error(`Devices API error: ${devicesResponse.status}`);
|
|
||||||
}
|
|
||||||
const devicesData = await devicesResponse.json();
|
|
||||||
|
|
||||||
// Create device lookup
|
// Create device lookup
|
||||||
const deviceMap = {};
|
const deviceMap = {};
|
||||||
|
|||||||
Reference in New Issue
Block a user