homebridge-gira-client
Version:
Homebridge Plugin für Gira Homeserver 4 mit automatischer Geräteerkennung über IoT REST API
345 lines • 12.1 kB
JavaScript
"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