UNPKG

knxultimate

Version:

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

1,732 lines (1,560 loc) 57.1 kB
import dgram, { RemoteInfo, Socket as UDPSocket } from 'dgram' import net, { Socket as TCPSocket } from 'net' import { ConnectionStatus, KNX_CONSTANTS } from './protocol/KNXConstants' import CEMIConstants from './protocol/cEMI/CEMIConstants' import CEMIFactory from './protocol/cEMI/CEMIFactory' import KNXProtocol, { KnxMessage, KnxResponse } from './protocol/KNXProtocol' import KNXConnectResponse from './protocol/KNXConnectResponse' import HPAI from './protocol/HPAI' import TunnelCRI, { TunnelTypes } from './protocol/TunnelCRI' import KNXConnectionStateResponse from './protocol/KNXConnectionStateResponse' import * as errors from './errors' import * as ipAddressHelper from './util/ipAddressHelper' import KNXAddress from './protocol/KNXAddress' import KNXDataBuffer, { IDataPoint } from './protocol/KNXDataBuffer' import * as DPTLib from './dptlib' import KnxLog, { KNXLogger, LogLevel, module as createLogger, setLogLevel, KNXLoggerOptions, } from './KnxLog' import { KNXDescriptionResponse, KNXPacket } from './protocol' import KNXRoutingIndication from './protocol/KNXRoutingIndication' import KNXConnectRequest from './protocol/KNXConnectRequest' import KNXTunnelingRequest from './protocol/KNXTunnelingRequest' import { TypedEventEmitter } from './TypedEmitter' import KNXHeader from './protocol/KNXHeader' import KNXTunnelingAck from './protocol/KNXTunnelingAck' import KNXSearchResponse from './protocol/KNXSearchResponse' import KNXDisconnectResponse from './protocol/KNXDisconnectResponse' import { wait, getTimestamp } from './utils' export enum ConncetionState { STARTED = 'STARTED', CONNECTING = 'CONNECTING', CONNECTED = 'CONNECTED', DISCONNECTING = 'DISCONNECTING', DISCONNECTED = 'DISCONNECTED', } export enum SocketEvents { error = 'error', message = 'message', listening = 'listening', data = 'data', close = 'close', } export type KNXClientProtocol = 'TunnelUDP' | 'Multicast' | 'TunnelTCP' export enum KNXClientEvents { error = 'error', disconnected = 'disconnected', discover = 'discover', indication = 'indication', connected = 'connected', ready = 'ready', response = 'response', connecting = 'connecting', ackReceived = 'ackReceived', close = 'close', descriptionResponse = 'descriptionResponse', } export interface KNXClientEventCallbacks { error: (error: Error) => void disconnected: (reason: string) => void discover: ( host: string, header: KNXHeader, message: KNXSearchResponse, ) => void getGatewayDescription: (searchResponse: KNXDescriptionResponse) => void indication: (packet: KNXRoutingIndication, echoed: boolean) => void connected: (options: KNXClientOptions) => void ready: () => void response: (host: string, header: KNXHeader, message: KnxResponse) => void connecting: (options: KNXClientOptions) => void ackReceived: ( packet: KNXTunnelingAck | KNXTunnelingRequest, ack: boolean, ) => void close: () => void descriptionResponse: (packet: KNXDescriptionResponse) => void } const jKNXSecureKeyring: string = '' export type KNXClientOptions = { /** The physical address to be identified in the KNX bus */ physAddr?: string /** Connection keep alive timeout. Time after which the connection is closed if no ping received */ connectionKeepAliveTimeout?: number /** The IP of your KNX router/interface (for Routers, use "224.0.23.12") */ ipAddr?: string /** The port, default is "3671" */ ipPort?: number | string /** Default: "TunnelUDP". "Multicast" if you're connecting to a KNX Router. "TunnelUDP" for KNX Interfaces, or "TunnelTCP" for secure KNX Interfaces (not yet implemented) */ hostProtocol?: KNXClientProtocol /** True: Enables the secure connection. Leave false until KNX-Secure has been released. */ isSecureKNXEnabled?: boolean /** Avoid sending/receive the ACK telegram. Leave false. If you encounter issues with old interface, set it to true */ suppress_ack_ldatareq?: boolean /** Leave true forever. This is used only in Node-Red KNX-Ultimate node */ localEchoInTunneling?: boolean /** The local IP address to be used to connect to the KNX/IP Bus. Leave blank, will be automatically filled by KNXUltimate */ localIPAddress?: string /** Specifies the local eth interface to be used to connect to the KNX Bus. */ interface?: string /** ETS Keyring JSON file content (leave blank until KNX-Secure has been released) */ jKNXSecureKeyring?: any /** Local socket address. Automatically filled by KNXClient */ localSocketAddress?: string // ** Local queue interval between each KNX telegram. Default is 1 telegram each 25ms KNXQueueSendIntervalMilliseconds?: number /** Enables sniffing mode to monitor KNX */ sniffingMode?: boolean /** Sets the tunnel_endpoint with the localIPAddress instead of the standard 0.0.0.0 */ theGatewayIsKNXVirtual?: boolean } & KNXLoggerOptions const optionsDefaults: KNXClientOptions = { physAddr: '15.15.200', connectionKeepAliveTimeout: 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, } export function getDecodedKeyring() { return jKNXSecureKeyring } export enum KNXTimer { /** Triggers when an ACK is not received in time */ ACK = 'ack', /** Delay between heartbeats */ HEARTBEAT = 'heartbeat', /** Triggers when no connection state response is received */ CONNECTION_STATE = 'connection_state', /** Waiting for a connect response */ CONNECTION = 'connection', /** Delay before sending the connect request */ CONNECT_REQUEST = 'connect_request', /** Delay after receiving a disconnect request */ DISCONNECT = 'disconnect', /** Waits for discovery responses */ DISCOVERY = 'discovery', /** Waits for the gateway description gather responses */ GATEWAYDESCRIPTION = 'GatewayDescription', } export type SnifferPacket = { reqType?: string request?: string response?: string resType?: string /** Time in ms between this request and the previous */ deltaReq: number /** Time in ms between the request and the response */ deltaRes?: number } interface KNXQueueItem { knxPacket: KNXPacket ACK: KNXTunnelingRequest expectedSeqNumberForACK: number } export default class KNXClient extends TypedEventEmitter<KNXClientEventCallbacks> { private _channelID: number private _connectionState: string private _numFailedTelegramACK: number private _clientTunnelSeqNumber: number private _options: KNXClientOptions private _peerHost: string private _peerPort: number private _heartbeatFailures: number private _heartbeatRunning: boolean private max_HeartbeatFailures: number private _awaitingResponseType: number private _clientSocket: UDPSocket | TCPSocket private sysLogger: KNXLogger private jKNXSecureKeyring: any private _clearToSend = false private socketReady = false private timers: Map<KNXTimer, NodeJS.Timeout> public physAddr: KNXAddress public theGatewayIsKNXVirtual: boolean private commandQueue: Array<KNXQueueItem> = [] private exitProcessingKNXQueueLoop: boolean private currentItemHandledByTheQueue: KNXQueueItem private queueLock = false private sniffingPackets: SnifferPacket[] private lastSnifferRequest: number get udpSocket() { if (this._clientSocket instanceof UDPSocket) { return this._clientSocket } return null } get tcpSocket() { if (this._clientSocket instanceof TCPSocket) { return this._clientSocket } return null } constructor( options: KNXClientOptions, createSocket?: (client: KNXClient) => void, ) { super() this.timers = new Map() // This is the KNX telegram's queue list this.commandQueue = [] this.exitProcessingKNXQueueLoop = false if (options === undefined) { options = optionsDefaults } else { options = { ...optionsDefaults, ...options, } } this._options = options this.sniffingPackets = [] this.sysLogger = createLogger(this._options.setPrefix || 'KNXEngine') if (this._options.loglevel) { setLogLevel(this._options.loglevel) } this._channelID = null this._connectionState = ConncetionState.DISCONNECTED this._numFailedTelegramACK = 0 this._clientTunnelSeqNumber = -1 this._options.connectionKeepAliveTimeout = KNX_CONSTANTS.CONNECTION_ALIVE_TIME this._peerHost = this._options.ipAddr this._peerPort = parseInt(this._options.ipPort as string, 10) this._options.localSocketAddress = options.localSocketAddress this._heartbeatFailures = 0 this.max_HeartbeatFailures = 3 this._awaitingResponseType = null this._clientSocket = null this.jKNXSecureKeyring = this._options.jKNXSecureKeyring // Configure the limiter try { if (Number(this._options.KNXQueueSendIntervalMilliseconds) < 20) { this._options.KNXQueueSendIntervalMilliseconds = 20 // Protection avoiding handleKNXQueue hangs } } catch (error) { this._options.KNXQueueSendIntervalMilliseconds = 25 this.sysLogger.error( `KNXQueueSendIntervalMilliseconds:${error.message}. Defaulting to 25`, ) } // add an empty error listener, without this // every "error" emitted throws an unhandled exception this.on('error', (error) => { this.sysLogger.error(error.stack) }) if (typeof this._options.physAddr === 'string') { this.physAddr = KNXAddress.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() } } private createSocket() { if (this._options.hostProtocol === 'TunnelUDP') { this._clientSocket = dgram.createSocket({ type: 'udp4', reuseAddr: true, }) as UDPSocket 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( { // port: this._peerPort, // Local port shall be assigned by the socket. address: this._options.localIPAddress, // Force UDP to be heard trough this interface }, () => { 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') { // November 2024 - DIRTY function, to be checked this._clientSocket = new net.Socket() // this._clientSocket.removeAllListeners() this.tcpSocket.on(SocketEvents.data, (data) => { this.sysLogger.debug( `[${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.createSocket({ type: 'udp4', reuseAddr: true, }) as UDPSocket // this._clientSocket.removeAllListeners() 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) }) // The multicast traffic is not sent to a specific local IP, so we cannot set the this._options.localIPAddress in the bind // otherwise the socket will never ever receive a packet. 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) } }, ) } } /** * The channel ID of the connection. Only defined after a successful connection */ get channelID() { return this._channelID } /** * Handle the busy state, for example while waiting for ACK. When true means we can send new telegrams to bus */ get clearToSend(): boolean { return this._clearToSend } set clearToSend(val: boolean) { this._clearToSend = val if (val) { this.handleKNXQueue() } } private getKNXDataBuffer(data: Buffer, dptid: string | number) { if (typeof dptid === 'number') { dptid = dptid.toString() } const adpu = {} as DPTLib.APDU DPTLib.populateAPDU(data, adpu, dptid) const iDatapointType: number = parseInt( dptid.substring(0, dptid.indexOf('.')), ) const isSixBits: boolean = adpu.bitlength <= 6 this.sysLogger.debug( `[${getTimestamp()}] ` + `isSixBits:${isSixBits} Includes (should be = isSixBits):${[ 1, 2, 3, 5, 9, 10, 11, 14, 18, ].includes(iDatapointType)} ADPU BitLength:${adpu.bitlength}`, ) const datapoint: IDataPoint = { id: '', value: 'any', type: { type: isSixBits }, bind: null, read: () => null, write: null, } return new KNXDataBuffer(adpu.data, datapoint) } /** Waits till providden event occurs for at most the providden timeout */ private async waitForEvent(event: KNXClientEvents, timeout: number) { let resolveRef: () => void return Promise.race<void>([ new Promise((resolve) => { resolveRef = resolve this.once(event, resolve) }), wait(timeout), ]).then(() => { this.off(event, resolveRef) }) } private setTimer(type: KNXTimer, cb: () => void, delay: number) { if (this.timers.has(type)) { clearTimeout(this.timers.get(type)) this.timers.delete(type) // TODO: should we throw error? this.sysLogger.warn(`Timer "${type}" was already running`) } this.timers.set( type, setTimeout(() => { this.timers.delete(type) cb() }, delay), ) } private clearTimer(type: KNXTimer) { if (this.timers.has(type)) { clearTimeout(this.timers.get(type)) this.timers.delete(type) } } private clearAllTimers() { // use dedicated methods where possible this.stopDiscovery() this.stopHeartBeat() this.stopGatewayDescription() // clear all other timers for (const timer of this.timers.keys()) { this.clearTimer(timer) } } private processKnxPacketQueueItem(_knxPacket: KNXPacket): Promise<boolean> { return new Promise((resolve) => { // Prepare the debug log ************************ if (this.sysLogger.level === 'debug') { if ( _knxPacket instanceof KNXTunnelingRequest || _knxPacket instanceof KNXRoutingIndication ) { // Composing debug string 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( `[${getTimestamp()}] ` + `KNXEngine: <outgoing telegram>: ${sDebugString} `, ) } else if (_knxPacket instanceof KNXTunnelingAck) { this.sysLogger.debug( `[${getTimestamp()}] ` + `KNXEngine: <outgoing telegram ACK>:${this.getKNXConstantName(_knxPacket.status)} channelID:${_knxPacket.channelID} seqCounter:${_knxPacket.seqCounter}`, ) } } // End Prepare the debug log ************************ 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 as any)?.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) } } }) } private async handleKNXQueue() { if (this.queueLock) { this.sysLogger.debug( `[${getTimestamp()}] ` + `KNXClient: handleKNXQueue: HandleQueue has called, but the queue loop is already running. Exit.`, ) return } this.sysLogger.debug( `[${getTimestamp()}] ` + `KNXClient: handleKNXQueue: Start Processing queued KNX. Found ${this.commandQueue.length} telegrams in queue.`, ) // lock the queue this.queueLock = true // Limiter: limits max telegrams per second while (this.commandQueue.length > 0) { if (!this.clearToSend) { this.sysLogger.debug( `[${getTimestamp()}] ` + `KNXClient: handleKNXQueue: Clear to send is false. Pause processing queue.`, ) break } if (this.exitProcessingKNXQueueLoop) { this.sysLogger.debug( `[${getTimestamp()}] ` + `KNXClient: handleKNXQueue: exitProcessingKNXQueueLoop is true. Exit processing queue loop`, ) break } if (this.socketReady === false) { this.sysLogger.debug( `[${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)}`, ) // Clear the queue this.commandQueue = [] break } await wait(this._options.KNXQueueSendIntervalMilliseconds) } this.queueLock = false this.sysLogger.debug( `[${getTimestamp()}] ` + `KNXClient: handleKNXQueue: End Processing queued KNX.`, ) } /** * Write knxPacket to socket */ send( _knxPacket: KNXPacket, _ACK: KNXTunnelingRequest, _priority: boolean, _expectedSeqNumberForACK: number, ): void { const toBeAdded: KNXQueueItem = { 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) // Put the item as first to be sent. this.clearToSend = true } else { this.commandQueue.unshift(toBeAdded) // Put the item as last to be sent. } this.handleKNXQueue() this.sysLogger.debug( `[${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'}`, ) } /** Sends a WRITE telegram to the BUS. * `dstAddress` is the group address (for example "0/0/1"), * `data` is the value you want to send (for example true), * `dptid` is a string/number representing the datapoint (for example "5.001") */ write( dstAddress: KNXAddress | string, data: any, dptid: string | number, ): void { if (this._connectionState !== ConncetionState.CONNECTED) throw new Error( 'The socket is not connected. Unable to access the KNX BUS', ) // Get the Data Buffer from the plain value const knxBuffer = this.getKNXDataBuffer(data, dptid) if (typeof dstAddress === 'string') dstAddress = KNXAddress.createFromString( dstAddress, KNXAddress.TYPE_GROUP, ) const srcAddress = this.physAddr if (this._options.hostProtocol === 'Multicast') { // Multicast. const cEMIMessage = CEMIFactory.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.newKNXRoutingIndication(cEMIMessage) this.send(knxPacketRequest, undefined, false, this.getSeqNumber()) // 06/12/2021 Multivast automaticalli echoes telegrams } else { // Tunneling const cEMIMessage = CEMIFactory.newLDataRequestMessage( 'write', srcAddress, dstAddress, knxBuffer, ) // 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: number = this.incSeqNumber() // 26/12/2021 const knxPacketRequest = KNXProtocol.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(), ) } // 06/12/2021 Echo the sent telegram. Last parameter is the echo true/false if (this._options.localEchoInTunneling) this.emit(KNXClientEvents.indication, knxPacketRequest, true) } } /** * Sends a RESPONSE telegram to the BUS. * `dstAddress` is the group address (for example "0/0/1"), * `data` is the value you want to send (for example true), * `dptid` is a string/number representing the datapoint (for example "5.001") */ respond( dstAddress: KNXAddress | string, data: Buffer, dptid: string | number, ): void { if (this._connectionState !== ConncetionState.CONNECTED) throw new Error( 'The socket is not connected. Unable to access the KNX BUS', ) // Get the Data Buffer from the plain value const knxBuffer = this.getKNXDataBuffer(data, dptid) if (typeof dstAddress === 'string') dstAddress = KNXAddress.createFromString( dstAddress, KNXAddress.TYPE_GROUP, ) const srcAddress = this.physAddr if (this._options.hostProtocol === 'Multicast') { // Multicast const cEMIMessage = CEMIFactory.newLDataIndicationMessage( 'response', srcAddress, dstAddress, knxBuffer, ) 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.newKNXRoutingIndication(cEMIMessage) this.send(knxPacketRequest, undefined, false, this.getSeqNumber()) // 06/12/2021 Multivast automatically echoes telegrams } else { // Tunneling const cEMIMessage = CEMIFactory.newLDataRequestMessage( 'response', srcAddress, dstAddress, knxBuffer, ) // 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: number = this.incSeqNumber() // 26/12/2021 const knxPacketRequest = KNXProtocol.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(), ) } // 06/12/2021 Echo the sent telegram. Last parameter is the echo true/false if (this._options.localEchoInTunneling) this.emit(KNXClientEvents.indication, knxPacketRequest, true) } } /** * Sends a READ telegram to the BUS. GA is the group address (for example "0/0/1"). */ read(dstAddress: KNXAddress | string): void { 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.createFromString( dstAddress, KNXAddress.TYPE_GROUP, ) const srcAddress = this.physAddr if (this._options.hostProtocol === 'Multicast') { // Multicast const cEMIMessage = 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.newKNXRoutingIndication(cEMIMessage) this.send(knxPacketRequest, undefined, false, this.getSeqNumber()) // 06/12/2021 Multivast automaticalli echoes telegrams } else { // Tunneling const cEMIMessage = 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: number = this.incSeqNumber() // 26/12/2021 const knxPacketRequest = KNXProtocol.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(), ) } // 06/12/2021 Echo the sent telegram. Last parameter is the echo true/false if (this._options.localEchoInTunneling) { this.emit(KNXClientEvents.indication, knxPacketRequest, true) } } } /** * Sends a WRITE telegram to the BUS. * `dstAddress` is the group address (for example "0/0/1"), * `rawDataBuffer` is the buffer you want to send, * `dptid` is a string/number representing the datapoint (for example "5.001") */ writeRaw( dstAddress: KNXAddress | string, rawDataBuffer: Buffer, bitlength: number, ): void { // bitlength is unused and only for backward compatibility 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: boolean = bitlength <= 6 const datapoint: IDataPoint = { id: '', value: 'any', type: { type: isSixBits }, bind: null, read: () => null, write: null, } // Get the KNDDataBuffer const baseBufferFromBitLength: Buffer = Buffer.alloc( Math.ceil(bitlength / 8), ) // The buffer length must be like specified by bitlength rawDataBuffer.copy(baseBufferFromBitLength, 0) const data: KNXDataBuffer = new KNXDataBuffer( baseBufferFromBitLength, datapoint, ) if (typeof dstAddress === 'string') dstAddress = KNXAddress.createFromString( dstAddress, KNXAddress.TYPE_GROUP, ) const srcAddress = this.physAddr if (this._options.hostProtocol === 'Multicast') { // Multicast const cEMIMessage = 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.newKNXRoutingIndication(cEMIMessage) this.send(knxPacketRequest, undefined, false, this.getSeqNumber()) // 06/12/2021 Multivast automaticalli echoes telegrams } else { // Tunneling const cEMIMessage = 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: number = this.incSeqNumber() // 26/12/2021 const knxPacketRequest = KNXProtocol.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(), ) } // 06/12/2021 Echo the sent telegram. Last parameter is the echo true/false if (this._options.localEchoInTunneling) this.emit(KNXClientEvents.indication, knxPacketRequest, true) } } private startHeartBeat(): void { this.stopHeartBeat() this._heartbeatFailures = 0 this._heartbeatRunning = true this.runHeartbeat() } private stopHeartBeat(): void { this._heartbeatRunning = false this.clearTimer(KNXTimer.HEARTBEAT) this.clearTimer(KNXTimer.CONNECTION_STATE) } /** * Returns true if discovery is running */ isDiscoveryRunning() { return this.timers.has(KNXTimer.DISCOVERY) } /** * Send a search request message to the KNX bus and wait for responses * Set _clearToSend to true to allow the discovery packet to process. Initially set to false to prevent premature sends! */ startDiscovery() { this._clearToSend = true if (this.isDiscoveryRunning()) { throw new Error('Discovery already running') } this.setTimer( KNXTimer.DISCOVERY, () => {}, 1000 * KNX_CONSTANTS.SEARCH_TIMEOUT, ) this.sendSearchRequestMessage() } /** * Stop the discovery process */ stopDiscovery() { this.clearTimer(KNXTimer.DISCOVERY) } /** * Returns an array of discovered KNX interfaces in the format "<ip>:<port>" */ public static async discover(eth?: string | number, timeout = 5000) { if (typeof eth === 'number') { timeout = eth eth = undefined } const client = new KNXClient({ interface: eth as string, hostProtocol: 'Multicast', }) const discovered: string[] = [] client.on(KNXClientEvents.discover, (host, header, searchResponse) => { discovered.push( `${host}:${searchResponse.deviceInfo?.name.replace(/:/g, ' ') ?? ''}:${searchResponse.deviceInfo?.formattedAddress ?? ''}`, ) }) client.startDiscovery() await wait(timeout) await client.Disconnect() return discovered } /** * Returns true if the gw description's gatherer is running */ isGatewayDescriptionRunning() { return this.timers.has(KNXTimer.GATEWAYDESCRIPTION) } /** * Send a get description message to the KNX bus and wait for responses * Set _clearToSend to true to allow the gatherer packet to process. Initially set to false to prevent premature sends. */ startGatewayDescription() { this._clearToSend = true if (this.isGatewayDescriptionRunning()) { throw new Error('GatewayDescription gather is already running') } this.setTimer( KNXTimer.GATEWAYDESCRIPTION, () => {}, 1000 * KNX_CONSTANTS.DEVICE_CONFIGURATION_REQUEST_TIMEOUT, ) this.sendDescriptionRequestMessage() } /** * Stop the process */ stopGatewayDescription() { this.clearTimer(KNXTimer.GATEWAYDESCRIPTION) } /** * Returns an array of "search_responses" from the KNX interfaces in the format of a KNX descriptionResponse */ public static async getGatewayDescription( ipAddr: string, ipPort: string, eth?: string | number, timeout = 5000, ) { if (typeof eth === 'number') { timeout = eth eth = undefined } const client = new KNXClient({ ipAddr, ipPort, interface: eth as string, hostProtocol: 'TunnelUDP', }) const descriptions = [] client.on(KNXClientEvents.descriptionResponse, (searchResponse) => { descriptions.push(searchResponse) }) client.startGatewayDescription() await wait(timeout) await client.Disconnect() return descriptions } /** * Connect to the KNX bus */ Connect(knxLayer = 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 // 25/12/2021 Reset the failed ACK counter this.clearToSend = true // 26/12/2021 allow to send this.clearTimer(KNXTimer.CONNECTION) // 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._awaitingResponseType = KNX_CONSTANTS.CONNECT_RESPONSE this._clientTunnelSeqNumber = -1 this.setTimer( KNXTimer.CONNECTION, () => { this.emit(KNXClientEvents.error, timeoutError) }, 1000 * KNX_CONSTANTS.CONNECT_REQUEST_TIMEOUT, ) // 27/06/2023, leave some time to the dgram, to do the bind and read local ip and local port this.setTimer( KNXTimer.CONNECT_REQUEST, () => { this.sendConnectRequestMessage(new TunnelCRI(knxLayer)) }, 2000, ) } else if (this._options.hostProtocol === 'TunnelTCP') { this.tcpSocket.connect(this._peerPort, this._peerHost, () => { this._awaitingResponseType = KNX_CONSTANTS.CONNECT_RESPONSE this._clientTunnelSeqNumber = 0 if (this._options.isSecureKNXEnabled) this.sendSecureSessionRequestMessage( new TunnelCRI(knxLayer), ) }) } else { // Multicast this._connectionState = ConncetionState.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 this.emit(KNXClientEvents.connected, this._options) } } /** * Close the socket connection */ private async closeSocket() { this.exitProcessingKNXQueueLoop = true // Exits KNX processing queue loop return new Promise<void>((resolve) => { // already closed if (!this._clientSocket) return this.socketReady = false const client = this._clientSocket this._clientSocket = null const cb = () => { resolve() } try { if (client instanceof TCPSocket) { // use destroy instead of end here to ensure socket is closed // we could try to see if `end()` works well too client.destroy() } else { client.close(cb) } } catch (error) { this.sysLogger.error( `KNXClient: into async closeSocket(): ${error.stack}`, ) resolve() } }) } /** * Sends a DISCONNECT_REQUEST telegram to the BUS and closes the socket */ async Disconnect() { if (this._clientSocket === null) { throw new Error('No client socket defined') } if (this._connectionState === ConncetionState.DISCONNECTING) { throw new Error('Already disconnecting') } // clear active timers this.clearAllTimers() this._connectionState = ConncetionState.DISCONNECTING // 20/04/2022 this._channelID === null can happen when the KNX Gateway is already disconnected if (this._channelID === null) { // 11/10/2022 Close the socket this.sysLogger.debug( `[${getTimestamp()}] ` + `KNXClient: into Disconnect(), channel id is not defined so skip disconnect packet and close socket`, ) await this.closeSocket() return } this._awaitingResponseType = KNX_CONSTANTS.DISCONNECT_RESPONSE this.sendDisconnectRequestMessage(this._channelID) // wait for disconnect event or at most 2 seconds await this.waitForEvent(KNXClientEvents.disconnected, 2000) // fix #54 : disconnect request is not sent properly this.exitProcessingKNXQueueLoop = true // Exits KNX processing queue loop // 12/03/2021 Set disconnected if not already set by DISCONNECT_RESPONSE sent from the IP Interface 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) } } /** * Returns true if the socket is connected */ isConnected() { return this._connectionState === ConncetionState.CONNECTED } /** * Close the socket connection without sending a disconnect request */ private async setDisconnected(_sReason = '') { this.sysLogger.debug( `[${getTimestamp()}] ` + `KNXClient: called _setDisconnected ${this._options.ipAddr}:${this._options.ipPort} ${_sReason}`, ) this._connectionState = ConncetionState.DISCONNECTED // clear active timers 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 // 26/12/2021 allow to send } /** * Send a connection state request message to the KNX bus and schedule the next heartbeat */ private 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}`, ) // timeout triggered if no connection state response received this.setTimer( KNXTimer.CONNECTION_STATE, () => { this.sysLogger.error( `KNXClient: getConnectionStatus Timeout ${this._heartbeatFailures} out of ${this.max_HeartbeatFailures}`, ) // this.emit(KNXClientEvents.error, timeoutError) this._heartbeatFailures++ if (this._heartbeatFailures >= this.max_HeartbeatFailures) { this._heartbeatFailures = 0 this.emit(KNXClientEvents.error, deadError) this.setDisconnected(deadError.message) } }, 1000 * KNX_CONSTANTS.CONNECTIONSTATE_REQUEST_TIMEOUT, ) this._awaitingResponseType = KNX_CONSTANTS.CONNECTIONSTATE_RESPONSE this.sendConnectionStateRequestMessage(this._channelID) // schedule next heartbeat this.setTimer( KNXTimer.HEARTBEAT, () => { this.runHeartbeat() }, 1000 * this._options.connectionKeepAliveTimeout, ) } /** * Get actual tunneling sequence number */ private getSeqNumber() { return this._clientTunnelSeqNumber } private getCurrentItemHandledByTheQueue() { return this.currentItemHandledByTheQueue.expectedSeqNumberForACK } /** * Increment the tunneling sequence number */ private incSeqNumber() { this._clientTunnelSeqNumber++ if (this._clientTunnelSeqNumber > 255) { this._clientTunnelSeqNumber = 0 } return this._clientTunnelSeqNumber } /** * Setup a timer while waiting for an ACK of `knxTunnelingRequest` */ private setTimerWaitingForACK(knxTunnelingRequest: KNXTunnelingRequest) { this.clearToSend = false // 26/12/2021 stop sending until ACK received 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 // 08/04/2022 Emits the event informing that the last ACK has not been acknowledge. 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 { // 26/12/2021 // If no ACK received, resend the datagram once with the same sequence number 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, ) } }, KNX_CONSTANTS.TUNNELING_REQUEST_TIMEOUT * 1000, ) } /** * Utility function */ private getKNXConstantName(serviceType: number): string | undefined { const entry = Object.entries(KNX_CONSTANTS).find( ([, value]) => value === serviceType, ) return entry ? entry[0] : undefined } /** * Process a raw message coming from the socket */ private processInboundMessage(msg: Buffer, rinfo: RemoteInfo) { let sProcessInboundLog = '' try { const { knxHeader, knxMessage } = KNXProtocol.parseMessage(msg) // Composing debug string sProcessInboundLog = `peerHost:${this._peerHost}:${this._peerPort}` sProcessInboundLog += ` srcAddress:${rinfo?.address}:${rinfo?.port}` sProcessInboundLog += ` channelID:${this._channelID === null || this._channelID === undefined ? 'None' : this._channelID}` sProcessInboundLog += ` service_type:${this.getKNXConstantName(knxHeader?.service_type)}` sProcessInboundLog += ` knxHeader:${JSON.stringify(knxHeader)} knxMessage:${JSON.stringify(knxMessage)}` sProcessInboundLog += ` raw: ${msg.toString('hex')}` this.sysLogger.debug( `[${getTimestamp()}] ` + `KNXEngine: <incoming telegram>: ${sProcessInboundLog} `, ) if (this._options.sniffingMode) { const lastEntry = this.sniffingPackets[this.sniffingPackets.length - 1] if (lastEntry) { // last entry already has a response, so create a new entry 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 } } } // 26/12/2021 ROUTING LOST MESSAGE OR BUSY if (knxHeader.service_type === KNX_CONSTANTS.ROUTING_LOST_MESSAGE) { 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. return } if (knxHeader.service_type === KNX_CONSTANTS.ROUTING_BUSY) { 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. return } if (knxHeader.service_type === KNX_CONSTANTS.SEARCH_RESPONSE) { if (!this.isDiscoveryRunning()) return this.emit( KNXClientEvents.discover, `${rinfo.address}:${rinfo.port}`, knxHeader, knxMessage as KNXSearchResponse, ) } else if ( knxHeader.service_type === KNX_CONSTANTS.DESCRIPTION_RESPONSE ) { const knxDescriptionResponse = knxMessage as KNXDescriptionResponse this.sysLogger.debug( `[${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 === KNX_CONSTANTS.CONNECT_RESPONSE ) { if (this._connectionState === ConncetionState.CONNECTING) { this.clearTimer(KNXTimer.CONNECTION) const knxConnectResponse = knxMessage as KNXConnectResponse if ( knxConnectResponse.status !== ConnectionStatus.E_NO_ERROR ) { this.emit( KNXClientEvents.error, Error( KNXConnectResponse.statusToString( knxConnectResponse.status, ), ), ) this.setDisconnected( `Connect response error ${knxConnectResponse.status}`, ) return } // 16/03/2022 this.clearTimer(KNXTimer.ACK) this._channelID = knxConnectResponse.channelID this._connectionState = ConncetionState.CONNECTED this._numFailedTelegramACK = 0 // 16/03/2022 Reset the failed ACK counter this.clearToSend = true // 16/03/2022 allow to send this.emit(KNXClientEvents.connected, this._options) this.startHeartBeat() } } else if ( knxHeader.service_type === 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 === KNX_CONSTANTS.DISCONNECT_REQUEST ) { const knxDisconnectRequest = knxMessage as KNXDisconnectResponse if (knxDisconnectRequest.channelID !== this._channelID) { return } this._connectionState = ConncetionState.DISCONNECTING this.sendDisconnectResponseMessage( knxDisconnectRequest.channelID, ) // 12/03/2021 Added 1 sec delay. 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 === KNX_CONSTANTS.TUNNELING_REQUEST ) { const knxTunnelingRequest = knxMessage as KNXTunnelingRequest if (knxTunnelingRequest.channelID !== this._channelID) { this.sysLogger.debu