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