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
JavaScript
"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();