lib-comfoair
Version:
Library to communicate with Zehnder ComfoAirQ ventilation unit through the ComfoControl gateway
174 lines (173 loc) • 7.5 kB
JavaScript
"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;