UNPKG

lib-comfoair

Version:

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

171 lines (170 loc) 7.72 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ComfoControlTransport = void 0; const node_net_1 = require("node:net"); const node_events_1 = require("node:events"); const index_1 = require("./util/logging/index"); const comfoControlMessage_1 = require("./comfoControlMessage"); const comfoConnect_1 = require("./protocol/comfoConnect"); const opcodes_1 = require("./opcodes"); const consts_1 = require("./consts"); const comfoControlHeader_1 = require("./comfoControlHeader"); var ConnectionState; (function (ConnectionState) { ConnectionState[ConnectionState["DISCONNECTED"] = 0] = "DISCONNECTED"; ConnectionState[ConnectionState["CONNECTING"] = 1] = "CONNECTING"; ConnectionState[ConnectionState["CONNECTED"] = 2] = "CONNECTED"; })(ConnectionState || (ConnectionState = {})); /** * The ComfoControlTransport class is responsible for managing the connection to the ComfoControl device on the network. * It sends and receives messages to the device and emits events when messages are received. * Events: * - connect: emitted when the connection to the device is established * - message: emitted when a message is received from the device - the message is a ComfoControlMessage instance * - disconnect: emitted when the connection to the device is closed - the underlying socket is closed * */ class ComfoControlTransport extends node_events_1.EventEmitter { options; logger; socket = null; messageId = 0; clientUuid; keepAlive; state = ConnectionState.DISCONNECTED; keepAliveHandle = null; get isConnected() { return this.state === ConnectionState.CONNECTED; } get isConnecting() { return this.state === ConnectionState.CONNECTING; } /** * Create a new device instance with the specified details. * Use the static discover method to find devices on the network if you do not have the details. */ constructor(options, logger = new index_1.Logger('ComfoControlTransport')) { super(); this.options = options; this.logger = logger; if (options.clientUuid && options.clientUuid.length > 32) { throw new Error('Client ID too long, must be a 32 characters hex string'); } if (!options.uuid) { throw new Error('ComfoControl Server UUID is required to start the connection'); } this.clientUuid = options.clientUuid ?? consts_1.CLIENT_UUID; this.keepAlive = Math.max(options.keepAliveInterval ?? 30000, 5000); } disconnect() { this.socket?.destroySoon(); } async connect() { if (this.state !== ConnectionState.DISCONNECTED) { throw new Error('Cannot connect a transport that is already connected or connecting'); } this.state = ConnectionState.CONNECTING; return new Promise((resolve, reject) => { const socket = new node_net_1.Socket(); const onConnectError = (err) => { this.logger.error('Error connecting transport:', err); this.state = ConnectionState.DISCONNECTED; this.socket = null; reject(err); }; const onConnectSuccess = () => { this.logger.info(`Connected to ${this.options.address} on port ${this.options.port}`); socket.off('error', onConnectError); socket.on('data', this.onSocketData.bind(this)); socket.on('error', this.onSocketError.bind(this)); socket.on('close', this.onSocketClose.bind(this)); socket.on('timeout', this.onSocketTimeout.bind(this)); if (this.keepAlive > 0) { // Transport layer will try to keep the connection alive by sending keep-alive messages this.logger.info(`Starting keep-alive interval every ${this.keepAlive}ms`); this.keepAliveHandle = setInterval(this.sendKeepAlive.bind(this), this.keepAlive); this.socket?.setKeepAlive(true, Math.max(this.keepAlive, 15000)); } else { this.logger.info('Keep-alive disabled'); } this.state = ConnectionState.CONNECTED; this.socket = socket; this.emit('connect'); resolve(this); }; // Connect and listen for connection events socket.once('error', onConnectError); socket.once('connect', onConnectSuccess); socket.connect(this.options.port ?? consts_1.GATEWAY_PORT, this.options.address); }); } send(opcode, data) { if (this.state !== ConnectionState.CONNECTED || this.socket === null) { throw new Error('Cannot send data on a disconnected socket; connect the transport first before calling send'); } const refId = ++this.messageId; const messageBuffer = this.prepareMessage(opcode, refId, data); this.logger.verbose(`Send ${comfoConnect_1.Opcode[opcode]} (${refId}) >>`, () => JSON.stringify(data)); this.logger.debug(`Send ${comfoConnect_1.Opcode[opcode]} (${refId}) >>`, () => messageBuffer.toString('hex')); const writtenLength = messageBuffer.readUint32BE(0) + 4; if (writtenLength !== messageBuffer.length) { throw new Error(`Failed to write message length to buffer; expected ${messageBuffer.length} but got ${writtenLength}`); } return new Promise((resolve, reject) => { this.socket.write(messageBuffer, (err) => { if (err) { this.logger.error('Send error >>', err); reject(err); } else { resolve(refId); } }); }); } prepareMessage(opcode, id, data) { if (!opcodes_1.opcodes[opcode]) { throw new Error(`Unsupported opcode: ${comfoConnect_1.Opcode[opcode]}`); } const message = opcodes_1.opcodes[opcode].toBinary.bind(opcodes_1.opcodes[opcode])(data); const operation = comfoConnect_1.GatewayOperation.toBinary({ opcode, id }); const header = new comfoControlHeader_1.ComfoControlHeader(this.clientUuid, this.options.uuid, operation.length, message.length); return Buffer.concat([header.toBinary(), operation, message]); } onSocketData(data) { this.logger.debug('Recv >>', () => data.toString('hex')); try { const messages = comfoControlMessage_1.ComfoControlMessage.fromBinary(data); messages.forEach((message) => { this.logger.verbose(`Recv ${message.opcodeName} (${message.id}) >>`, () => JSON.stringify(message.deserialize())); this.emit('message', message); }); } catch (err) { this.logger.error('Error processing message:', err); } } onSocketClose() { this.logger.info('Transport socket disconnected'); if (this.keepAliveHandle) { clearInterval(this.keepAliveHandle); } this.state = ConnectionState.DISCONNECTED; this.socket = null; this.emit('disconnect'); } onSocketError(err) { this.logger.error('Transport error:', err); } onSocketTimeout() { this.logger.error('Transport timeout'); this.socket?.destroy(); } sendKeepAlive() { this.send(comfoConnect_1.Opcode.KEEP_ALIVE, {}).catch((err) => { this.logger.error('Error sending keep-alive:', err); }); } } exports.ComfoControlTransport = ComfoControlTransport;