knxultimate
Version:
KNX IP protocol implementation for Node. This is the ENGINE of Node-Red KNX-Ultimate node.
1,029 lines • 54.8 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 (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