node-red-contrib-knx-ultimate
Version:
Control your KNX intallation via Node-Red! Single Node KNX IN/OUT with optional ETS group address importer. Easy to use and highly configurable.
997 lines (885 loc) • 54.3 kB
JavaScript
// Made with love by Supergiovane
"use strict";
const EventEmitter = require("events");
const dgram = require("dgram");
const net = require('net')
const KNXConstants = require("./protocol/KNXConstants");
const CEMIConstants = require("./protocol/cEMI/CEMIConstants");
const CEMIFactory = require("./protocol/cEMI/CEMIFactory");
const KNXProtocol = require("./protocol/KNXProtocol");
const KNXConnectResponse = require("./protocol/KNXConnectResponse");
const HPAI = require("./protocol/HPAI");
const TunnelCRI = require("./protocol/TunnelCRI");
const KNXConnectionStateResponse = require("./protocol/KNXConnectionStateResponse");
const errors = require("./errors");
const ipAddressHelper = require("./util/ipAddressHelper");
const KNXAddress = require("./protocol/KNXAddress").KNXAddress;
const KNXDataBuffer = require("./protocol/KNXDataBuffer").KNXDataBuffer;
const DPTLib = require('./dptlib');
//const KNXsecureKeyring = require("./KNXsecureKeyring.js");
//const lodash = require("lodash");
var STATE;
(function (STATE) {
STATE[STATE["STARTED"] = 0] = "STARTED";
STATE[STATE["CONNECTING"] = 3] = "CONNECTING";
STATE[STATE["CONNECTED"] = 4] = "CONNECTED";
STATE[STATE["DISCONNECTING"] = 5] = "DISCONNECTING";
STATE[STATE["DISCONNECTED"] = 6] = "DISCONNECTED";
})(STATE || (STATE = {}));
var TUNNELSTATE;
(function (TUNNELSTATE) {
TUNNELSTATE[TUNNELSTATE["READY"] = 0] = "READY";
})(TUNNELSTATE || (TUNNELSTATE = {}));
const SocketEvents = {
error: 'error',
message: 'message',
listening: "listening",
data: "data",
close: "close"
};
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 || (KNXClientEvents = {}));
// const KNXClientEvents = {
// error: "error",
// disconnected: "disconnected",
// discover: "discover",
// indication: "indication",
// connected: "connected",
// ready: "ready",
// response: "response",
// connecting: "connecting"
// };
// Contains the decrypted keyring file
var jKNXSecureKeyring = "";
// options:
const optionsDefaults = {
physAddr: '15.15.200',
connectionKeepAliveTimeout: KNXConstants.KNX_CONSTANTS.CONNECTION_ALIVE_TIME,
ipAddr: "224.0.23.12",
ipPort: 3671,
hostProtocol: "TunnelUDP", // TunnelUDP, TunnelTCP, Multicast
isSecureKNXEnabled: false,
suppress_ack_ldatareq: false,
loglevel: "info",
localEchoInTunneling: true,
localIPAddress: "",
interface: "",
jKNXSecureKeyring: {}
};
class KNXClient extends EventEmitter {
constructor(options) {
if (options === undefined) {
options = optionsDefaults;
}
super();
this._clientTunnelSeqNumber = -1;
this._options = options;//Object.assign(optionsDefaults, options);
this._options.connectionKeepAliveTimeout = KNXConstants.KNX_CONSTANTS.CONNECTION_ALIVE_TIME;
//this._localPort = null;
this._peerHost = this._options.ipAddr;
this._peerPort = this._options.ipPort;
this._connectionTimeoutTimer = null;
this._heartbeatFailures = 0;
this.max_HeartbeatFailures = 3;
this._heartbeatTimer = null;
this._discovery_timer = null;
this._awaitingResponseType = null;
this._processInboundMessage = this._processInboundMessage.bind(this);
this._clientSocket = null;
this.sysLogger = null;
jKNXSecureKeyring = this._options.jKNXSecureKeyring; // 28/12/2021 Contains the Keyring JSON object
try {
this.sysLogger = require("./KnxLog.js").get({ loglevel: this._options.loglevel }); // 08/04/2021 new logger to adhere to the loglevel selected in the config-window
} catch (error) {
console.log("BANANA ERRORE this.sysLogger = require", error.message);
throw (error);
}
if (typeof this._options.physAddr === "string") this._options.physAddr = KNXAddress.createFromString(this._options.physAddr);
try {
this._options.localIPAddress = ipAddressHelper.getLocalAddress(this._options.interface); // Get the local address of the selected interface
} catch (error) {
if (this.sysLogger !== undefined && this.sysLogger !== null) this.sysLogger.error("ipAddressHelper.getLocalAddress:" + error.message);
throw (error);
}
// 12/03/2022 Remove all listeners
this.removeAllListeners();
// 07/12/2021 Based on protocol instantiate the right socket
if (this._options.hostProtocol === "TunnelUDP") {
this._clientSocket = dgram.createSocket({ type: 'udp4', reuseAddr: false });
this._clientSocket.removeAllListeners(); // 12/03/2022 Remove all listeners
this._clientSocket.on(SocketEvents.message, this._processInboundMessage);
this._clientSocket.on(SocketEvents.error, error => this.emit(KNXClientEvents.error, error));
this._clientSocket.on(SocketEvents.close, info => this.emit(KNXClientEvents.close, info));
let conn = this;
this._clientSocket.bind({ address: this._options.localIPAddress, port: this._options._peerPort }, function () {
try {
conn._clientSocket.setTTL(128);
} catch (error) {
if (conn.sysLogger !== undefined && conn.sysLogger !== null) conn.sysLogger.error("UDP: Error setting SetTTL " + error.message || "");
}
conn = null;
});
} else if (this._options.hostProtocol === "TunnelTCP") {
// TCP
this._clientSocket = new net.Socket();
this._clientSocket.removeAllListeners(); // 12/03/2022 Remove all listeners
//this._clientSocket.on(SocketEvents.data, this._processInboundMessage);
this._clientSocket.on(SocketEvents.data, function (msg, rinfo, callback) {
console.log(msg, rinfo, callback);
});
this._clientSocket.on(SocketEvents.error, error => this.emit(KNXClientEvents.error, error));
this._clientSocket.on(SocketEvents.close, info => this.emit(KNXClientEvents.close, info));
} else if (this._options.hostProtocol === "Multicast") {
this._clientSocket = dgram.createSocket({ type: 'udp4', reuseAddr: true });
this._clientSocket.removeAllListeners(); // 12/03/2022 Remove all listeners
this._clientSocket.on(SocketEvents.listening, function () {
});
let conn = this;
this._clientSocket.on(SocketEvents.message, this._processInboundMessage);
this._clientSocket.on(SocketEvents.error, error => this.emit(KNXClientEvents.error, error));
this._clientSocket.on(SocketEvents.close, info => this.emit(KNXClientEvents.close, info));
this._clientSocket.bind(this._peerPort, function () {
try {
conn._clientSocket.setMulticastTTL(128);
conn._clientSocket.setMulticastInterface(conn._options.localIPAddress);
} catch (error) {
if (conn.sysLogger !== undefined && conn.sysLogger !== null) conn.sysLogger.error("Multicast: Error setting SetTTL " + error.message || "");
}
try {
conn._clientSocket.addMembership(conn._peerHost, conn._options.localIPAddress);
} catch (err) {
if (conn.sysLogger !== undefined && conn.sysLogger !== null) conn.sysLogger.error("Multicast: cannot add membership (%s)", err);
try {
conn.emit(KNXClientEvents.error, err);
} catch (error) { }
conn = null;
return;
}
conn = null;
//this._localPort = this._clientSocket.address().port;// 07/12/2021 Get the local port used bu the socket
});
}
this._clientTunnelSeqNumber = -1;
this._channelID = null;
this._connectionState = STATE.DISCONNECTED;
this._timerWaitingForACK = null;
this._numFailedTelegramACK = 0; // 25/12/2021 Keep count of the failed tunnelig ACK telegrams
}
get channelID() {
return this._channelID;
}
// 16/12/2021 Transform the plain value "data" into a KNXDataBuffer. The datapoints without "null" are SixBits
// dataPointsSixBits = {
// DPT1,
// DPT2,
// DPT3,
// DPT4: null,
// DPT5,
// DPT6: null,
// DPT7: null,
// DPT8: null,
// DPT9,
// DPT10,
// DPT11,
// DPT12: null,
// DPT13: null,
// DPT14,
// DPT15: null,
// DPT16: null,
// DPT17: null,
// DPT18,
// DPT19: null,
// DPT20: null
// };
getKNXDataBuffer(_data, _dptid) {
let adpu = {};
DPTLib.populateAPDU(_data, adpu, _dptid);
let iDatapointType = parseInt(_dptid.substr(0, _dptid.indexOf(".")));
let isSixBits = adpu.bitlength <= 6;
//let isSixBits = [1,2,3,5,9,10,11,14,18].includes(iDatapointType);
//console.log("isSixBits", isSixBits, "Includes (should be = isSixBits)", [1, 2, 3, 5, 9, 10, 11, 14, 18].includes(iDatapointType), "ADPU BitLenght", adpu.bitlength);
try {
if (this.sysLogger !== undefined && this.sysLogger !== null) this.sysLogger.trace("isSixBits:" + isSixBits + " Includes (should be = isSixBits):" + [1, 2, 3, 5, 9, 10, 11, 14, 18].includes(iDatapointType) + " ADPU BitLenght:" + adpu.bitlength);
} catch (error) { }
let IDataPoint = {
id: "",
value: "any",
//type: { type: adpu.bitlength.toString() || null },
type: { type: isSixBits },
bind: null,
read: () => null,
write: null
}
return new KNXDataBuffer(adpu.data, IDataPoint);
}
// bindSocketPortAsync(port = KNXConstants.KNX_CONSTANTS.KNX_PORT, host = '0.0.0.0') {
// return new Promise((resolve, reject) => {
// try {
// this._clientSocket.bind(port, host, () => {
// this._clientSocket.setMulticastInterface(host);
// this._clientSocket.setMulticastTTL(128);
// this._options.localIPAddress = host;
// resolve();
// });
// }
// catch (err) {
// reject(err);
// }
// });
// }
send(knxPacket) {
// Logging
if (this.sysLogger !== undefined && this.sysLogger !== null) {
try {
if (knxPacket.constructor.name !== undefined && knxPacket.constructor.name.toLowerCase() === "knxconnectrequest") this.sysLogger.debug("Sending KNX packet: " + knxPacket.constructor.name + " Host:" + this._peerHost + ":" + this._peerPort);
if (knxPacket.constructor.name !== undefined && (knxPacket.constructor.name.toLowerCase() === "knxtunnelingrequest" || knxPacket.constructor.name.toLowerCase() === "knxroutingindication")) {
let sTPCI = ""
if (knxPacket.cEMIMessage.npdu.isGroupRead) sTPCI = "Read";
if (knxPacket.cEMIMessage.npdu.isGroupResponse) sTPCI = "Response";
if (knxPacket.cEMIMessage.npdu.isGroupWrite) sTPCI = "Write";
// Composing debug string
let sDebugString = "???";
try {
sDebugString = "Data: " + JSON.stringify(knxPacket.cEMIMessage.npdu);
sDebugString += " srcAddress: " + knxPacket.cEMIMessage.srcAddress.toString();
sDebugString += " dstAddress: " + knxPacket.cEMIMessage.dstAddress.toString();
} catch (error) { }
this.sysLogger.debug("Sending KNX packet: " + knxPacket.constructor.name + " " + sDebugString + " Host:" + this._peerHost + ":" + this._peerPort + " channelID:" + knxPacket.channelID + " seqCounter:" + knxPacket.seqCounter + " Dest:" + knxPacket.cEMIMessage.dstAddress.toString(), " Data:" + knxPacket.cEMIMessage.npdu.dataValue.toString("hex") + " TPCI:" + sTPCI);
}
} catch (error) {
}
}
// Real send to KNX wires
if (this._options.hostProtocol === "Multicast" || this._options.hostProtocol === "TunnelUDP") {
// UDP
try {
this._clientSocket.send(knxPacket.toBuffer(), this._peerPort, this._peerHost, err => {
if (err) {
if (this.sysLogger !== undefined && this.sysLogger !== null) this.sysLogger.error("Sending KNX packet: Send UDP sending error: " + err.message || "Undef error");
try {
this.emit(KNXClientEvents.error, err);
} catch (error) {
}
}
});
} catch (error) {
if (this.sysLogger !== undefined && this.sysLogger !== null) this.sysLogger.error("Sending KNX packet: Send UDP Catch error: " + error.message + " " + typeof (knxPacket) + " seqCounter:" + knxPacket.seqCounter);
try {
this.emit(KNXClientEvents.error, error);
} catch (error) {
}
}
} else {
// TCP
try {
this._clientSocket.write(knxPacket.toBuffer(), err => {
if (err) {
if (this.sysLogger !== undefined && this.sysLogger !== null) this.sysLogger.error("Sending KNX packet: Send TCP sending error: " + err.message || "Undef error");
this.emit(KNXClientEvents.error, err);
}
});
} catch (error) {
if (this.sysLogger !== undefined && this.sysLogger !== null) this.sysLogger.error("Sending KNX packet: Send TCP Catch error: " + error.message || "Undef error");
try {
this.emit(KNXClientEvents.error, error);
} catch (error) { }
}
}
}
/**
*
* @param {KNXAddress} dstAddress
* @param {KNXDataBuffer} data
*/
// sendWriteRequest(dstAddress, data) {
write(dstAddress, data, dptid) {
if (this._connectionState !== STATE.CONNECTED) throw new Error("The socket is not connected. Unable to access the KNX BUS");
// Get the Data Buffer from the plain value
data = this.getKNXDataBuffer(data, dptid);
if (typeof dstAddress === "string") dstAddress = KNXAddress.createFromString(dstAddress, KNXAddress.TYPE_GROUP);
let srcAddress = this._options.physAddr;
if (this._options.hostProtocol === "Multicast") {
// Multicast
const cEMIMessage = CEMIFactory.CEMIFactory.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.KNXProtocol.newKNXRoutingIndication(cEMIMessage);
this.send(knxPacketRequest);
// 06/12/2021 Multivast automaticalli echoes telegrams
} else {
// Tunneling
const cEMIMessage = CEMIFactory.CEMIFactory.newLDataRequestMessage("write", srcAddress, dstAddress, data);
//cEMIMessage.control.ack = this._options.suppress_ack_ldatareq ? 0 : 1; // No ack like telegram sent from ETS (0 means don't care)
cEMIMessage.control.ack = 0;// No ack like telegram sent from ETS (0 means don't care)
cEMIMessage.control.broadcast = 1;
cEMIMessage.control.priority = 3;
cEMIMessage.control.addressType = 1;
cEMIMessage.control.hopCount = 6;
const seqNum = this._incSeqNumber(); // 26/12/2021
const knxPacketRequest = KNXProtocol.KNXProtocol.newKNXTunnelingRequest(this._channelID, seqNum, cEMIMessage);
if (!this._options.suppress_ack_ldatareq) this._setTimerWaitingForACK(knxPacketRequest);
//if (this.sysLogger !== undefined && this.sysLogger !== null) this.sysLogger.error("this._tunnelReqTimer "+ this._tunnelReqTimer.size);
this.send(knxPacketRequest);
// 06/12/2021 Echo the sent telegram. Last parameter is the echo true/false
try {
if (this._options.localEchoInTunneling) this.emit(KNXClientEvents.indication, knxPacketRequest, true);
} catch (error) {
}
}
}
// sendResponseRequest
respond(dstAddress, data, dptid) {
if (this._connectionState !== STATE.CONNECTED) throw new Error("The socket is not connected. Unable to access the KNX BUS");
// Get the Data Buffer from the plain value
data = this.getKNXDataBuffer(data, dptid);
if (typeof dstAddress === "string") dstAddress = KNXAddress.createFromString(dstAddress, KNXAddress.TYPE_GROUP);
let srcAddress = this._options.physAddr;
if (this._options.hostProtocol === "Multicast") {
// Multicast
const cEMIMessage = CEMIFactory.CEMIFactory.newLDataIndicationMessage("response", srcAddress, dstAddress, data);
cEMIMessage.control.ack = 0; // No ack like telegram sent from ETS (0 means don't care)
cEMIMessage.control.broadcast = 1;
cEMIMessage.control.priority = 3;
cEMIMessage.control.addressType = 1;
cEMIMessage.control.hopCount = 6;
const knxPacketRequest = KNXProtocol.KNXProtocol.newKNXRoutingIndication(cEMIMessage);
this.send(knxPacketRequest);
// 06/12/2021 Multivast automaticalli echoes telegrams
} else {
// Tunneling
const cEMIMessage = CEMIFactory.CEMIFactory.newLDataRequestMessage("response", srcAddress, dstAddress, data);
//cEMIMessage.control.ack = this._options.suppress_ack_ldatareq ? 0 : 1;
cEMIMessage.control.ack = 0;// No ack like telegram sent from ETS (0 means don't care)
cEMIMessage.control.broadcast = 1;
cEMIMessage.control.priority = 3;
cEMIMessage.control.addressType = 1;
cEMIMessage.control.hopCount = 6;
const seqNum = this._incSeqNumber(); // 26/12/2021
const knxPacketRequest = KNXProtocol.KNXProtocol.newKNXTunnelingRequest(this._channelID, seqNum, cEMIMessage);
if (!this._options.suppress_ack_ldatareq) this._setTimerWaitingForACK(knxPacketRequest);
this.send(knxPacketRequest);
// 06/12/2021 Echo the sent telegram. Last parameter is the echo true/false
try {
if (this._options.localEchoInTunneling) this.emit(KNXClientEvents.indication, knxPacketRequest, true);
} catch (error) {
}
}
}
// sendReadRequest
read(dstAddress) {
if (this._connectionState !== STATE.CONNECTED) throw new Error("The socket is not connected. Unable to access the KNX BUS");
if (typeof dstAddress === "string") dstAddress = KNXAddress.createFromString(dstAddress, KNXAddress.TYPE_GROUP);
let srcAddress = this._options.physAddr;
if (this._options.hostProtocol === "Multicast") {
// Multicast
const cEMIMessage = CEMIFactory.CEMIFactory.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.KNXProtocol.newKNXRoutingIndication(cEMIMessage);
this.send(knxPacketRequest);
// 06/12/2021 Multivast automaticalli echoes telegrams
} else {
// Tunneling
const cEMIMessage = CEMIFactory.CEMIFactory.newLDataRequestMessage("read", srcAddress, dstAddress, null);
//cEMIMessage.control.ack = this._options.suppress_ack_ldatareq ? 0 : 1;
cEMIMessage.control.ack = 0;// No ack like telegram sent from ETS (0 means don't care)
cEMIMessage.control.broadcast = 1;
cEMIMessage.control.priority = 3;
cEMIMessage.control.addressType = 1;
cEMIMessage.control.hopCount = 6;
const seqNum = this._incSeqNumber(); // 26/12/2021
const knxPacketRequest = KNXProtocol.KNXProtocol.newKNXTunnelingRequest(this._channelID, seqNum, cEMIMessage);
if (!this._options.suppress_ack_ldatareq) this._setTimerWaitingForACK(knxPacketRequest);
this.send(knxPacketRequest);
// 06/12/2021 Echo the sent telegram. Last parameter is the echo true/false
try {
if (this._options.localEchoInTunneling) this.emit(KNXClientEvents.indication, knxPacketRequest, true);
} catch (error) {
}
}
}
writeRaw(dstAddress, _rawDataBuffer, bitlength) {
// bitlength is unused and only for backward compatibility
if (this._connectionState !== STATE.CONNECTED) throw new Error("The socket is not connected. Unable to access the KNX BUS");
if (!Buffer.isBuffer(_rawDataBuffer)) {
if (this.sysLogger !== undefined && this.sysLogger !== null) this.sysLogger.error('KNXClient: writeRaw: Value must be a buffer! ');
return
}
let isSixBits = bitlength <= 6;
let IDataPoint = {
id: "",
value: "any",
type: { type: isSixBits },
bind: null,
read: () => null,
write: null
}
// Get the KNDDataBuffer
var baseBufferFromBitLenght = Buffer.alloc((bitlength / 8)); // The buffer lenght must be like specified by bitlenght
_rawDataBuffer.copy(baseBufferFromBitLenght, 0);
let data = new KNXDataBuffer(baseBufferFromBitLenght, IDataPoint);
if (typeof dstAddress === "string") dstAddress = KNXAddress.createFromString(dstAddress, KNXAddress.TYPE_GROUP);
let srcAddress = this._options.physAddr;
if (this._options.hostProtocol === "Multicast") {
// Multicast
const cEMIMessage = CEMIFactory.CEMIFactory.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.KNXProtocol.newKNXRoutingIndication(cEMIMessage);
this.send(knxPacketRequest);
// 06/12/2021 Multivast automaticalli echoes telegrams
} else {
// Tunneling
const cEMIMessage = CEMIFactory.CEMIFactory.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(); // 26/12/2021
const knxPacketRequest = KNXProtocol.KNXProtocol.newKNXTunnelingRequest(this._channelID, seqNum, cEMIMessage);
if (!this._options.suppress_ack_ldatareq) this._setTimerWaitingForACK(knxPacketRequest);
this.send(knxPacketRequest);
// 06/12/2021 Echo the sent telegram. Last parameter is the echo true/false
try {
if (this._options.localEchoInTunneling) this.emit(KNXClientEvents.indication, knxPacketRequest, true);
} catch (error) {
}
}
}
startHeartBeat() {
this.stopHeartBeat();
this._heartbeatFailures = 0;
this._heartbeatRunning = true;
this._runHeartbeat();
}
stopHeartBeat() {
if (this._heartbeatTimer !== null) {
this._heartbeatRunning = false;
clearTimeout(this._heartbeatTimer);
}
}
// isDiscoveryRunning() {
// return this._discovery_timer != null;
// }
// startDiscovery() {
// if (this.isDiscoveryRunning()) {
// throw new Error('Discovery already running');
// }
// this._discovery_timer = setTimeout(() => {
// this._discovery_timer = null;
// }, 1000 * KNXConstants.KNX_CONSTANTS.SEARCH_TIMEOUT);
// this._sendSearchRequestMessage();
// }
// stopDiscovery() {
// if (!this.isDiscoveryRunning()) {
// return;
// }
// if (this._discovery_timer !== null) clearTimeout(this._discovery_timer);
// this._discovery_timer = null;
// }
// getDescription(host, port) {
// if (this._clientSocket == null) {
// throw new Error('No client socket defined');
// }
// this._connectionTimeoutTimer = setTimeout(() => {
// this._connectionTimeoutTimer = null;
// }, 1000 * KNXConstants.KNX_CONSTANTS.DEVICE_CONFIGURATION_REQUEST_TIMEOUT);
// this._awaitingResponseType = KNXConstants.KNX_CONSTANTS.DESCRIPTION_RESPONSE;
// this._sendDescriptionRequestMessage(host, port);
// }
Connect(knxLayer = TunnelCRI.TunnelTypes.TUNNEL_LINKLAYER) {
if (this._clientSocket == null) {
throw new Error('No client socket defined');
}
if (this._connectionState === STATE.DISCONNECTING) {
throw new Error('Socket is disconnecting. Please wait until disconnected.');
}
if (this._connectionState === STATE.CONNECTING) {
throw new Error('Socket is connecting. Please wait until connected.');
}
if (this._connectionState === STATE.CONNECTED) {
throw new Error('Socket is already connected. Disconnect first.');
}
this._connectionState = STATE.CONNECTING;
this._numFailedTelegramACK = 0; // 25/12/2021 Reset the failed ACK counter
this._clearToSend = true; // 26/12/2021 allow to send
if (this._connectionTimeoutTimer !== null) clearTimeout(this._connectionTimeoutTimer);
// Emit connecting
this.emit(KNXClientEvents.connecting, this._options);
if (this._options.hostProtocol === "TunnelUDP") {
// Unicast, need to explicitly create the connection
const timeoutError = new Error(`Connection timeout to ${this._peerHost}:${this._peerPort}`);
this._connectionTimeoutTimer = setTimeout(() => {
this._connectionTimeoutTimer = null;
try {
this.emit(KNXClientEvents.error, timeoutError);
} catch (error) {
}
}, 1000 * KNXConstants.KNX_CONSTANTS.CONNECT_REQUEST_TIMEOUT);
this._awaitingResponseType = KNXConstants.KNX_CONSTANTS.CONNECT_RESPONSE;
this._clientTunnelSeqNumber = -1;
try {
this._sendConnectRequestMessage(new TunnelCRI.TunnelCRI(knxLayer));
} catch (error) { }
} else if (this._options.hostProtocol === "TunnelTCP") {
// TCP
const timeoutError = new Error(`Connection timeout to ${this._peerHost}:${this._peerPort}`);
let conn = this;
this._clientSocket.connect(this._peerPort, this._peerHost, function () {
// conn._timer = setTimeout(() => {
// conn._timer = null;
// conn.emit(KNXClientEvents.error, timeoutError);
// }, 1000 * KNXConstants.KNX_CONSTANTS.CONNECT_REQUEST_TIMEOUT);
conn._awaitingResponseType = KNXConstants.KNX_CONSTANTS.CONNECT_RESPONSE;
conn._clientTunnelSeqNumber = 0;
if (conn._options.isSecureKNXEnabled) conn._sendSecureSessionRequestMessage(new TunnelCRI.TunnelCRI(knxLayer));
conn = null;
});
} else {
// Multicast
this._connectionState = STATE.CONNECTED;
// 16/03/2022 These two are referring to tunneling connection, but i set it here as well. Non si sa mai.
this._numFailedTelegramACK = 0; // 25/12/2021 Reset the failed ACK counter
this._clearToSend = true; // 26/12/2021 allow to send
this._clientTunnelSeqNumber = -1;
try {
this.emit(KNXClientEvents.connected, this._options);
} catch (error) {
}
}
}
getConnectionStatus() {
if (this._clientSocket == null) {
throw new Error('No client socket defined');
}
const timeoutError = new Error(`HeartBeat failure with ${this._peerHost}:${this._peerPort}`);
const deadError = new Error(`Connection dead with ${this._peerHost}:${this._peerPort}`);
this._heartbeatTimer = setTimeout(() => {
this._heartbeatTimer = null;
try {
if (this.sysLogger !== undefined && this.sysLogger !== null) this.sysLogger.error("KNXClient: getConnectionStatus Timeout " + this._heartbeatFailures + " out of " + this.max_HeartbeatFailures);
//this.emit(KNXClientEvents.error, timeoutError);
} catch (error) {
}
this._heartbeatFailures++;
if (this._heartbeatFailures >= this.max_HeartbeatFailures) {
this._heartbeatFailures = 0;
try {
this.emit(KNXClientEvents.error, deadError);
} catch (error) {
}
this._setDisconnected(deadError.message);
}
}, 1000 * KNXConstants.KNX_CONSTANTS.CONNECTIONSTATE_REQUEST_TIMEOUT);
this._awaitingResponseType = KNXConstants.KNX_CONSTANTS.CONNECTIONSTATE_RESPONSE;
try {
this._sendConnectionStateRequestMessage(this._channelID);
} catch (error) { }
}
Disconnect() {
if (this._clientSocket === null) {
throw new Error('No client socket defined');
}
// 20/04/2022 this._channelID === null can happen when the KNX Gateway is already disconnected
if (this._channelID === null) {
throw new Error('KNX Socket is already disconnected');
}
this.stopHeartBeat();
this._connectionState = STATE.DISCONNECTING;
this._awaitingResponseType = KNXConstants.KNX_CONSTANTS.DISCONNECT_RESPONSE;
try {
this._sendDisconnectRequestMessage(this._channelID);
} catch (error) {
}
// 12/03/2021 Set disconnected if not already set by DISCONNECT_RESPONSE sent from the IP Interface
let t = setTimeout(() => { // 21/03/2022 fixed possible memory leak. Previously was setTimeout without "let t = ".
if (this._connectionState !== STATE.DISCONNECTED) this._setDisconnected("Forced call from KNXClient Disconnect() function, because the KNX Interface hasn't sent the DISCONNECT_RESPONSE in time.");
}, 2000);
}
isConnected() {
return this._connectionState === STATE.CONNECTED;
}
_setDisconnected(_sReason = "") {
try {
if (this.sysLogger !== undefined && this.sysLogger !== null) this.sysLogger.debug("KNXClient: called _setDisconnected " + this._options.ipAddr + ":" + this._options.ipPort + " " + _sReason);
} catch (error) {
}
this._connectionState = STATE.DISCONNECTED;
this.stopHeartBeat();
this._timerTimeoutSendDisconnectRequestMessage = null;
if (this._connectionTimeoutTimer !== null) clearTimeout(this._connectionTimeoutTimer);
if (this._timerWaitingForACK !== null) clearTimeout(this._timerWaitingForACK);
this._clientTunnelSeqNumber = -1;
this._channelID = null;
// 08/12/2021
try {
this._clientSocket.close();
} catch (error) { }
try {
this.emit(KNXClientEvents.disconnected, this._options.ipAddr + ":" + this._options.ipPort + " " + _sReason);
} catch (error) {
}
this._clearToSend = true; // 26/12/2021 allow to send
}
_runHeartbeat() {
if (this._heartbeatRunning) {
this.getConnectionStatus();
let t = setTimeout(() => { // 21/03/2022 fixed possible memory leak. Previously was setTimeout without "let t = ".
this._runHeartbeat();
}, 1000 * this._options.connectionKeepAliveTimeout);
}
}
_getSeqNumber() {
return this._clientTunnelSeqNumber;
}
// 26/12/2021 Handle the busy state, for example while waiting for ACK
_getClearToSend() {
return (this._clearToSend !== undefined ? this._clearToSend : true);
}
_incSeqNumber() {
this._clientTunnelSeqNumber++;
if (this._clientTunnelSeqNumber > 255) {
this._clientTunnelSeqNumber = 0;
}
return this._clientTunnelSeqNumber;
}
// _keyFromCEMIMessage(cEMIMessage) {
// return cEMIMessage.dstAddress.toString();
// }
_setTimerWaitingForACK(knxTunnelingRequest) {
this._clearToSend = false; // 26/12/2021 stop sending until ACK received
const timeoutErr = new errors.RequestTimeoutError(`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"}`);
if (this._timerWaitingForACK !== null) clearTimeout(this._timerWaitingForACK);
this._timerWaitingForACK = setTimeout(() => {
try {
this._numFailedTelegramACK += 1;
if (this._numFailedTelegramACK > 2) {
this._numFailedTelegramACK = 0;
// 08/04/2022 Emits the event informing that the last ACK has not been acknowledge.
try {
this.emit(KNXClientEvents.ackReceived, knxTunnelingRequest, false);
} catch (error) {
}
this._clearToSend = true;
this.emit(KNXClientEvents.error, timeoutErr);
if (this.sysLogger !== undefined && this.sysLogger !== null) 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 {
// 26/12/2021 // If no ACK received, resend the datagram once with the same sequence number
this._setTimerWaitingForACK(knxTunnelingRequest);
this.send(knxTunnelingRequest);
if (this.sysLogger !== undefined && this.sysLogger !== null) this.sysLogger.error("KNXClient: _setTimerWaitingForACK: " + (timeoutErr.message || "Undef error") + " no ACK received. Retransmit datagram with seqNumber " + this._getSeqNumber() + " from " + knxTunnelingRequest.cEMIMessage.srcAddress.toString() + " to " + knxTunnelingRequest.cEMIMessage.dstAddress.toString());
}
} catch (error) { }
}, KNXConstants.KNX_CONSTANTS.TUNNELING_REQUEST_TIMEOUT * 1000);
}
_processInboundMessage(msg, rinfo) {
try {
// Composing debug string
try {
if (this.sysLogger !== undefined && this.sysLogger !== null) {
var sProcessInboundLog = "???";
try {
sProcessInboundLog = "Data received: " + msg.toString("hex");
sProcessInboundLog += " srcAddress: " + JSON.stringify(rinfo);
} catch (error) { }
this.sysLogger.trace("Received KNX packet: _processInboundMessage, " + sProcessInboundLog + " ChannelID:" + this._channelID || "??" + " Host:" + this._options.ipAddr + ":" + this._options.ipPort);
}
} catch (error) { }
// BUGFIXING https://github.com/Supergiovane/node-red-contrib-knx-ultimate/issues/162
//msg = Buffer.from("0610053000102900b06011fe11150080","hex");
const { knxHeader, knxMessage } = KNXProtocol.KNXProtocol.parseMessage(msg);
// 26/12/2021 ROUTING LOST MESSAGE OR BUSY
if (knxHeader.service_type === KNXConstants.KNX_CONSTANTS.ROUTING_LOST_MESSAGE) {
try {
this.emit(KNXClientEvents.error, new Error('ROUTING_LOST_MESSAGE'));
//this._setDisconnected("Routing Lost Message"); // 31/03/2022 Commented, because it doesn't matter. Non need to disconnect.
} catch (error) { }
return;
} else if (knxHeader.service_type === KNXConstants.KNX_CONSTANTS.ROUTING_BUSY) {
try {
this.emit(KNXClientEvents.error, new Error('ROUTING_BUSY'));
//this._setDisconnected("Routing Busy"); // 31/03/2022 Commented, because it doesn't matter. Non need to disconnect.
} catch (error) { }
return;
}
if (knxHeader.service_type === KNXConstants.KNX_CONSTANTS.SEARCH_RESPONSE) {
if (this._discovery_timer == null) {
return;
}
try {
this.emit(KNXClientEvents.discover, `${rinfo.address}:${rinfo.port}`, knxHeader, knxMessage);
} catch (error) {
}
}
else if (knxHeader.service_type === KNXConstants.KNX_CONSTANTS.CONNECT_RESPONSE) {
if (this._connectionState === STATE.CONNECTING) {
if (this._connectionTimeoutTimer !== null) clearTimeout(this._connectionTimeoutTimer);
this._connectionTimeoutTimer = null;
const knxConnectResponse = knxMessage;
if (knxConnectResponse.status !== KNXConstants.ConnectionStatus.E_NO_ERROR) {
try {
this.emit(KNXClientEvents.error, KNXConnectResponse.KNXConnectResponse.statusToString(knxConnectResponse.status));
} catch (error) { }
this._setDisconnected("Connect response error " + knxConnectResponse.status);
return;
}
// 16/03/2022
if (this._timerWaitingForACK !== null) clearTimeout(this._timerWaitingForACK);
this._numFailedTelegramACK = 0; // 16/03/2022 Reset the failed ACK counter
this._clearToSend = true; // 16/03/2022 allow to send
this._connectionState = STATE.CONNECTED;
this._channelID = knxConnectResponse.channelID;
try {
if (this.sysLogger !== undefined && this.sysLogger !== null) this.sysLogger.debug("Received KNX packet: CONNECT_RESPONSE, ChannelID:" + this._channelID + " Host:" + this._options.ipAddr + ":" + this._options.ipPort);
} catch (error) { }
try {
this.emit(KNXClientEvents.connected, this._options);
} catch (error) {
}
this.startHeartBeat();
}
}
else if (knxHeader.service_type === KNXConstants.KNX_CONSTANTS.DISCONNECT_RESPONSE) {
try {
if (this.sysLogger !== undefined && this.sysLogger !== null) this.sysLogger.debug("Received KNX packet: DISCONNECT_RESPONSE, ChannelID:" + this._channelID + " Host:" + this._options.ipAddr + ":" + this._options.ipPort);
} catch (error) { }
if (this._connectionState !== STATE.DISCONNECTING) {
try {
this.emit(KNXClientEvents.error, new Error('Unexpected Disconnect Response.'));
} catch (error) {
}
}
this._setDisconnected("Received DISCONNECT_RESPONSE from the KNX interface.");
}
else if (knxHeader.service_type === KNXConstants.KNX_CONSTANTS.DISCONNECT_REQUEST) {
const knxDisconnectRequest = knxMessage;
if (knxDisconnectRequest.channelID !== this._channelID) {
return;
}
try {
if (this.sysLogger !== undefined && this.sysLogger !== null) this.sysLogger.error("Received KNX packet: DISCONNECT_REQUEST, ChannelID:" + this._channelID + " Host:" + this._options.ipAddr + ":" + this._options.ipPort);
} catch (error) { }
this._connectionState = STATE.DISCONNECTING;
try {
this._sendDisconnectResponseMessage(knxDisconnectRequest.channelID);
} catch (error) { }
// 12/03/2021 Added 1 sec delay.
let t = setTimeout(() => { // 21/03/2022 fixed possible memory leak. Previously was setTimeout without "let t = ".
this._setDisconnected("Received KNX packet: DISCONNECT_REQUEST, ChannelID:" + this._channelID + " Host:" + this._options.ipAddr + ":" + this._options.ipPort);
}, 1000);
}
else if (knxHeader.service_type === KNXConstants.KNX_CONSTANTS.TUNNELING_REQUEST) {
const knxTunnelingRequest = knxMessage;
if (knxTunnelingRequest.channelID !== this._channelID) {
try {
this.sysLogger.debug("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);
} catch (error) { }
return;
}
// 26/12/2021 send the ACK if the server requestet that
// Then REMOVED, because some interfaces sets the "ack request" always to 0 even if it needs ack.
//if (knxMessage.cEMIMessage.control.ack){
//setTimeout(() => {
try {
let knxTunnelAck = KNXProtocol.KNXProtocol.newKNXTunnelingACK(knxTunnelingRequest.channelID, knxTunnelingRequest.seqCounter, KNXConstants.KNX_CONSTANTS.E_NO_ERROR);
this.send(knxTunnelAck);
} catch (error) {
this.sysLogger.error("Received KNX packet: TUNNELING: L_DATA_IND, ERROR BUOLDING THE TUNNELINK ACK: " + error.message + " MyChannelID:" + this._channelID + " ReceivedPacketChannelID: " + knxTunnelingRequest.channelID + " ReceivedPacketseqCounter:" + knxTunnelingRequest.seqCounter + " Host:" + this._options.ipAddr + ":" + this._options.ipPort);
}
//}, 20);
//}
if (knxTunnelingRequest.cEMIMessage.msgCode === CEMIConstants.CEMIConstants.L_DATA_IND) {
// Composing debug string
try {
if (this.sysLogger !== undefined && this.sysLogger !== null) {
let sDebugString = "???";
try {
sDebugString = "Data: " + JSON.stringify(knxTunnelingRequest.cEMIMessage.npdu);
sDebugString += " srcAddress: " + knxTunnelingRequest.cEMIMessage.srcAddress.toString();
sDebugString += " dstAddress: " + knxTunnelingRequest.cEMIMessage.dstAddress.toString();
} catch (error) { }
this.sysLogger.debug("Received KNX packet: TUNNELING: L_DATA_IND, " + sDebugString + " ChannelID:" + this._channelID + " seqCounter:" + knxTunnelingRequest.seqCounter + " Host:" + this._options.ipAddr + ":" + this._options.ipPort);
}
} catch (error) { }
try {
this.emit(KNXClientEvents.indication, knxTunnelingRequest, false);
} catch (error) { }
} else if (knxTunnelingRequest.cEMIMessage.msgCode === CEMIConstants.CEMIConstants.L_DATA_CON) {
try {
if (this.sysLogger !== undefined && this.sysLogger !== null) this.sysLogger.debug("Received KNX packet: TUNNELING: L_DATA_CON, ChannelID:" + this._channelID + " seqCounter:" + knxTunnelingRequest.seqCounter + " Host:" + this._options.ipAddr + ":" + this._options.ipPort);
} catch (error) { }
}
} else if (knxHeader.service_type === KNXConstants.KNX_CONSTANTS.TUNNELING_ACK) {
const knxTunnelingAck = knxMessage;
if (knxTunnelingAck.channelID !== this._channelID) {
return;
}
try {
if (this.sysLogger !== undefined && this.sysLogger !== null) this.sysLogger.debug("Received KNX packet: TUNNELING: TUNNELING_ACK, ChannelID:" + this._channelID + " seqCounter:" + knxTunnelingAck.seqCounter + " Host:" + this._options.ipAddr + ":" + this._options.ipPort);
} catch (error) { }
// Check the received ACK sequence number
if (!this._options.suppress_ack_ldatareq) {
if (knxTunnelingAck.seqCounter === this._getSeqNumber()) {
if (this._timerWaitingForACK !== null) clearTimeout(this._timerWaitingForACK);
this._numFailedTelegramACK = 0; // 25/12/2021 clear the current ACK failed telegram number
this._clearToSend = true; // I'm ready to send a new datagram now
// 08/04/2022 Emits the event informing that the last ACK has been acknowledge.
try {
this.emit(KNXClientEvents.ackReceived, knxMessage, true);
} catch (error) {
}
try {
if (this.sysLogger !== undefined && this.sysLogger !== null) this.sysLogger.debug("Received KNX packet: TUNNELING: DELETED_TUNNELING_ACK FROM PENDING ACK's, ChannelID:" + this._channelID + " seqCounter:" + knxTunnelingAck.seqCounter + " Host:" + this._options.ipAddr + ":" + this._options.ipPort);
} catch (error) { }
} else {
// Inform that i received an ACK with an unexpected sequence number
try {
if (this.sysLogger !== undefined && this.sysLogger !== null) this.sysLogger.error("Received KNX packet: TUNNELING: Unexpected Tunnel Ack with seqCounter = " + knxTunnelingAck.seqCounter);
} catch (error) { }
//this.emit(KNXClientEvents.error, `Unexpected Tunnel Ack ${knxTunnelingAck.seqCounter}`);
}
}
} else if (knxHeader.service_type === KNXConstants.KNX_CONSTANTS.ROUTING_INDICATION) {
// 07/12/2021 Multicast routing indication
const knxRoutingInd = knxMessage;
if (knxRoutingInd.cEMIMessage.msgCode === CEMIConstants.CEMIConstants.L_DATA_IND) {
// Composing debug string
try {
if (this.sysLogger !== undefined && this.sysLogger !== null) {
let sDebugString = "???";
try {
sDebugString = "Data: " + JSON.stringify(knxRoutingInd.cEMIMessage.npdu);
sDebugString += " srcAddress: " + knxRoutingInd.cEMIMessage.srcAddress.toString();
sDebugString += " dstAddress: " + knxRoutingInd.cEMIMessage.dstAddress.toString();
} catch (error) { }
this.sysLogger.debug("Received KNX packet: ROUTING: L_DATA_IND, " + sDebugString + " Host:" + this._options.ipAddr + ":" + this._options.ipPort);
}
} catch (error) { }
try {
this.emit(KNXClientEvents.indication, knxRoutingInd, false);
} catch (error) {
}
}
else if (knxRoutingInd.cEMIMessage.msgCode === CEMIConstants.CEMIConstants.L_DATA_CON) {
try {
if (this.sysLogger !== undefined && this.sysLogger !== null) this.sysLogger.debug("Received KNX packet: ROUTING: L_DATA_CON, Host:" + this._options.ipAddr + ":" + this._options.