UNPKG

knxultimate

Version:

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

1,083 lines (1,082 loc) 148 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 () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __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.KNXClientEvents = exports.SocketEvents = exports.ConncetionState = void 0; const dgram_1 = __importStar(require("dgram")); const net_1 = __importStar(require("net")); const crypto = __importStar(require("crypto")); const KNXConstants_1 = require("./protocol/KNXConstants"); const CEMIConstants_1 = __importDefault(require("./protocol/cEMI/CEMIConstants")); const CEMIFactory_1 = __importDefault(require("./protocol/cEMI/CEMIFactory")); const CEMIMessage_1 = __importDefault(require("./protocol/cEMI/CEMIMessage")); const KNXProtocol_1 = __importDefault(require("./protocol/KNXProtocol")); const KNXConnectResponse_1 = __importDefault(require("./protocol/KNXConnectResponse")); const HPAI_1 = __importStar(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"); const SerialFT12_1 = __importDefault(require("./transports/SerialFT12")); const perf_hooks_1 = require("perf_hooks"); const keyring_1 = require("./secure/keyring"); const security_primitives_1 = require("./secure/security_primitives"); const secure_knx_constants_1 = require("./secure/secure_knx_constants"); 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["connect"] = "connect"; })(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 optionsDefaults = { physAddr: '', 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', localIPAddress: '', interface: '', KNXQueueSendIntervalMilliseconds: 25, theGatewayIsKNXVirtual: false, secureRoutingWaitForTimer: true, }; 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; } isSerialTransport() { return this._options.hostProtocol === 'SerialFT12'; } constructor(options, createSocket) { super(); this._clearToSend = false; this.socketReady = false; this.commandQueue = []; this.queueLock = false; this._secureSessionId = 0; this._secureWrapperSeq = 0; this._secureTunnelSeq = 0; this._secureUserId = 2; this._secureGroupKeys = new Map(); this._secureSendSeq48 = 0n; this._secureSerial = Buffer.from('000000000000', 'hex'); this._secureAssignedIa = 0; this._secureCandidateIAs = []; this._secureCandidateIndex = 0; this._secureSearchProbed = new Set(); this._secureRoutingTimerOffsetMs = 0; this._secureRoutingTimerAuthenticated = false; this._secureRoutingLatencyMs = 1000; this.timers = new Map(); this.commandQueue = []; this.exitProcessingKNXQueueLoop = false; if (options === undefined) { options = optionsDefaults; } else { options = { ...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; 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 (this._options.physAddr !== '') { 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.isSerialTransport()) { this.socketReady = false; return; } if (this._options.hostProtocol === 'TunnelUDP') { this._clientSocket = dgram_1.default.createSocket({ type: 'udp4', reuseAddr: true, }); this.udpSocket.on(SocketEvents.message, (msg, rinfo) => { try { this.processInboundMessage(msg, rinfo); } catch (e) { this.emit(KNXClientEvents.error, e instanceof Error ? e : new Error('UDP data error')); } }); 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; if (this._connectionState !== ConncetionState.DISCONNECTING && this._connectionState !== ConncetionState.DISCONNECTED) { try { this.setDisconnected('Socket closed by peer').catch(() => { }); } catch { } } this.emit(KNXClientEvents.close); }); this.udpSocket.on(SocketEvents.listening, () => { this.socketReady = true; this.handleKNXQueue(); }); this.udpSocket.bind({ address: this._options.localIPAddress, }, () => { try { try { this.udpSocket.setMulticastInterface(this._options.localIPAddress); this.udpSocket.setMulticastTTL(55); } catch { } this.udpSocket.setTTL(55); 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.initTcpSocket(); } 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(); if (this._connectionState === ConncetionState.CONNECTING && !this._options.isSecureKNXEnabled) { this._connectionState = ConncetionState.CONNECTED; this._numFailedTelegramACK = 0; this.clearToSend = true; this._clientTunnelSeqNumber = -1; this.emit(KNXClientEvents.connected, this._options); } if (this._options.hostProtocol === 'Multicast' && this._options.isSecureKNXEnabled) { try { clearTimeout(this._secureRoutingSyncTimer); } catch { } this._secureRoutingSyncTimer = setTimeout(() => { try { if (!this._secureBackboneKey) this.secureEnsureKeyring(); this.secureSendTimerNotify(); } catch { } }, 120); } }); this.udpSocket.on(SocketEvents.message, (msg, rinfo) => { try { if (this._options.isSecureKNXEnabled) { this.secureOnUdpData(msg, rinfo); return; } this.processInboundMessage(msg, rinfo); } catch (e) { this.emit(KNXClientEvents.error, e instanceof Error ? e : new Error('UDP data error')); } }); 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; if (this._connectionState !== ConncetionState.DISCONNECTING && this._connectionState !== ConncetionState.DISCONNECTED) { try { this.setDisconnected('Socket closed by peer').catch(() => { }); } catch { } } 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(55); this.udpSocket.setMulticastInterface(this._options.localIPAddress); try { this.udpSocket.setMulticastLoopback(true); } catch { } this.sysLogger.debug(`[${(0, utils_1.getTimestamp)()}] Multicast socket bound on ${this._options.localIPAddress || '0.0.0.0'}:${this._peerPort}`); } catch (error) { this.sysLogger.error(`Multicast: Error setting SetTTL ${error.message}` || ''); } try { this.udpSocket.addMembership(this._peerHost, this._options.localIPAddress); this.sysLogger.debug(`[${(0, utils_1.getTimestamp)()}] Joined multicast group ${this._peerHost} on ${this._options.localIPAddress}`); } catch (err) { this.sysLogger.error('Multicast: cannot add membership (%s)', err); this.emit(KNXClientEvents.error, err); } }); } } initTcpSocket() { this._clientSocket = new net_1.default.Socket(); this._tcpRxBuffer = Buffer.alloc(0); this.tcpSocket.on('connect', () => { this.socketReady = true; this.exitProcessingKNXQueueLoop = false; this.secureStartSession().catch((err) => { this.emit(KNXClientEvents.error, err); }); }); this.tcpSocket.on('data', (data) => { try { this.secureOnTcpData(data); } catch (e) { this.emit(KNXClientEvents.error, e instanceof Error ? e : new Error('TCP data error')); } }); this.tcpSocket.on('error', (error) => { this.socketReady = false; this.emit(KNXClientEvents.error, error); }); this.tcpSocket.on('close', () => { this.socketReady = false; this.exitProcessingKNXQueueLoop = true; try { this.sysLogger.debug(`[${(0, utils_1.getTimestamp)()}] TCP close: set exitProcessingKNXQueueLoop=true`); } catch { } if (this._connectionState !== ConncetionState.DISCONNECTING && this._connectionState !== ConncetionState.DISCONNECTED) { try { this.setDisconnected('Socket closed by peer').catch(() => { }); } catch { } } this.emit(KNXClientEvents.close); }); } async connectSerialTransport() { const options = { ...(this._options.serialInterface || {}), }; this._peerHost = options.path || '/dev/ttyAMA0'; this._peerPort = 0; if (this._options.isSecureKNXEnabled) { try { await this.secureEnsureKeyring(); } catch (err) { try { this.sysLogger.error(`[${(0, utils_1.getTimestamp)()}] Serial FT1.2: secure keyring error: ${err.message}`); } catch { } } } this._serialDriver = new SerialFT12_1.default(options); this._serialDriver.on('cemi', (payload) => this.handleSerialCemi(payload)); this._serialDriver.on('error', (err) => { this.emit(KNXClientEvents.error, err instanceof Error ? err : new Error(String(err))); }); this._serialDriver.on('close', () => { if (this.isSerialTransport()) { this.socketReady = false; if (this._connectionState !== ConncetionState.DISCONNECTED && this._connectionState !== ConncetionState.DISCONNECTING) { this.setDisconnected('Serial FT1.2 port closed').catch(() => { }); } } }); await this._serialDriver.open(); } async closeSerialTransport() { if (!this._serialDriver) return; try { await this._serialDriver.close(); } catch (error) { this.sysLogger.warn(`[${(0, utils_1.getTimestamp)()}] Serial FT1.2 close error: ${error.message}`); } finally { this._serialDriver = undefined; this.socketReady = false; } } handleSerialCemi(payload) { try { if (!payload || payload.length < 2) return; const msgCode = payload.readUInt8(0); switch (msgCode) { case CEMIConstants_1.default.L_DATA_IND: { const cemi = CEMIFactory_1.default.createFromBuffer(msgCode, payload, 1); this.ensurePlainCEMI(cemi); this.emit(KNXClientEvents.indication, new KNXRoutingIndication_1.default(cemi), false); break; } case CEMIConstants_1.default.L_DATA_CON: { break; } } } catch (error) { this.sysLogger.error(`[${(0, utils_1.getTimestamp)()}] Serial FT1.2 parse error: ${error.message}`); } } extractCemiMessage(packet) { const cemi = packet?.cEMIMessage; if (!cemi) { throw new Error('KNX packet does not contain a cEMI message'); } return cemi; } 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.isSerialTransport()) { try { const cemi = this.extractCemiMessage(_knxPacket); if (this._options.isSecureKNXEnabled && _knxPacket instanceof KNXRoutingIndication_1.default) { this.maybeApplyDataSecure(cemi); try { _knxPacket.length = cemi.length ?? _knxPacket.length; } catch { } } this._serialDriver ?.sendCemiPayload(cemi.toBuffer()) .then(() => resolve(true)) .catch((error) => { this.emit(KNXClientEvents.error, error instanceof Error ? error : new Error(String(error))); resolve(false); }); } catch (error) { this.emit(KNXClientEvents.error, error instanceof Error ? error : new Error(String(error))); resolve(false); } return; } if (this._options.hostProtocol === 'Multicast' || this._options.hostProtocol === 'TunnelUDP') { try { try { if (this._options.hostProtocol === 'Multicast' && this._options.isSecureKNXEnabled && _knxPacket instanceof KNXRoutingIndication_1.default) { const kri = _knxPacket; this.maybeApplyDataSecure(kri.cEMIMessage); try { kri.length = kri.cEMIMessage?.length ?? kri.length; kri.header.length = KNXConstants_1.KNX_CONSTANTS.HEADER_SIZE_10 + kri.length; } catch { } } } catch { } let outBuf = _knxPacket.toBuffer(); if (this._options.hostProtocol === 'Multicast' && this._options.isSecureKNXEnabled && (_knxPacket instanceof KNXRoutingIndication_1.default || _knxPacket?.header?.service_type === KNXConstants_1.KNX_CONSTANTS.ROUTING_INDICATION)) { try { outBuf = this.secureWrapRouting(outBuf); if (this.isLevelEnabled('debug')) { this.sysLogger.debug(`[${(0, utils_1.getTimestamp)()}] TX 0950 SecureWrapper (routing) len=${outBuf.length}`); } } catch (e) { this.sysLogger.error(`Secure multicast wrap error: ${e.message}`); } } this.udpSocket.send(outBuf, 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?.seqCounter}`); this.emit(KNXClientEvents.error, error); resolve(false); } } else if (this._options.hostProtocol === 'TunnelTCP') { try { if (this._options.isSecureKNXEnabled && _knxPacket instanceof KNXTunnelingRequest_1.default && _knxPacket.cEMIMessage?.msgCode === CEMIConstants_1.default.L_DATA_REQ) { this.maybeApplyDataSecure(_knxPacket.cEMIMessage); try { const ktr = _knxPacket; const cemiLen = ktr.cEMIMessage?.length ?? 0; ktr.header.length = KNXConstants_1.KNX_CONSTANTS.HEADER_SIZE_10 + (4 + cemiLen); } catch { } } try { if (_knxPacket instanceof KNXTunnelingRequest_1.default) { const ktr = _knxPacket; const cemi = ktr?.cEMIMessage; const dstStr = cemi?.dstAddress?.toString?.(); const srcStr = cemi?.srcAddress?.toString?.(); const ctrlBuf = cemi?.control?.toBuffer?.(); const flags16 = Buffer.isBuffer(ctrlBuf) ? (ctrlBuf[0] << 8) | ctrlBuf[1] : undefined; const isSecApdu = !!(cemi?.npdu && (cemi.npdu.tpci & 0xff) === secure_knx_constants_1.APCI_SEC.HIGH && (cemi.npdu.apci & 0xff) === secure_knx_constants_1.APCI_SEC.LOW); let scf; let seq48Hex; if (isSecApdu) { const dbuf = cemi.npdu.dataBuffer?.value; if (Buffer.isBuffer(dbuf) && dbuf.length >= 1 + secure_knx_constants_1.SECURE_SEQ_LEN) { scf = dbuf[0]; const seq = dbuf.subarray(1, 1 + secure_knx_constants_1.SECURE_SEQ_LEN); seq48Hex = seq.toString('hex'); } } this.sysLogger.debug(`[${(0, utils_1.getTimestamp)()}] ` + `TX TunnelTCP: dst=${dstStr} src=${srcStr} flags=0x${(flags16 ?? 0).toString(16)} dataSecure=${isSecApdu} scf=${typeof scf === 'number' ? scf : 'n/a'} seq48=${seq48Hex ?? 'n/a'}`); try { if (this.isLevelEnabled('debug')) { const innerHex = ktr .toBuffer() .toString('hex'); this.sysLogger.debug(`[${(0, utils_1.getTimestamp)()}] TX inner (KNX/IP TunnelReq): ${innerHex}`); } } catch { } } } catch { } const inner = _knxPacket.toBuffer(); const payload = this._options.isSecureKNXEnabled ? this.secureWrap(inner) : inner; this.tcpSocket.write(payload, (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(); if (this._options.hostProtocol === 'Multicast' && this._options.isSecureKNXEnabled && (this._options.secureRoutingWaitForTimer ?? true) && !this._secureRoutingTimerAuthenticated && item.knxPacket instanceof KNXRoutingIndication_1.default) { try { this.sysLogger.debug(`[${(0, utils_1.getTimestamp)()}] Secure multicast: waiting timer auth, deferring 0950 send`); } catch { } this.commandQueue.push(item); await (0, utils_1.wait)(200); continue; } this.currentItemHandledByTheQueue = item; try { if (this._options.hostProtocol === 'TunnelTCP') { if (item.knxPacket instanceof KNXTunnelingRequest_1.default) { const ktr = item.knxPacket; const seq = this.secureIncTunnelSeq(); ktr.seqCounter = seq; if (item.ACK) { item.expectedSeqNumberForACK = seq; } try { this.sysLogger.debug(`[${(0, utils_1.getTimestamp)()}] Assign tunnel seq=${seq} ch=${ktr?.channelID} dst=${ktr?.cEMIMessage?.dstAddress?.toString?.()}`); } catch { } } } } catch { } if (item.ACK !== undefined && this._options.hostProtocol !== 'TunnelTCP') { 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) { 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:${toBeAdded.ACK?.channelID || 'filled later'} seqCounter:${toBeAdded.ACK?.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 if (this.isSerialTransport()) { const cEMIMessage = CEMIFactory_1.default.newLDataRequestMessage('write', srcAddress, dstAddress, knxBuffer); 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 knxPacketRequest = KNXProtocol_1.default.newKNXRoutingIndication(cEMIMessage); this.send(knxPacketRequest, undefined, false, this.getSeqNumber()); this.ensurePlainCEMI(cEMIMessage); this.emit(KNXClientEvents.indication, knxPacketRequest, true); } else { const cEMIMessage = CEMIFactory_1.default.newLDataRequestMessage('write', srcAddress, dstAddress, knxBuffer); cEMIMessage.control.ack = this._options.hostProtocol === 'TunnelTCP' ? 0 : 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._options.hostProtocol === 'TunnelTCP' ? 0 : this.incSeqNumber(); const knxPacketRequest = KNXProtocol_1.default.newKNXTunnelingRequest(this._channelID, seqNum, cEMIMessage); if (!this._options.suppress_ack_ldatareq) { this.send(knxPacketRequest, knxPacketRequest, false, seqNum); } else { this.send(knxPacketRequest, undefined, false, seqNum); } this.ensurePlainCEMI(knxPacketRequest.cEMIMessage); 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 if (this.isSerialTransport()) { 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 knxPacketRequest = KNXProtocol_1.default.newKNXRoutingIndication(cEMIMessage); this.send(knxPacketRequest, undefined, false, this.getSeqNumber()); this.ensurePlainCEMI(cEMIMessage); this.emit(KNXClientEvents.indication, knxPacketRequest, true); } 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._options.hostProtocol === 'TunnelTCP' ? 0 : this.incSeqNumber(); const knxPacketRequest = KNXProtocol_1.default.newKNXTunnelingRequest(this._channelID, seqNum, cEMIMessage); if (!this._options.suppress_ack_ldatareq) { this.send(knxPacketRequest, knxPacketRequest, false, seqNum); } else { this.send(knxPacketRequest, undefined, false, seqNum); } this.ensurePlainCEMI(knxPacketRequest.cEMIMessage); 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 if (this.isSerialTransport()) { const cEMIMessage = CEMIFactory_1.default.newLDataRequestMessage('read', srcAddress, dstAddress, null); 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 knxPacketRequest = KNXProtocol_1.default.newKNXRoutingIndication(cEMIMessage); this.send(knxPacketRequest, undefined, false, this.getSeqNumber()); this.ensurePlainCEMI(cEMIMessage); this.emit(KNXClientEvents.indication, knxPacketRequest, true); } else { const cEMIMessage = CEMIFactory_1.default.newLDataRequestMessage('read', srcAddress, dstAddress, null); cEMIMessage.control.ack = this._options.hostProtocol === 'TunnelTCP' ? 0 : 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._options.hostProtocol === 'TunnelTCP' ? 0 : this.incSeqNumber(); const knxPacketRequest = KNXProtocol_1.default.newKNXTunnelingRequest(this._channelID, seqNum, cEMIMessage); if (!this._options.suppress_ack_ldatareq) { this.send(knxPacketRequest, knxPacketRequest, false, seqNum); } else { this.send(knxPacketRequest, undefined, false, seqNum); } this.ensurePlainCEMI(knxPacketRequest.cEMIMessage); 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 if (this.isSerialTransport()) { 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 knxPacketRequest = KNXProtocol_1.default.newKNXRoutingIndication(cEMIMessage); this.send(knxPacketRequest, undefined, false, this.getSeqNumber()); this.ensurePlainCEMI(cEMIMessage); this.emit(KNXClientEvents.indication, knxPacketRequest, true); } else { const cEMIMessage = CEMIFactory_1.default.newLDataRequestMessage('write', srcAddress, dstAddress, data); if (this._options.hostProtocol === 'TunnelTCP') { cEMIMessage.control.ack = 0; } else { 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._options.hostProtocol === 'TunnelTCP' ? this.secureIncTunnelSeq() : this.incSeqNumber(); const knxPacketRequest = KNXProtocol_1.default.newKNXTunnelingRequest(this._channelID, seqNum, cEMIMessage); if (!this._options.suppress_ack_ldatareq) { this.send(knxPacketRequest, knxPacketRequest, false, seqNum); } else { this.send(knxPacketRequest, undefined, false, seqNum); } this.ensurePlainCEMI(knxPacketRequest.cEMIMessage); this.emit(KNXClientEvents.indication, knxPacketRequest, true); } } startHeartBeat() { this.stopHeartBeat(); this._heartbeatFailures = 0; this._heartbeatRunning = true; this.runHeartbeat();