UNPKG

knxultimate

Version:

KNX IP protocol implementation for Node. This is the ENGINE of Node-Red KNX-Ultimate node.

1,029 lines 54.8 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.KNXTimer = exports.getDecodedKeyring = exports.KNXClientEvents = exports.SocketEvents = exports.ConncetionState = void 0; const dgram_1 = __importStar(require("dgram")); const net_1 = __importStar(require("net")); const KNXConstants_1 = require("./protocol/KNXConstants"); const CEMIConstants_1 = __importDefault(require("./protocol/cEMI/CEMIConstants")); const CEMIFactory_1 = __importDefault(require("./protocol/cEMI/CEMIFactory")); const KNXProtocol_1 = __importDefault(require("./protocol/KNXProtocol")); const KNXConnectResponse_1 = __importDefault(require("./protocol/KNXConnectResponse")); const HPAI_1 = __importDefault(require("./protocol/HPAI")); const TunnelCRI_1 = __importStar(require("./protocol/TunnelCRI")); const KNXConnectionStateResponse_1 = __importDefault(require("./protocol/KNXConnectionStateResponse")); const errors = __importStar(require("./errors")); const ipAddressHelper = __importStar(require("./util/ipAddressHelper")); const KNXAddress_1 = __importDefault(require("./protocol/KNXAddress")); const KNXDataBuffer_1 = __importDefault(require("./protocol/KNXDataBuffer")); const DPTLib = __importStar(require("./dptlib")); const KnxLog_1 = require("./KnxLog"); const KNXRoutingIndication_1 = __importDefault(require("./protocol/KNXRoutingIndication")); const KNXTunnelingRequest_1 = __importDefault(require("./protocol/KNXTunnelingRequest")); const TypedEmitter_1 = require("./TypedEmitter"); const KNXTunnelingAck_1 = __importDefault(require("./protocol/KNXTunnelingAck")); const utils_1 = require("./utils"); var ConncetionState; (function (ConncetionState) { ConncetionState["STARTED"] = "STARTED"; ConncetionState["CONNECTING"] = "CONNECTING"; ConncetionState["CONNECTED"] = "CONNECTED"; ConncetionState["DISCONNECTING"] = "DISCONNECTING"; ConncetionState["DISCONNECTED"] = "DISCONNECTED"; })(ConncetionState || (exports.ConncetionState = ConncetionState = {})); var SocketEvents; (function (SocketEvents) { SocketEvents["error"] = "error"; SocketEvents["message"] = "message"; SocketEvents["listening"] = "listening"; SocketEvents["data"] = "data"; SocketEvents["close"] = "close"; })(SocketEvents || (exports.SocketEvents = SocketEvents = {})); var KNXClientEvents; (function (KNXClientEvents) { KNXClientEvents["error"] = "error"; KNXClientEvents["disconnected"] = "disconnected"; KNXClientEvents["discover"] = "discover"; KNXClientEvents["indication"] = "indication"; KNXClientEvents["connected"] = "connected"; KNXClientEvents["ready"] = "ready"; KNXClientEvents["response"] = "response"; KNXClientEvents["connecting"] = "connecting"; KNXClientEvents["ackReceived"] = "ackReceived"; KNXClientEvents["close"] = "close"; KNXClientEvents["descriptionResponse"] = "descriptionResponse"; })(KNXClientEvents || (exports.KNXClientEvents = KNXClientEvents = {})); const jKNXSecureKeyring = ''; const optionsDefaults = { physAddr: '15.15.200', connectionKeepAliveTimeout: KNXConstants_1.KNX_CONSTANTS.CONNECTION_ALIVE_TIME, ipAddr: '224.0.23.12', ipPort: 3671, hostProtocol: 'Multicast', isSecureKNXEnabled: false, suppress_ack_ldatareq: false, loglevel: 'info', localEchoInTunneling: true, localIPAddress: '', interface: '', jKNXSecureKeyring: {}, KNXQueueSendIntervalMilliseconds: 25, theGatewayIsKNXVirtual: false, }; function getDecodedKeyring() { return jKNXSecureKeyring; } exports.getDecodedKeyring = getDecodedKeyring; var KNXTimer; (function (KNXTimer) { KNXTimer["ACK"] = "ack"; KNXTimer["HEARTBEAT"] = "heartbeat"; KNXTimer["CONNECTION_STATE"] = "connection_state"; KNXTimer["CONNECTION"] = "connection"; KNXTimer["CONNECT_REQUEST"] = "connect_request"; KNXTimer["DISCONNECT"] = "disconnect"; KNXTimer["DISCOVERY"] = "discovery"; KNXTimer["GATEWAYDESCRIPTION"] = "GatewayDescription"; })(KNXTimer || (exports.KNXTimer = KNXTimer = {})); class KNXClient extends TypedEmitter_1.TypedEventEmitter { get udpSocket() { if (this._clientSocket instanceof dgram_1.Socket) { return this._clientSocket; } return null; } get tcpSocket() { if (this._clientSocket instanceof net_1.Socket) { return this._clientSocket; } return null; } constructor(options, createSocket) { super(); this._clearToSend = false; this.socketReady = false; this.commandQueue = []; this.queueLock = false; this.timers = new Map(); this.commandQueue = []; this.exitProcessingKNXQueueLoop = false; if (options === undefined) { options = optionsDefaults; } else { options = Object.assign(Object.assign({}, optionsDefaults), options); } this._options = options; this.sniffingPackets = []; this.sysLogger = (0, KnxLog_1.module)(this._options.setPrefix || 'KNXEngine'); if (this._options.loglevel) { (0, KnxLog_1.setLogLevel)(this._options.loglevel); } this._channelID = null; this._connectionState = ConncetionState.DISCONNECTED; this._numFailedTelegramACK = 0; this._clientTunnelSeqNumber = -1; this._options.connectionKeepAliveTimeout = KNXConstants_1.KNX_CONSTANTS.CONNECTION_ALIVE_TIME; this._peerHost = this._options.ipAddr; this._peerPort = parseInt(this._options.ipPort, 10); this._options.localSocketAddress = options.localSocketAddress; this._heartbeatFailures = 0; this.max_HeartbeatFailures = 3; this._awaitingResponseType = null; this._clientSocket = null; this.jKNXSecureKeyring = this._options.jKNXSecureKeyring; try { if (Number(this._options.KNXQueueSendIntervalMilliseconds) < 20) { this._options.KNXQueueSendIntervalMilliseconds = 20; } } catch (error) { this._options.KNXQueueSendIntervalMilliseconds = 25; this.sysLogger.error(`KNXQueueSendIntervalMilliseconds:${error.message}. Defaulting to 25`); } this.on('error', (error) => { this.sysLogger.error(error.stack); }); if (typeof this._options.physAddr === 'string') { this.physAddr = KNXAddress_1.default.createFromString(this._options.physAddr); } try { this._options.localIPAddress = ipAddressHelper.getLocalAddress(this._options.interface); } catch (error) { this.sysLogger.error(`ipAddressHelper.getLocalAddress:${error.message}`); throw error; } if (createSocket) { createSocket(this); } else { this.createSocket(); } } createSocket() { if (this._options.hostProtocol === 'TunnelUDP') { this._clientSocket = dgram_1.default.createSocket({ type: 'udp4', reuseAddr: true, }); this.udpSocket.on(SocketEvents.message, this.processInboundMessage.bind(this)); this.udpSocket.on(SocketEvents.error, (error) => { this.socketReady = false; this.emit(KNXClientEvents.error, error); }); this.udpSocket.on(SocketEvents.close, () => { this.socketReady = false; this.exitProcessingKNXQueueLoop = true; this.emit(KNXClientEvents.close); }); this.udpSocket.on(SocketEvents.listening, () => { this.socketReady = true; this.handleKNXQueue(); }); this.udpSocket.bind({ address: this._options.localIPAddress, }, () => { try { this.udpSocket.setTTL(5); if (this._options.localSocketAddress === undefined) { this._options.localSocketAddress = this.udpSocket.address().address; } } catch (error) { this.sysLogger.error(`UDP: Error setting SetTTL ${error.message}` || ''); } }); } else if (this._options.hostProtocol === 'TunnelTCP') { this._clientSocket = new net_1.default.Socket(); this.tcpSocket.on(SocketEvents.data, (data) => { this.sysLogger.debug(`[${(0, utils_1.getTimestamp)()}] Received message`, data); }); this.tcpSocket.on(SocketEvents.error, (error) => { this.socketReady = false; this.emit(KNXClientEvents.error, error); }); this.tcpSocket.on(SocketEvents.close, (hadError) => { this.socketReady = false; this.exitProcessingKNXQueueLoop = true; this.emit(KNXClientEvents.close); }); this.tcpSocket.on('connect', () => { this.socketReady = true; this.handleKNXQueue(); this.emit(KNXClientEvents.connected, this._options); }); } else if (this._options.hostProtocol === 'Multicast') { this._clientSocket = dgram_1.default.createSocket({ type: 'udp4', reuseAddr: true, }); this.udpSocket.on(SocketEvents.listening, () => { this.socketReady = true; this.handleKNXQueue(); }); this.udpSocket.on(SocketEvents.message, this.processInboundMessage.bind(this)); this.udpSocket.on(SocketEvents.error, (error) => { this.socketReady = false; this.emit(KNXClientEvents.error, error); }); this.udpSocket.on(SocketEvents.close, () => { this.socketReady = false; this.exitProcessingKNXQueueLoop = true; this.emit(KNXClientEvents.close); }); this.udpSocket.bind(this._peerPort, this._options.theGatewayIsKNXVirtual ? this._options.localIPAddress || '0.0.0.0' : '0.0.0.0', () => { try { this.udpSocket.setMulticastTTL(5); this.udpSocket.setMulticastInterface(this._options.localIPAddress); } catch (error) { this.sysLogger.error(`Multicast: Error setting SetTTL ${error.message}` || ''); } try { this.udpSocket.addMembership(this._peerHost, this._options.localIPAddress); } catch (err) { this.sysLogger.error('Multicast: cannot add membership (%s)', err); this.emit(KNXClientEvents.error, err); } }); } } get channelID() { return this._channelID; } get clearToSend() { return this._clearToSend; } set clearToSend(val) { this._clearToSend = val; if (val) { this.handleKNXQueue(); } } getKNXDataBuffer(data, dptid) { if (typeof dptid === 'number') { dptid = dptid.toString(); } const adpu = {}; DPTLib.populateAPDU(data, adpu, dptid); const iDatapointType = parseInt(dptid.substring(0, dptid.indexOf('.'))); const isSixBits = adpu.bitlength <= 6; this.sysLogger.debug(`[${(0, utils_1.getTimestamp)()}] ` + `isSixBits:${isSixBits} Includes (should be = isSixBits):${[ 1, 2, 3, 5, 9, 10, 11, 14, 18, ].includes(iDatapointType)} ADPU BitLength:${adpu.bitlength}`); const datapoint = { id: '', value: 'any', type: { type: isSixBits }, bind: null, read: () => null, write: null, }; return new KNXDataBuffer_1.default(adpu.data, datapoint); } async waitForEvent(event, timeout) { let resolveRef; return Promise.race([ new Promise((resolve) => { resolveRef = resolve; this.once(event, resolve); }), (0, utils_1.wait)(timeout), ]).then(() => { this.off(event, resolveRef); }); } setTimer(type, cb, delay) { if (this.timers.has(type)) { clearTimeout(this.timers.get(type)); this.timers.delete(type); this.sysLogger.warn(`Timer "${type}" was already running`); } this.timers.set(type, setTimeout(() => { this.timers.delete(type); cb(); }, delay)); } clearTimer(type) { if (this.timers.has(type)) { clearTimeout(this.timers.get(type)); this.timers.delete(type); } } clearAllTimers() { this.stopDiscovery(); this.stopHeartBeat(); this.stopGatewayDescription(); for (const timer of this.timers.keys()) { this.clearTimer(timer); } } processKnxPacketQueueItem(_knxPacket) { return new Promise((resolve) => { if (this.sysLogger.level === 'debug') { if (_knxPacket instanceof KNXTunnelingRequest_1.default || _knxPacket instanceof KNXRoutingIndication_1.default) { let sTPCI = ''; if (_knxPacket.cEMIMessage.npdu.isGroupRead) { sTPCI = 'Read'; } if (_knxPacket.cEMIMessage.npdu.isGroupResponse) { sTPCI = 'Response'; } if (_knxPacket.cEMIMessage.npdu.isGroupWrite) { sTPCI = 'Write'; } let sDebugString = ''; sDebugString = `peerHost:${this._peerHost}:${this._peerPort}`; sDebugString += ` dstAddress: ${_knxPacket.cEMIMessage.dstAddress.toString()}`; sDebugString += ` channelID:${this._channelID === null || this._channelID === undefined ? 'None' : this._channelID}`; sDebugString += ` npdu: ${sTPCI}`; sDebugString += ` knxHeader: ${_knxPacket.constructor.name}`; sDebugString += ` raw: ${JSON.stringify(_knxPacket)}`; this.sysLogger.debug(`[${(0, utils_1.getTimestamp)()}] ` + `KNXEngine: <outgoing telegram>: ${sDebugString} `); } else if (_knxPacket instanceof KNXTunnelingAck_1.default) { this.sysLogger.debug(`[${(0, utils_1.getTimestamp)()}] ` + `KNXEngine: <outgoing telegram ACK>:${this.getKNXConstantName(_knxPacket.status)} channelID:${_knxPacket.channelID} seqCounter:${_knxPacket.seqCounter}`); } } if (this._options.hostProtocol === 'Multicast' || this._options.hostProtocol === 'TunnelUDP') { try { this.udpSocket.send(_knxPacket.toBuffer(), this._peerPort, this._peerHost, (error) => { if (error) { this.sysLogger.error(`Sending KNX packet: Send UDP sending error: ${error.message}`); this.emit(KNXClientEvents.error, error); } resolve(!error); }); } catch (error) { this.sysLogger.error(`Sending KNX packet: Send UDP Catch error: ${error.message} ${typeof _knxPacket} seqCounter:${_knxPacket === null || _knxPacket === void 0 ? void 0 : _knxPacket.seqCounter}`); this.emit(KNXClientEvents.error, error); resolve(false); } } else { try { this.tcpSocket.write(_knxPacket.toBuffer(), (error) => { if (error) { this.sysLogger.error(`Sending KNX packet: Send TCP sending error: ${error.message}` || 'Undef error'); this.emit(KNXClientEvents.error, error); } resolve(!error); }); } catch (error) { this.sysLogger.error(`Sending KNX packet: Send TCP Catch error: ${error.message}` || 'Undef error'); this.emit(KNXClientEvents.error, error); resolve(false); } } }); } async handleKNXQueue() { if (this.queueLock) { this.sysLogger.debug(`[${(0, utils_1.getTimestamp)()}] ` + `KNXClient: handleKNXQueue: HandleQueue has called, but the queue loop is already running. Exit.`); return; } this.sysLogger.debug(`[${(0, utils_1.getTimestamp)()}] ` + `KNXClient: handleKNXQueue: Start Processing queued KNX. Found ${this.commandQueue.length} telegrams in queue.`); this.queueLock = true; while (this.commandQueue.length > 0) { if (!this.clearToSend) { this.sysLogger.debug(`[${(0, utils_1.getTimestamp)()}] ` + `KNXClient: handleKNXQueue: Clear to send is false. Pause processing queue.`); break; } if (this.exitProcessingKNXQueueLoop) { this.sysLogger.debug(`[${(0, utils_1.getTimestamp)()}] ` + `KNXClient: handleKNXQueue: exitProcessingKNXQueueLoop is true. Exit processing queue loop`); break; } if (this.socketReady === false) { this.sysLogger.debug(`[${(0, utils_1.getTimestamp)()}] ` + `KNXClient: handleKNXQueue: Socket is not ready. Stop processing queue.`); break; } const item = this.commandQueue.pop(); this.currentItemHandledByTheQueue = item; if (item.ACK !== undefined) { this.setTimerWaitingForACK(item.ACK); } if (!(await this.processKnxPacketQueueItem(item.knxPacket))) { this.sysLogger.error(`KNXClient: handleKNXQueue: returning from processKnxPacketQueueItem ${JSON.stringify(item)}`); this.commandQueue = []; break; } await (0, utils_1.wait)(this._options.KNXQueueSendIntervalMilliseconds); } this.queueLock = false; this.sysLogger.debug(`[${(0, utils_1.getTimestamp)()}] ` + `KNXClient: handleKNXQueue: End Processing queued KNX.`); } send(_knxPacket, _ACK, _priority, _expectedSeqNumberForACK) { var _a, _b; const toBeAdded = { knxPacket: _knxPacket, ACK: _ACK, expectedSeqNumberForACK: _expectedSeqNumberForACK, }; if (this._options.sniffingMode) { const buffer = _knxPacket.toBuffer(); this.sniffingPackets.push({ reqType: _knxPacket.constructor.name, request: buffer.toString('hex'), deltaReq: this.lastSnifferRequest ? Date.now() - this.lastSnifferRequest : 0, }); this.lastSnifferRequest = Date.now(); } if (_priority) { this.commandQueue.push(toBeAdded); this.clearToSend = true; } else { this.commandQueue.unshift(toBeAdded); } this.handleKNXQueue(); this.sysLogger.debug(`[${(0, utils_1.getTimestamp)()}] ` + `KNXClient: <added telegram to queue> queueLength:${this.commandQueue.length} priority:${_priority} type:${this.getKNXConstantName(toBeAdded.knxPacket.type)} channelID:${((_a = toBeAdded.ACK) === null || _a === void 0 ? void 0 : _a.channelID) || 'filled later'} seqCounter:${((_b = toBeAdded.ACK) === null || _b === void 0 ? void 0 : _b.seqCounter) || 'filled later'}`); } write(dstAddress, data, dptid) { if (this._connectionState !== ConncetionState.CONNECTED) throw new Error('The socket is not connected. Unable to access the KNX BUS'); const knxBuffer = this.getKNXDataBuffer(data, dptid); if (typeof dstAddress === 'string') dstAddress = KNXAddress_1.default.createFromString(dstAddress, KNXAddress_1.default.TYPE_GROUP); const srcAddress = this.physAddr; if (this._options.hostProtocol === 'Multicast') { const cEMIMessage = CEMIFactory_1.default.newLDataIndicationMessage('write', srcAddress, dstAddress, knxBuffer); cEMIMessage.control.ack = 0; cEMIMessage.control.broadcast = 1; cEMIMessage.control.priority = 3; cEMIMessage.control.addressType = 1; cEMIMessage.control.hopCount = 6; const knxPacketRequest = KNXProtocol_1.default.newKNXRoutingIndication(cEMIMessage); this.send(knxPacketRequest, undefined, false, this.getSeqNumber()); } else { const cEMIMessage = CEMIFactory_1.default.newLDataRequestMessage('write', srcAddress, dstAddress, knxBuffer); cEMIMessage.control.ack = 0; cEMIMessage.control.broadcast = 1; cEMIMessage.control.priority = 3; cEMIMessage.control.addressType = 1; cEMIMessage.control.hopCount = 6; const seqNum = this.incSeqNumber(); const knxPacketRequest = KNXProtocol_1.default.newKNXTunnelingRequest(this._channelID, seqNum, cEMIMessage); if (!this._options.suppress_ack_ldatareq) { this.send(knxPacketRequest, knxPacketRequest, false, this.getSeqNumber()); } else { this.send(knxPacketRequest, undefined, false, this.getSeqNumber()); } if (this._options.localEchoInTunneling) this.emit(KNXClientEvents.indication, knxPacketRequest, true); } } respond(dstAddress, data, dptid) { if (this._connectionState !== ConncetionState.CONNECTED) throw new Error('The socket is not connected. Unable to access the KNX BUS'); const knxBuffer = this.getKNXDataBuffer(data, dptid); if (typeof dstAddress === 'string') dstAddress = KNXAddress_1.default.createFromString(dstAddress, KNXAddress_1.default.TYPE_GROUP); const srcAddress = this.physAddr; if (this._options.hostProtocol === 'Multicast') { const cEMIMessage = CEMIFactory_1.default.newLDataIndicationMessage('response', srcAddress, dstAddress, knxBuffer); cEMIMessage.control.ack = 0; cEMIMessage.control.broadcast = 1; cEMIMessage.control.priority = 3; cEMIMessage.control.addressType = 1; cEMIMessage.control.hopCount = 6; const knxPacketRequest = KNXProtocol_1.default.newKNXRoutingIndication(cEMIMessage); this.send(knxPacketRequest, undefined, false, this.getSeqNumber()); } else { const cEMIMessage = CEMIFactory_1.default.newLDataRequestMessage('response', srcAddress, dstAddress, knxBuffer); cEMIMessage.control.ack = 0; cEMIMessage.control.broadcast = 1; cEMIMessage.control.priority = 3; cEMIMessage.control.addressType = 1; cEMIMessage.control.hopCount = 6; const seqNum = this.incSeqNumber(); const knxPacketRequest = KNXProtocol_1.default.newKNXTunnelingRequest(this._channelID, seqNum, cEMIMessage); if (!this._options.suppress_ack_ldatareq) { this.send(knxPacketRequest, knxPacketRequest, false, this.getSeqNumber()); } else { this.send(knxPacketRequest, undefined, false, this.getSeqNumber()); } if (this._options.localEchoInTunneling) this.emit(KNXClientEvents.indication, knxPacketRequest, true); } } read(dstAddress) { if (this._connectionState !== ConncetionState.CONNECTED) throw new Error('The socket is not connected. Unable to access the KNX BUS'); if (typeof dstAddress === 'string') dstAddress = KNXAddress_1.default.createFromString(dstAddress, KNXAddress_1.default.TYPE_GROUP); const srcAddress = this.physAddr; if (this._options.hostProtocol === 'Multicast') { const cEMIMessage = CEMIFactory_1.default.newLDataIndicationMessage('read', srcAddress, dstAddress, null); cEMIMessage.control.ack = 0; cEMIMessage.control.broadcast = 1; cEMIMessage.control.priority = 3; cEMIMessage.control.addressType = 1; cEMIMessage.control.hopCount = 6; const knxPacketRequest = KNXProtocol_1.default.newKNXRoutingIndication(cEMIMessage); this.send(knxPacketRequest, undefined, false, this.getSeqNumber()); } else { const cEMIMessage = CEMIFactory_1.default.newLDataRequestMessage('read', srcAddress, dstAddress, null); cEMIMessage.control.ack = 0; cEMIMessage.control.broadcast = 1; cEMIMessage.control.priority = 3; cEMIMessage.control.addressType = 1; cEMIMessage.control.hopCount = 6; const seqNum = this.incSeqNumber(); const knxPacketRequest = KNXProtocol_1.default.newKNXTunnelingRequest(this._channelID, seqNum, cEMIMessage); if (!this._options.suppress_ack_ldatareq) { this.send(knxPacketRequest, knxPacketRequest, false, this.getSeqNumber()); } else { this.send(knxPacketRequest, undefined, false, this.getSeqNumber()); } if (this._options.localEchoInTunneling) { this.emit(KNXClientEvents.indication, knxPacketRequest, true); } } } writeRaw(dstAddress, rawDataBuffer, bitlength) { if (this._connectionState !== ConncetionState.CONNECTED) throw new Error('The socket is not connected. Unable to access the KNX BUS'); if (!Buffer.isBuffer(rawDataBuffer)) { this.sysLogger.error('KNXClient: writeRaw: Value must be a buffer! '); return; } const isSixBits = bitlength <= 6; const datapoint = { id: '', value: 'any', type: { type: isSixBits }, bind: null, read: () => null, write: null, }; const baseBufferFromBitLength = Buffer.alloc(Math.ceil(bitlength / 8)); rawDataBuffer.copy(baseBufferFromBitLength, 0); const data = new KNXDataBuffer_1.default(baseBufferFromBitLength, datapoint); if (typeof dstAddress === 'string') dstAddress = KNXAddress_1.default.createFromString(dstAddress, KNXAddress_1.default.TYPE_GROUP); const srcAddress = this.physAddr; if (this._options.hostProtocol === 'Multicast') { const cEMIMessage = CEMIFactory_1.default.newLDataIndicationMessage('write', srcAddress, dstAddress, data); cEMIMessage.control.ack = 0; cEMIMessage.control.broadcast = 1; cEMIMessage.control.priority = 3; cEMIMessage.control.addressType = 1; cEMIMessage.control.hopCount = 6; const knxPacketRequest = KNXProtocol_1.default.newKNXRoutingIndication(cEMIMessage); this.send(knxPacketRequest, undefined, false, this.getSeqNumber()); } else { const cEMIMessage = CEMIFactory_1.default.newLDataRequestMessage('write', srcAddress, dstAddress, data); cEMIMessage.control.ack = this._options.suppress_ack_ldatareq ? 0 : 1; cEMIMessage.control.broadcast = 1; cEMIMessage.control.priority = 3; cEMIMessage.control.addressType = 1; cEMIMessage.control.hopCount = 6; const seqNum = this.incSeqNumber(); const knxPacketRequest = KNXProtocol_1.default.newKNXTunnelingRequest(this._channelID, seqNum, cEMIMessage); if (!this._options.suppress_ack_ldatareq) { this.send(knxPacketRequest, knxPacketRequest, false, this.getSeqNumber()); } else { this.send(knxPacketRequest, undefined, false, this.getSeqNumber()); } if (this._options.localEchoInTunneling) this.emit(KNXClientEvents.indication, knxPacketRequest, true); } } startHeartBeat() { this.stopHeartBeat(); this._heartbeatFailures = 0; this._heartbeatRunning = true; this.runHeartbeat(); } stopHeartBeat() { this._heartbeatRunning = false; this.clearTimer(KNXTimer.HEARTBEAT); this.clearTimer(KNXTimer.CONNECTION_STATE); } isDiscoveryRunning() { return this.timers.has(KNXTimer.DISCOVERY); } startDiscovery() { this._clearToSend = true; if (this.isDiscoveryRunning()) { throw new Error('Discovery already running'); } this.setTimer(KNXTimer.DISCOVERY, () => { }, 1000 * KNXConstants_1.KNX_CONSTANTS.SEARCH_TIMEOUT); this.sendSearchRequestMessage(); } stopDiscovery() { this.clearTimer(KNXTimer.DISCOVERY); } static async discover(eth, timeout = 5000) { if (typeof eth === 'number') { timeout = eth; eth = undefined; } const client = new KNXClient({ interface: eth, hostProtocol: 'Multicast', }); const discovered = []; client.on(KNXClientEvents.discover, (host, header, searchResponse) => { var _a, _b, _c, _d; discovered.push(`${host}:${(_b = (_a = searchResponse.deviceInfo) === null || _a === void 0 ? void 0 : _a.name.replace(/:/g, ' ')) !== null && _b !== void 0 ? _b : ''}:${(_d = (_c = searchResponse.deviceInfo) === null || _c === void 0 ? void 0 : _c.formattedAddress) !== null && _d !== void 0 ? _d : ''}`); }); client.startDiscovery(); await (0, utils_1.wait)(timeout); await client.Disconnect(); return discovered; } isGatewayDescriptionRunning() { return this.timers.has(KNXTimer.GATEWAYDESCRIPTION); } startGatewayDescription() { this._clearToSend = true; if (this.isGatewayDescriptionRunning()) { throw new Error('GatewayDescription gather is already running'); } this.setTimer(KNXTimer.GATEWAYDESCRIPTION, () => { }, 1000 * KNXConstants_1.KNX_CONSTANTS.DEVICE_CONFIGURATION_REQUEST_TIMEOUT); this.sendDescriptionRequestMessage(); } stopGatewayDescription() { this.clearTimer(KNXTimer.GATEWAYDESCRIPTION); } static async getGatewayDescription(ipAddr, ipPort, eth, timeout = 5000) { if (typeof eth === 'number') { timeout = eth; eth = undefined; } const client = new KNXClient({ ipAddr, ipPort, interface: eth, hostProtocol: 'TunnelUDP', }); const descriptions = []; client.on(KNXClientEvents.descriptionResponse, (searchResponse) => { descriptions.push(searchResponse); }); client.startGatewayDescription(); await (0, utils_1.wait)(timeout); await client.Disconnect(); return descriptions; } Connect(knxLayer = TunnelCRI_1.TunnelTypes.TUNNEL_LINKLAYER) { if (this._clientSocket === null) { throw new Error('No client socket defined'); } if (this._connectionState === ConncetionState.DISCONNECTING) { throw new Error('Socket is disconnecting. Please wait until disconnected.'); } if (this._connectionState === ConncetionState.CONNECTING) { throw new Error('Socket is connecting. Please wait until connected.'); } if (this._connectionState === ConncetionState.CONNECTED) { throw new Error('Socket is already connected. Disconnect first.'); } this._connectionState = ConncetionState.CONNECTING; this._numFailedTelegramACK = 0; this.clearToSend = true; this.clearTimer(KNXTimer.CONNECTION); this.emit(KNXClientEvents.connecting, this._options); if (this._options.hostProtocol === 'TunnelUDP') { const timeoutError = new Error(`Connection timeout to ${this._peerHost}:${this._peerPort}`); this._awaitingResponseType = KNXConstants_1.KNX_CONSTANTS.CONNECT_RESPONSE; this._clientTunnelSeqNumber = -1; this.setTimer(KNXTimer.CONNECTION, () => { this.emit(KNXClientEvents.error, timeoutError); }, 1000 * KNXConstants_1.KNX_CONSTANTS.CONNECT_REQUEST_TIMEOUT); this.setTimer(KNXTimer.CONNECT_REQUEST, () => { this.sendConnectRequestMessage(new TunnelCRI_1.default(knxLayer)); }, 2000); } else if (this._options.hostProtocol === 'TunnelTCP') { this.tcpSocket.connect(this._peerPort, this._peerHost, () => { this._awaitingResponseType = KNXConstants_1.KNX_CONSTANTS.CONNECT_RESPONSE; this._clientTunnelSeqNumber = 0; if (this._options.isSecureKNXEnabled) this.sendSecureSessionRequestMessage(new TunnelCRI_1.default(knxLayer)); }); } else { this._connectionState = ConncetionState.CONNECTED; this._numFailedTelegramACK = 0; this.clearToSend = true; this._clientTunnelSeqNumber = -1; this.emit(KNXClientEvents.connected, this._options); } } async closeSocket() { this.exitProcessingKNXQueueLoop = true; return new Promise((resolve) => { if (!this._clientSocket) return; this.socketReady = false; const client = this._clientSocket; this._clientSocket = null; const cb = () => { resolve(); }; try { if (client instanceof net_1.Socket) { client.destroy(); } else { client.close(cb); } } catch (error) { this.sysLogger.error(`KNXClient: into async closeSocket(): ${error.stack}`); resolve(); } }); } async Disconnect() { if (this._clientSocket === null) { throw new Error('No client socket defined'); } if (this._connectionState === ConncetionState.DISCONNECTING) { throw new Error('Already disconnecting'); } this.clearAllTimers(); this._connectionState = ConncetionState.DISCONNECTING; if (this._channelID === null) { this.sysLogger.debug(`[${(0, utils_1.getTimestamp)()}] ` + `KNXClient: into Disconnect(), channel id is not defined so skip disconnect packet and close socket`); await this.closeSocket(); return; } this._awaitingResponseType = KNXConstants_1.KNX_CONSTANTS.DISCONNECT_RESPONSE; this.sendDisconnectRequestMessage(this._channelID); await this.waitForEvent(KNXClientEvents.disconnected, 2000); this.exitProcessingKNXQueueLoop = true; if (this._connectionState !== ConncetionState.DISCONNECTED) { this.setDisconnected("Forced call from KNXClient Disconnect() function, because the KNX Interface hasn't sent the DISCONNECT_RESPONSE in time."); } if (this._options.sniffingMode) { this.sysLogger.info('Sniffing mode is enabled. Dumping sniffing buffers...'); this.sysLogger.info(this.sniffingPackets); } } isConnected() { return this._connectionState === ConncetionState.CONNECTED; } async setDisconnected(_sReason = '') { this.sysLogger.debug(`[${(0, utils_1.getTimestamp)()}] ` + `KNXClient: called _setDisconnected ${this._options.ipAddr}:${this._options.ipPort} ${_sReason}`); this._connectionState = ConncetionState.DISCONNECTED; this.clearAllTimers(); this._clientTunnelSeqNumber = -1; this._channelID = null; await this.closeSocket(); this.emit(KNXClientEvents.disconnected, `${this._options.ipAddr}:${this._options.ipPort} ${_sReason}`); this.clearToSend = true; } runHeartbeat() { if (!this._heartbeatRunning) { return; } if (this._clientSocket == null) { throw new Error('No client socket defined'); } const deadError = new Error(`Connection dead with ${this._peerHost}:${this._peerPort}`); this.setTimer(KNXTimer.CONNECTION_STATE, () => { this.sysLogger.error(`KNXClient: getConnectionStatus Timeout ${this._heartbeatFailures} out of ${this.max_HeartbeatFailures}`); this._heartbeatFailures++; if (this._heartbeatFailures >= this.max_HeartbeatFailures) { this._heartbeatFailures = 0; this.emit(KNXClientEvents.error, deadError); this.setDisconnected(deadError.message); } }, 1000 * KNXConstants_1.KNX_CONSTANTS.CONNECTIONSTATE_REQUEST_TIMEOUT); this._awaitingResponseType = KNXConstants_1.KNX_CONSTANTS.CONNECTIONSTATE_RESPONSE; this.sendConnectionStateRequestMessage(this._channelID); this.setTimer(KNXTimer.HEARTBEAT, () => { this.runHeartbeat(); }, 1000 * this._options.connectionKeepAliveTimeout); } getSeqNumber() { return this._clientTunnelSeqNumber; } getCurrentItemHandledByTheQueue() { return this.currentItemHandledByTheQueue.expectedSeqNumberForACK; } incSeqNumber() { this._clientTunnelSeqNumber++; if (this._clientTunnelSeqNumber > 255) { this._clientTunnelSeqNumber = 0; } return this._clientTunnelSeqNumber; } setTimerWaitingForACK(knxTunnelingRequest) { this.clearToSend = false; const timeoutErr = new errors.RequestTimeoutError(`seqCounter:${knxTunnelingRequest.seqCounter}, DestAddr:${knxTunnelingRequest.cEMIMessage.dstAddress.toString() || 'Non definito'}, AckRequested:${knxTunnelingRequest.cEMIMessage.control.ack}, timed out waiting telegram acknowledge by ${this._options.ipAddr || 'No Peer host detected'}`); this.setTimer(KNXTimer.ACK, () => { this._numFailedTelegramACK += 1; if (this._numFailedTelegramACK > 2) { this._numFailedTelegramACK = 0; this.emit(KNXClientEvents.ackReceived, knxTunnelingRequest, false); this.clearToSend = true; this.emit(KNXClientEvents.error, timeoutErr); this.sysLogger.error(`KNXClient: _setTimerWaitingForACK: ${timeoutErr.message || 'Undef error'} no ACK received. ABORT sending datagram with seqNumber ${this.getSeqNumber()} from ${knxTunnelingRequest.cEMIMessage.srcAddress.toString()} to ${knxTunnelingRequest.cEMIMessage.dstAddress.toString()}`); } else { this.sysLogger.error(`KNXClient: _setTimerWaitingForACK: ${timeoutErr.message || 'Undef error'} no ACK received. Retransmit datagram with seqNumber ${this.currentItemHandledByTheQueue .expectedSeqNumberForACK} from ${knxTunnelingRequest.cEMIMessage.srcAddress.toString()} to ${knxTunnelingRequest.cEMIMessage.dstAddress.toString()}`); this.send(knxTunnelingRequest, knxTunnelingRequest, true, this.currentItemHandledByTheQueue .expectedSeqNumberForACK); } }, KNXConstants_1.KNX_CONSTANTS.TUNNELING_REQUEST_TIMEOUT * 1000); } getKNXConstantName(serviceType) { const entry = Object.entries(KNXConstants_1.KNX_CONSTANTS).find(([, value]) => value === serviceType); return entry ? entry[0] : undefined; } processInboundMessage(msg, rinfo) { let sProcessInboundLog = ''; try { const { knxHeader, knxMessage } = KNXProtocol_1.default.parseMessage(msg); sProcessInboundLog = `peerHost:${this._peerHost}:${this._peerPort}`; sProcessInboundLog += ` srcAddress:${rinfo === null || rinfo === void 0 ? void 0 : rinfo.address}:${rinfo === null || rinfo === void 0 ? void 0 : rinfo.port}`; sProcessInboundLog += ` channelID:${this._channelID === null || this._channelID === undefined ? 'None' : this._channelID}`; sProcessInboundLog += ` service_type:${this.getKNXConstantName(knxHeader === null || knxHeader === void 0 ? void 0 : knxHeader.service_type)}`; sProcessInboundLog += ` knxHeader:${JSON.stringify(knxHeader)} knxMessage:${JSON.stringify(knxMessage)}`; sProcessInboundLog += ` raw: ${msg.toString('hex')}`; this.sysLogger.debug(`[${(0, utils_1.getTimestamp)()}] ` + `KNXEngine: <incoming telegram>: ${sProcessInboundLog} `); if (this._options.sniffingMode) { const lastEntry = this.sniffingPackets[this.sniffingPackets.length - 1]; if (lastEntry) { if (lastEntry.response) { this.sniffingPackets.push({ reqType: knxMessage.constructor.name, response: msg.toString('hex'), deltaReq: Date.now() - this.lastSnifferRequest, }); } else { lastEntry.response = msg.toString('hex'); lastEntry.resType = knxMessage.constructor.name; lastEntry.deltaRes = Date.now() - this.lastSnifferRequest; } } } if (knxHeader.service_type === KNXConstants_1.KNX_CONSTANTS.ROUTING_LOST_MESSAGE) { this.emit(KNXClientEvents.error, new Error('ROUTING_LOST_MESSAGE')); return; } if (knxHeader.service_type === KNXConstants_1.KNX_CONSTANTS.ROUTING_BUSY) { this.emit(KNXClientEvents.error, new Error('ROUTING_BUSY')); return; } if (knxHeader.service_type === KNXConstants_1.KNX_CONSTANTS.SEARCH_RESPONSE) { if (!this.isDiscoveryRunning()) return; this.emit(KNXClientEvents.discover, `${rinfo.address}:${rinfo.port}`, knxHeader, knxMessage); } else if (knxHeader.service_type === KNXConstants_1.KNX_CONSTANTS.DESCRIPTION_RESPONSE) { const knxDescriptionResponse = knxMessage; this.sysLogger.debug(`[${(0, utils_1.getTimestamp)()}] ` + `Received KNX packet: TUNNELING: DESCRIPTION_RESPONSE, ChannelID:${this._channelID} DescriptionResponse:${JSON.stringify(knxDescriptionResponse)} Host:${this._options.ipAddr}:${this._options.ipPort}`); this.emit(KNXClientEvents.descriptionResponse, knxDescriptionResponse); } else if (knxHeader.service_type === KNXConstants_1.KNX_CONSTANTS.CONNECT_RESPONSE) { if (this._connectionState === ConncetionState.CONNECTING) { this.clearTimer(KNXTimer.CONNECTION); const knxConnectResponse = knxMessage; if (knxConnectResponse.status !== KNXConstants_1.ConnectionStatus.E_NO_ERROR) { this.emit(KNXClientEvents.error, Error(KNXConnectResponse_1.default.statusToString(knxConnectResponse.status))); this.setDisconnected(`Connect response error ${knxConnectResponse.status}`); return; } this.clearTimer(KNXTimer.ACK); this._channelID = knxConnectResponse.channelID; this._connectionState = ConncetionState.CONNECTED; this._numFailedTelegramACK = 0; this.clearToSend = true; this.emit(KNXClientEvents.connected, this._options); this.startHeartBeat(); } } else if (knxHeader.service_type === KNXConstants_1.KNX_CONSTANTS.DISCONNECT_RESPONSE) { if (this._connectionState !== ConncetionState.DISCONNECTING) { this.emit(KNXClientEvents.error, new Error('Unexpected Disconnect Response.')); } this.setDisconnected('Received DISCONNECT_RESPONSE from the KNX interface.'); } else if (knxHeader.service_type === KNXConstants_1.KNX_CONSTANTS.DISCONNECT_REQUEST) { const knxDisconnectRequest = knxMessage; if (knxDisconnectRequest.channelID !== this._channelID) { return; } this._connectionState = ConncetionState.DISCONNECTING; this.sendDisconnectResponseMessage(knxDisconnectRequest.channelID); this.setTimer(KNXTimer.DISCONNECT, () => { this.setDisconnected(`Received KNX packet: DISCONNECT_REQUEST, ChannelID:${this._channelID} Host:${this._options.ipAddr}:${this._options.ipPort}`); }, 1000); } else if (knxHeader.service_type === KNXConstants_1.KNX_CONSTANTS.TUNNELING_REQUEST) { const knxTunnelingRequest = knxMessage; if (knxTunnelingRequest.channelID !== this._channelID) { this.sysLogger.debug(`[${(0, utils_1.getTimestamp)()}] ` + `Received KNX packet: TUNNELING: L_DATA_IND, NOT FOR ME: MyChannelID:${this._channelID} ReceivedPacketChannelID: ${knxTunnelingRequest.channelID} ReceivedPacketseqCounter:${knxTunnelingRequest.seqCounter} Host:${this._options.ipAddr}:${this._options.ipPort}`); return; } try { const knxTunnelAck = KNXProtocol_1.default.newKNXTunnelingACK(knxTunnelingRequest.channelID, knxTunnelingRequest.seqCounter, KNXConstants_1.KNX_CONSTANTS.E_NO_ERROR); this.send(knxTunnelAck, undefined, true, this.getSeqNumber()); } catch (error) { this.sysLogger.error(`Received KNX packet: TUNNELING: L_DATA_IND, ERROR BUILDING THE TUNNELINK ACK: ${error.message}`); } if (knxTunnelingRequest.cEMIMessage.msgCode === CEMIConstants_1.default.L_DATA_IND) { this.emit(KNXClientEvents.indication, knxTunnelingRequest, false); } else if (knxTunnelingRequest.cEMIMessage.msgCode === CEMIConstants_1.default.L_DATA_CON) { this.sysLogger.debug(`[${(0, utils_1.getTimestamp)()}] ` + `Received KNX packet: TUNNELING: L_DATA_CON, dont' care.`); } } else if (knxHeader.service_type === KNXConstants_1.KNX_CONSTANTS.TUNNELING_ACK) { const knxTunnelingAck = knxMessage; if (knxTunnelingAck.channelID !== this._channelID) { return; } if (!this._options.suppress_ack_ldatareq) { if (knxTunnelingAck.seqCounter === this.getCurrentItemHandledByTheQueue()) { this.clearTimer(KNXTimer.ACK); this._numFailedTelegramACK = 0; this.clearToSend = true; this.emit(KNXClientEvents.ackReceived, knxTunnelingAck, true); this.sysLogger.debug(`[${(0, utils_1.getTimestamp)()}] ` + `Received KNX packet: TUNNELING: DELETED_TUNNELING_ACK FROM PENDING ACK's, ChannelID:${this._channelID} seqCounter:${knxTunnelingAck.seqCounter} Host:${this._options.ipAddr}:${this._options.ipPort}`); } else { this.sysLogger.error(`Received KNX packet: T