UNPKG

lib-comfoair

Version:

Library to communicate with Zehnder ComfoAirQ ventilation unit through the ComfoControl gateway

174 lines (173 loc) 7.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.DiscoveryOperation = void 0; const node_dgram_1 = require("node:dgram"); const events_1 = require("events"); const deferredPromise_1 = require("./util/deferredPromise"); const comfoConnect_1 = require("./protocol/comfoConnect"); const index_1 = require("./util/logging/index"); const consts_1 = require("./consts"); /** * The discovery operation to find devices on the network. Sends a discovery message to one or more broadcast address * and listens for responses from devices. Uses UDP sockets to send and receive messages on the default discovery port (56747). * * For the discovery process to work, the devices must be on the same network segment as the host running the discovery operation. * If the network is segmented into multiple subnets, the discovery process will not work unless the router is configured to relay discovery message to other subnets. * By default routers do not relay broadcast messages between subnets. */ class DiscoveryOperation extends events_1.EventEmitter { port; logger; socket; timeoutHandle; broadcastHandle; discoveryPromise; discoveredDevices = []; broadcastAddresses; [Symbol.toStringTag] = 'DiscoveryOperation'; constructor(broadcastAddresses, port = consts_1.DISCOVERY_PORT, logger = new index_1.Logger('DiscoveryOperation')) { super(); this.port = port; this.logger = logger; this.broadcastAddresses = Array.isArray(broadcastAddresses) ? broadcastAddresses : [broadcastAddresses]; if (this.broadcastAddresses.length === 0) { throw new Error('At least one broadcast address must be provided'); } this.socket = (0, node_dgram_1.createSocket)({ type: 'udp4', reuseAddr: true }); } /** * Initiates the discovery process to find devices. * @param timeout - The duration in milliseconds to run the discovery before timing out. * @param abortSignal - Optional AbortSignal to cancel the discovery. * @returns The current instance of deviceDiscoveryOperation. * @throws Error if a discovery operation is already in progress. */ discover(options, abortSignal) { if (this.discoveryPromise) { this.logger.error('Discovery operation already in progress'); throw new Error('Discovery operation already in progress'); } this.logger.info('Starting discovery process'); this.discoveryPromise = new deferredPromise_1.DeferredPromise(); if (abortSignal) { abortSignal.addEventListener('abort', this.onAbort.bind(this)); } this.socket.on('message', (msg, rinfo) => { if (abortSignal?.aborted) { this.onAbort(); return; } this.logger.debug(`Received message from ${rinfo.address}:`, () => msg.toString('hex')); const device = this.parseDiscoveryResponse(msg); if (device && !this.discoveredDevices.some((b) => b.uuid === device.uuid)) { this.discoveredDevices.push(device); this.logger.info('Discovered device at', device.address, 'with UUID:', device.uuid); this.emit('discover', device); if (options.limit && this.discoveredDevices.length >= options.limit) { this.logger.verbose(`Discovery limit (${options.limit}) reached`); this.stop(); } } }); this.socket.on('error', this.onError.bind(this)); this.socket.bind(() => { if (abortSignal?.aborted) { this.onAbort(); return; } this.socket.setBroadcast(true); this.broadcastHandle = setInterval(() => this.sendDiscoveryMessages(), 2000); this.timeoutHandle = setTimeout(this.stop.bind(this), options.timeout); }); return this; } sendDiscoveryMessages() { const message = comfoConnect_1.GatewayDiscovery.toBinary({ request: {} }); for (const address of this.broadcastAddresses) { this.logger.debug(`Broadcast on ${address} (${this.port}):`, () => Buffer.from(message).toString('hex')); this.socket.send(message, 0, message.length, this.port, address, (err) => err && this.onError(err)); } } parseDiscoveryResponse(msg) { try { const { response } = comfoConnect_1.GatewayDiscovery.fromBinary(msg, { readUnknownField: false }); if (!response) { throw new Error('Invalid discovery response'); } const uuid = Buffer.from(response.uuid).toString('hex'); return { address: response.address, port: this.port, version: response.version, uuid, mac: uuid.slice(uuid.length - 12), }; } catch (err) { this.onError(err); } return undefined; } onError(error) { this.logger.error('Error during discovery:', error.message); this.emit('error', error); this.discoveryPromise?.reject(error); this.cleanup(); } onAbort() { this.logger.warn('Discovery aborted'); this.emit('abort'); this.discoveryPromise?.reject(new Error('Discovery aborted')); this.cleanup(); } stop() { this.logger.info('Discovery stopped'); this.discoveryPromise?.resolve(this.discoveredDevices); this.emit('completed', this.discoveryPromise); this.cleanup(); } cleanup() { this.logger.debug('Cleaning up discovery operation'); if (this.timeoutHandle) { clearTimeout(this.timeoutHandle); } clearInterval(this.broadcastHandle); this.discoveryPromise = undefined; this.socket.close(); } /** * Attaches callbacks for the resolution and/or rejection of the Promise. * @param onfulfilled - The callback to execute when the Promise is resolved. * @param onrejected - The callback to execute when the Promise is rejected. * @returns A Promise for the completion of the callback. */ then(onfulfilled, onrejected) { if (!this.discoveryPromise) { return Promise.reject(new Error('No discovery operation in progress')); } return this.discoveryPromise.then(onfulfilled, onrejected); } /** * Attaches a callback for only the rejection of the Promise. * @param onrejected - The callback to execute when the Promise is rejected. * @returns A Promise for the completion of the callback. */ catch(onrejected) { if (!this.discoveryPromise) { return Promise.reject(new Error('No discovery operation in progress')); } return this.discoveryPromise.catch(onrejected); } /** * Attaches a callback that is invoked when the Promise is settled (fulfilled or rejected). * @param onfinally - The callback to execute when the Promise is settled. * @returns A Promise for the completion of the callback. */ finally(onfinally) { if (!this.discoveryPromise) { return Promise.reject(new Error('No discovery operation in progress')); } return this.discoveryPromise.finally(onfinally); } } exports.DiscoveryOperation = DiscoveryOperation;