UNPKG

homebridge-gira-client

Version:

Homebridge Plugin für Gira Homeserver 4 mit automatischer Geräteerkennung über IoT REST API

345 lines 12.1 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.QuoadClient = void 0; const ws_1 = __importDefault(require("ws")); const crypto_1 = require("crypto"); const types_1 = require("./types"); class QuoadClient { constructor(config, log) { this.config = config; this.log = log; this.ws = null; this.connected = false; this.authenticated = false; this.messageId = 0; this.pendingRequests = new Map(); this.devices = new Map(); this.reconnectTimer = null; this.heartbeatTimer = null; this.connectionConfig = { host: config.host, port: config.port || 80, username: config.username, password: config.password, secure: false, }; } async connect() { if (this.connected) { return; } return new Promise((resolve, reject) => { const protocol = this.connectionConfig.secure ? 'wss' : 'ws'; const url = `${protocol}://${this.connectionConfig.host}:${this.connectionConfig.port}/ws`; this.log.debug(`Connecting to Gira Homeserver at ${url}`); this.ws = new ws_1.default(url, { perMessageDeflate: false, handshakeTimeout: 10000, }); this.ws.on('open', async () => { this.log.info('WebSocket connection established'); this.connected = true; try { await this.authenticate(); this.setupHeartbeat(); resolve(); } catch (error) { this.log.error('Authentication failed:', error); reject(error); } }); this.ws.on('message', (data) => { this.handleMessage(data.toString()); }); this.ws.on('close', (code) => { this.log.warn(`WebSocket connection closed with code ${code}`); this.handleDisconnection(); }); this.ws.on('error', (error) => { this.log.error('WebSocket error:', error); this.handleDisconnection(); reject(error); }); setTimeout(() => { if (!this.connected) { reject(new Error('Connection timeout')); } }, 15000); }); } async authenticate() { const challenge = await this.sendRequest('get_challenge'); const hash = (0, crypto_1.createHash)('sha256'); hash.update(this.connectionConfig.password + challenge.result); const hashedPassword = hash.digest('hex'); const authResult = await this.sendRequest('authenticate', { username: this.connectionConfig.username, password: hashedPassword, }); if (authResult.result !== 'success') { throw new Error('Authentication failed'); } this.authenticated = true; this.log.info('Successfully authenticated with Gira Homeserver'); } handleMessage(data) { try { const message = JSON.parse(data); if (this.config.debugMode) { this.log.debug('Received message:', JSON.stringify(message, null, 2)); } if (message.type === 'response' && message.id) { this.handleResponse(message); } else if (message.type === 'event') { this.handleEvent(message); } } catch (error) { this.log.error('Error parsing message:', error); } } handleResponse(message) { const pendingRequest = this.pendingRequests.get(message.id); if (pendingRequest) { clearTimeout(pendingRequest.timeout); this.pendingRequests.delete(message.id); if (message.error) { pendingRequest.reject(new Error(`Quoad error: ${message.error.message}`)); } else { pendingRequest.resolve(message); } } } handleEvent(message) { if (message.method === 'value_changed' && message.params) { const update = { deviceId: message.params.device_id, functionId: message.params.function_id, value: message.params.value, timestamp: new Date(), }; this.updateDeviceValue(update); } } updateDeviceValue(update) { const device = this.devices.get(update.deviceId); if (device) { const func = device.functions.find(f => f.id === update.functionId); if (func) { func.value = update.value; this.log.debug(`Updated device ${device.name}, function ${func.name}: ${update.value}`); } } } async sendRequest(method, params) { if (!this.connected || !this.ws) { throw new Error('Not connected to Homeserver'); } const id = (++this.messageId).toString(); const message = { type: 'request', id, method, params, }; return new Promise((resolve, reject) => { const timeout = setTimeout(() => { this.pendingRequests.delete(id); reject(new Error(`Request timeout for method: ${method}`)); }, 10000); this.pendingRequests.set(id, { resolve, reject, timeout }); if (this.config.debugMode) { this.log.debug('Sending request:', JSON.stringify(message, null, 2)); } this.ws.send(JSON.stringify(message)); }); } async getDevices() { const response = await this.sendRequest('get_devices'); const deviceList = response.result?.devices || []; this.devices.clear(); for (const deviceData of deviceList) { const device = await this.parseDevice(deviceData); if (device) { this.devices.set(device.id, device); } } return Array.from(this.devices.values()); } async parseDevice(deviceData) { try { const functions = []; if (deviceData.functions) { for (const funcData of deviceData.functions) { const func = this.parseFunction(funcData); if (func) { functions.push(func); } } } const device = { id: deviceData.id, name: deviceData.name || `Device ${deviceData.id}`, type: deviceData.type || 'unknown', room: deviceData.room, functions, properties: deviceData.properties || {}, }; return device; } catch (error) { this.log.error(`Error parsing device ${deviceData.id}:`, error); return null; } } parseFunction(funcData) { try { const func = { id: funcData.id, name: funcData.name || `Function ${funcData.id}`, type: this.mapFunctionType(funcData.type), value: funcData.value, writable: funcData.writable !== false, dataType: funcData.data_type || 'unknown', unit: funcData.unit, min: funcData.min, max: funcData.max, step: funcData.step, options: funcData.options, }; return func; } catch (error) { this.log.error(`Error parsing function ${funcData.id}:`, error); return null; } } mapFunctionType(type) { switch (type?.toLowerCase()) { case 'switching': case 'switch': return types_1.QuoadFunctionType.SWITCHING; case 'dimming': case 'dimmer': return types_1.QuoadFunctionType.DIMMING; case 'blinds': case 'shutter': return types_1.QuoadFunctionType.BLINDS; case 'temperature': return types_1.QuoadFunctionType.TEMPERATURE; case 'humidity': return types_1.QuoadFunctionType.HUMIDITY; case 'heating': case 'thermostat': return types_1.QuoadFunctionType.HEATING; case 'scene': return types_1.QuoadFunctionType.SCENE; case 'sensor': return types_1.QuoadFunctionType.SENSOR; case 'weather': return types_1.QuoadFunctionType.WEATHER; case 'energy': return types_1.QuoadFunctionType.ENERGY; default: return types_1.QuoadFunctionType.UNKNOWN; } } async setDeviceValue(deviceId, functionId, value) { await this.sendRequest('set_value', { device_id: deviceId, function_id: functionId, value: value, }); this.log.debug(`Set device ${deviceId}, function ${functionId} to ${value}`); } async refreshDeviceStates() { if (!this.authenticated) { return; } try { await this.sendRequest('refresh_states'); this.log.debug('Refreshed device states'); } catch (error) { this.log.error('Error refreshing device states:', error); } } setupHeartbeat() { this.heartbeatTimer = setInterval(async () => { try { await this.sendRequest('ping'); } catch (error) { this.log.error('Heartbeat failed:', error); this.handleDisconnection(); } }, 60000); } handleDisconnection() { this.connected = false; this.authenticated = false; if (this.heartbeatTimer) { clearInterval(this.heartbeatTimer); this.heartbeatTimer = null; } for (const [id, request] of this.pendingRequests) { clearTimeout(request.timeout); request.reject(new Error('Connection lost')); } this.pendingRequests.clear(); if (this.ws) { this.ws.removeAllListeners(); this.ws = null; } this.scheduleReconnect(); } scheduleReconnect() { if (this.reconnectTimer) { return; } this.log.info('Scheduling reconnection in 30 seconds...'); this.reconnectTimer = setTimeout(async () => { this.reconnectTimer = null; try { await this.connect(); this.log.info('Reconnected successfully'); } catch (error) { this.log.error('Reconnection failed:', error); } }, 30000); } disconnect() { this.log.info('Disconnecting from Gira Homeserver'); if (this.reconnectTimer) { clearTimeout(this.reconnectTimer); this.reconnectTimer = null; } if (this.heartbeatTimer) { clearInterval(this.heartbeatTimer); this.heartbeatTimer = null; } if (this.ws) { this.ws.close(); } this.connected = false; this.authenticated = false; } isConnected() { return this.connected && this.authenticated; } getDevice(deviceId) { return this.devices.get(deviceId); } getAllDevices() { return Array.from(this.devices.values()); } } exports.QuoadClient = QuoadClient; //# sourceMappingURL=quoad-client.js.map