@homebridge/hap-client
Version:
A client for HAP-NodeJS.
183 lines • 8.38 kB
JavaScript
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