UNPKG

@homebridge/hap-client

Version:
183 lines 8.38 kB
import { Buffer } from 'node:buffer'; import { EventEmitter } from 'node:events'; import { createConnection, parseMessage } from './eventedHttpClient/index.js'; const MAX_RECV_BUFFER_BYTES = 2 * 1024 * 1024; export function findMessageBoundary(buffer) { const raw = buffer.toString('latin1'); const sepMatch = raw.match(/\r?\n\r?\n/); if (!sepMatch || sepMatch.index === undefined) { return -1; } const headers = raw.slice(0, sepMatch.index); const bodyStart = sepMatch.index + sepMatch[0].length; const contentLengthMatch = headers.match(/Content-Length:\s*(\d+)/i); if (!contentLengthMatch) { return bodyStart; } const messageEnd = bodyStart + Number(contentLengthMatch[1]); if (buffer.length < messageEnd) { return -1; } return messageEnd; } export class HapMonitor extends EventEmitter { pin; evInstances; services; logger; debug; constructor(logger, debug, pin, services) { super(); this.logger = logger; this.debug = debug; this.pin = pin; this.services = services; this.evInstances = []; this.parseServices(); this.start(); } log(message) { this.logger?.log(`[HapMonitor] ${message}`); } error(message) { if (typeof this.logger?.error === 'function') { this.logger.error(`[HapMonitor] ${message}`); } else { this.logger?.log?.(`[HapMonitor] ERROR: ${message}`); } } start() { for (const instance of this.evInstances) { this.connectInstance(instance); } } connectInstance(instance) { try { this.debug(`[HapClient] [${instance.ipAddress}:${instance.port} (${instance.username})] Connecting`); instance.socket = createConnection(instance, this.pin, { characteristics: instance.evCharacteristics }); instance.monitoring = true; instance.recvBuffer = Buffer.alloc(0); this.debug(`[HapClient] [${instance.ipAddress}:${instance.port} (${instance.username})] Connected`); instance.socket.on('data', (data) => { const chunk = Buffer.isBuffer(data) ? data : Buffer.from(data); instance.recvBuffer = instance.recvBuffer?.length ? Buffer.concat([instance.recvBuffer, chunk]) : chunk; while (true) { const boundary = findMessageBoundary(instance.recvBuffer); if (boundary <= 0) { break; } const messageStr = instance.recvBuffer.subarray(0, boundary).toString(); instance.recvBuffer = instance.recvBuffer.subarray(boundary); this.handleEventMessage(instance, messageStr); } if ((instance.recvBuffer?.length ?? 0) > MAX_RECV_BUFFER_BYTES) { this.error(`[${instance.ipAddress}:${instance.port} (${instance.username})] ` + `receive buffer exceeded ${MAX_RECV_BUFFER_BYTES} bytes without a complete message; resetting connection`); instance.recvBuffer = Buffer.alloc(0); instance.socket.destroy(); } }); instance.socket.on('close', (hadError) => { this.emit('monitor-close', instance, hadError); instance.socket.destroy(); instance.socket.removeAllListeners(); this.debug(`[HapClient] [${instance.ipAddress}:${instance.port} (${instance.username})] closed: ${hadError}`); }); instance.socket.on('error', (error) => { this.emit('monitor-error', instance, error); this.debug(`[HapClient] [${instance.ipAddress}:${instance.port} (${instance.username})] error: ${error}`); }); } catch (e) { this.debug(e); this.error(`Monitor Start Error [${instance.ipAddress}:${instance.port} (${instance.username})]: ${e.message}`); } } finish() { for (const instance of this.evInstances) { if (instance.socket) { try { instance.socket.destroy(); instance.socket.removeAllListeners(); this.debug(`[HapClient] [${instance.ipAddress}:${instance.port} (${instance.username})] Disconnected`); } catch { } } } } refreshMonitorConnection(refreshInstance) { this.debug(`[HapClient] [${refreshInstance.ipAddress}:${refreshInstance.port} (${refreshInstance.username})] Refreshing Monitor`); const instance = this.evInstances.find(x => x.username === refreshInstance.username); if (instance) { instance.socket?.destroy(); instance.socket?.removeAllListeners(); instance.port = refreshInstance.port; instance.ipAddress = refreshInstance.ipAddress; this.connectInstance(instance); this.emit('monitor-refresh', instance); } } isInstanceMonitored(username) { const instance = this.evInstances.find(x => x.username === username); return instance?.monitoring === true; } isInstanceConnected(username) { const instance = this.evInstances.find(x => x.username === username); return instance?.socket != null && instance?.socket !== undefined && !instance.socket.destroyed; } handleEventMessage(instance, messageStr) { const message = parseMessage(messageStr); if (message.statusCode === 401) { this.debug(`[HapClient] [${instance.ipAddress}:${instance.port} (${instance.username})] ` + `${message.statusCode} ${message.statusMessage} - make sure Homebridge pin for this instance is set to ${this.pin}.`); } if (message.protocol === 'EVENT') { try { const body = JSON.parse(message.body); if (body.characteristics && body.characteristics.length) { this.debug(`[HapClient] [${instance.ipAddress}:${instance.port} (${instance.username})] ` + `Got Event: ${JSON.stringify(body.characteristics)}`); const response = body.characteristics.map((c) => { const services = this.services.filter(x => x.aid === c.aid && x.instance.username === instance.username); const service = services.find(x => x.serviceCharacteristics.find(y => y.iid === c.iid)); if (service) { const characteristic = service.serviceCharacteristics.find(x => x.iid === c.iid); if (characteristic) { characteristic.value = c.value; service.values[characteristic.type] = c.value; return service; } } return undefined; }); this.emit('service-update', response.filter(x => x)); } } catch { } } } parseServices() { for (const service of this.services) { const evCharacteristics = service.serviceCharacteristics.filter(x => x.perms.includes('ev')); if (evCharacteristics.length) { if (!this.evInstances.some(x => x.username === service.instance.username)) { const newInstance = { ...service.instance }; newInstance.evCharacteristics = []; this.evInstances.push(newInstance); } const instance = this.evInstances.find(x => x.username === service.instance.username); for (const evCharacteristic of evCharacteristics) { if (!instance.evCharacteristics.some(x => x.aid === service.aid && x.iid === evCharacteristic.iid)) { instance.evCharacteristics.push({ aid: service.aid, iid: evCharacteristic.iid, ev: true }); } } } } } } //# sourceMappingURL=monitor.js.map