knx.ts 1.0.2 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/LICENSE +51 -21
  2. package/README.md +274 -61
  3. package/dist/@types/interfaces/connection.d.ts +80 -13
  4. package/dist/@types/interfaces/servers.d.ts +18 -0
  5. package/dist/@types/interfaces/servers.js +2 -0
  6. package/dist/connection/KNXService.d.ts +13 -30
  7. package/dist/connection/KNXService.js +4 -164
  8. package/dist/connection/KNXTunneling.d.ts +4 -4
  9. package/dist/connection/KNXTunneling.js +35 -62
  10. package/dist/connection/KNXUSBConnection.d.ts +20 -0
  11. package/dist/connection/KNXUSBConnection.js +358 -0
  12. package/dist/connection/KNXnetIPServer.d.ts +29 -12
  13. package/dist/connection/KNXnetIPServer.js +261 -83
  14. package/dist/connection/Router.d.ts +52 -32
  15. package/dist/connection/Router.js +225 -153
  16. package/dist/connection/TPUART.d.ts +8 -3
  17. package/dist/connection/TPUART.js +41 -37
  18. package/dist/connection/TunnelConnection.d.ts +3 -1
  19. package/dist/connection/TunnelConnection.js +6 -4
  20. package/dist/core/CEMI.d.ts +7 -2
  21. package/dist/core/CEMI.js +5 -8
  22. package/dist/core/EMI.d.ts +312 -200
  23. package/dist/core/EMI.js +511 -1007
  24. package/dist/core/KNXnetIPStructures.d.ts +10 -1
  25. package/dist/core/KNXnetIPStructures.js +15 -10
  26. package/dist/core/MessageCodeField.d.ts +1 -1
  27. package/dist/core/cache/GroupAddressCache.d.ts +57 -0
  28. package/dist/core/cache/GroupAddressCache.js +227 -0
  29. package/dist/core/data/KNXDataDecode.d.ts +2 -2
  30. package/dist/core/data/KNXDataDecode.js +198 -183
  31. package/dist/core/enum/EnumControlField.d.ts +0 -5
  32. package/dist/core/enum/EnumControlField.js +1 -7
  33. package/dist/core/enum/EnumControlFieldExtended.d.ts +1 -1
  34. package/dist/core/enum/EnumShortACKFrame.d.ts +1 -1
  35. package/dist/core/enum/ErrorCodeSet.js +59 -0
  36. package/dist/core/enum/KNXnetIPEnum.d.ts +2 -2
  37. package/dist/core/enum/KNXnetIPEnum.js +19 -1
  38. package/dist/core/layers/data/NPDU.d.ts +2 -1
  39. package/dist/core/layers/data/NPDU.js +6 -3
  40. package/dist/index.d.ts +19 -2
  41. package/dist/index.js +36 -1
  42. package/dist/server/KNXMQTTGateway.d.ts +13 -0
  43. package/dist/server/KNXMQTTGateway.js +164 -0
  44. package/dist/server/KNXWebSocketServer.d.ts +12 -0
  45. package/dist/server/KNXWebSocketServer.js +118 -0
  46. package/dist/utils/CEMIAdapter.d.ts +4 -3
  47. package/dist/utils/CEMIAdapter.js +26 -30
  48. package/dist/utils/Logger.d.ts +4 -4
  49. package/dist/utils/Logger.js +3 -7
  50. package/package.json +27 -7
@@ -12,18 +12,18 @@ const CEMI_1 = require("../core/CEMI");
12
12
  const KNXnetIPStructures_1 = require("../core/KNXnetIPStructures");
13
13
  const KNXHelper_1 = require("../utils/KNXHelper");
14
14
  const localIp_1 = require("../utils/localIp");
15
- const Router_1 = require("./Router");
16
15
  const node_os_1 = __importDefault(require("node:os"));
17
16
  const DeviceDescriptorType_1 = require("../core/resources/DeviceDescriptorType");
18
17
  const TunnelConnection_1 = require("./TunnelConnection");
19
18
  const InvalidKnxAddresExeption_1 = require("../errors/InvalidKnxAddresExeption");
19
+ const GroupAddressCache_1 = require("../core/cache/GroupAddressCache");
20
20
  /**
21
- * Implements a KNXnet/IP Server (Gateway) that supports Routing and Tunneling protocols.
22
- * This class handles device discovery (Search/Description), manages multiple concurrent
23
- * tunneling connections, and bridges communication between IP multicast (Routing) and
24
- * point-to-point (Tunneling) clients. It includes implementation for flow control
25
- * (RoutingBusy), rate limiting, and echo cancellation.
26
- */
21
+ * Implements a KNXnet/IP Server (Gateway) that supports Routing and Tunneling protocols.
22
+ * This class handles device discovery (Search/Description), manages multiple concurrent
23
+ * tunneling connections, and bridges communication between IP multicast (Routing) and
24
+ * point-to-point (Tunneling) clients. It includes implementation for flow control
25
+ * (RoutingBusy), rate limiting, and echo cancellation.
26
+ */
27
27
  class KNXnetIPServer extends KNXService_1.KNXService {
28
28
  isRoutingBusy = false;
29
29
  routingBusyTimer = null;
@@ -37,6 +37,8 @@ class KNXnetIPServer extends KNXService_1.KNXService {
37
37
  // [MEJORA] Almacenamos la IA en formato entero para el filtro anti-eco rápido
38
38
  serverIAInt;
39
39
  _tunnelConnections = new Map();
40
+ isCacheDelegated = false;
41
+ isEventsDelegated = false;
40
42
  MAX_QUEUE_SIZE = 100;
41
43
  BUSY_THRESHOLD = 15;
42
44
  HEARTBEAT_TIMEOUT = KNXnetIPEnum_1.KNXTimeoutConstants.CONNECTION_ALIVE_TIME * 1000;
@@ -44,7 +46,6 @@ class KNXnetIPServer extends KNXService_1.KNXService {
44
46
  MAX_PENDING_REQUESTS_PER_CLIENT = 100; // [MEJORA] Límite de ráfagas
45
47
  maxTunnelConnections;
46
48
  clientAddrsStartInt;
47
- externalManager = null;
48
49
  constructor(options) {
49
50
  super(options);
50
51
  this._transport = "UDP";
@@ -53,6 +54,7 @@ class KNXnetIPServer extends KNXService_1.KNXService {
53
54
  const netInfo = (0, localIp_1.getNetworkInfo)();
54
55
  this.options.localIp = options.localIp || netInfo.address;
55
56
  routingOptions.individualAddress = options.individualAddress || "15.15.0";
57
+ this.individualAddress = routingOptions.individualAddress;
56
58
  if (!KNXHelper_1.KNXHelper.isValidIndividualAddress(routingOptions.individualAddress)) {
57
59
  throw new InvalidKnxAddresExeption_1.InvalidKnxAddressException(`This ${routingOptions.individualAddress} is not individual address`);
58
60
  }
@@ -60,6 +62,7 @@ class KNXnetIPServer extends KNXService_1.KNXService {
60
62
  this.logger = this.logger.child({ module: this.constructor.name });
61
63
  // Serial must be deterministic and unique per instance (MAC + Port), similar to knxd
62
64
  if (!options.serialNumber) {
65
+ // eslint-disable-next-line no-useless-escape
63
66
  const macBuf = Buffer.from(netInfo.mac.replace(/[:\-]/g, ""), "hex");
64
67
  const port = options.port || 3671;
65
68
  const serial = Buffer.from(macBuf);
@@ -70,7 +73,7 @@ class KNXnetIPServer extends KNXService_1.KNXService {
70
73
  else {
71
74
  routingOptions.serialNumber = options.serialNumber;
72
75
  }
73
- routingOptions.friendlyName = options.friendlyName || "KNX.ts Routing Node";
76
+ routingOptions.friendlyName = options.friendlyName || "KNX.ts";
74
77
  routingOptions.macAddress = options.macAddress || netInfo.mac;
75
78
  routingOptions.routingDelay = options.routingDelay ?? 20;
76
79
  if (routingOptions.MAX_PENDING_REQUESTS_PER_CLIENT)
@@ -94,14 +97,11 @@ class KNXnetIPServer extends KNXService_1.KNXService {
94
97
  this.maxTunnelConnections = 15;
95
98
  this.clientAddrsStartInt = serverIA + 1;
96
99
  }
97
- if (options.externals) {
98
- this.externalManager = new Router_1.Router(options.externals);
99
- }
100
- }
101
- get individualAddress() {
102
- return KNXHelper_1.KNXHelper.GetAddress(this.serverIAInt, ".", true);
103
100
  }
104
101
  async connect() {
102
+ if (this.socket) {
103
+ return;
104
+ }
105
105
  this.socket = dgram_1.default.createSocket({ type: "udp4", reuseAddr: true });
106
106
  this.socket.on("message", (msg, rinfo) => {
107
107
  this.handleMessage(msg, rinfo);
@@ -121,11 +121,12 @@ class KNXnetIPServer extends KNXService_1.KNXService {
121
121
  const joinedInterfaces = new Set();
122
122
  const useAllInterfaces = this.options.useAllInterfaces ?? true;
123
123
  // Siempre intenta unirse primero a la localIp especificada
124
- if (this.options.localIp && this.options.localIp !== "0.0.0.0") {
124
+ if (this.options.localIp && this.options.localIp !== "0.0.0.0" && this.options.ip) {
125
125
  try {
126
126
  socket.addMembership(this.options.ip, this.options.localIp);
127
127
  joinedInterfaces.add(this.options.localIp);
128
128
  this.logger.info(`Joined multicast on primary interface (${this.options.localIp})`);
129
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
129
130
  }
130
131
  catch (e) {
131
132
  this.logger.debug(`Failed to join multicast on primary interface ${this.options.localIp}`);
@@ -141,6 +142,7 @@ class KNXnetIPServer extends KNXService_1.KNXService {
141
142
  socket.addMembership(this.options.ip, net.address);
142
143
  joinedInterfaces.add(net.address);
143
144
  this.logger.info(`Joined multicast on interface ${name} (${net.address})`);
145
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
144
146
  }
145
147
  catch (err) {
146
148
  // Ignora interfaces virtuales que no soportan IGMP
@@ -153,31 +155,6 @@ class KNXnetIPServer extends KNXService_1.KNXService {
153
155
  else {
154
156
  this.logger.info("Multi-homing disabled. Only primary interface used for multicast.");
155
157
  }
156
- // Central listener for all KNX indications (from IP Multicast, TP, or Tunnels)
157
- this.on("indication", (cemi) => {
158
- const body = cemi.toBuffer();
159
- const srcIAStr = cemi.sourceAddress;
160
- let busmonBody = null;
161
- // Optional: Re-emit by Group Address for specific listening (e.g., server.on("1/1/1", (cemi) => ...))
162
- if (cemi.controlField2 && cemi.controlField2.addressType === 1) {
163
- this.emit(cemi.destinationAddress, cemi);
164
- }
165
- this._tunnelConnections.forEach((conn) => {
166
- // Echo cancellation: Don't forward back to the client that originated this message
167
- if (srcIAStr === conn.knxAddressStr) {
168
- return;
169
- }
170
- if (conn.knxLayer === KNXnetIPEnum_1.KNXLayer.BUSMONITOR_LAYER) {
171
- if (!busmonBody)
172
- busmonBody = this.convertDataIndToBusmonInd(body);
173
- conn.enqueue(busmonBody, KNXnetIPEnum_1.KNXnetIPServiceType.TUNNELLING_REQUEST);
174
- }
175
- else {
176
- // Link Layer or Raw Layer
177
- conn.enqueue(body, KNXnetIPEnum_1.KNXnetIPServiceType.TUNNELLING_REQUEST);
178
- }
179
- });
180
- });
181
158
  this.emit("connected");
182
159
  resolve();
183
160
  }
@@ -188,22 +165,124 @@ class KNXnetIPServer extends KNXService_1.KNXService {
188
165
  });
189
166
  });
190
167
  await connectPromise;
191
- if (this.externalManager) {
192
- this.externalManager.registerLink(this);
193
- await this.externalManager.connect();
194
- }
195
168
  }
196
169
  disconnect() {
197
- if (this.externalManager) {
198
- this.externalManager.unregisterLink(this);
199
- this.externalManager.disconnect();
200
- }
201
170
  if (this.socket) {
202
171
  this.socket.close();
203
172
  this.socket = null;
204
173
  }
205
174
  this.clearTimers();
206
175
  }
176
+ /**
177
+ * Discovers KNXnet/IP devices on the network by sending SEARCH_REQUEST and SEARCH_REQUEST_EXTENDED
178
+ * multicasts to 224.0.23.12:3671. Returns an array of discovered devices with their properties.
179
+ *
180
+ * @param timeout Wait time in milliseconds for responses
181
+ * @param useExtended Whether to send SEARCH_REQUEST_EXTENDED alongside SEARCH_REQUEST
182
+ * @returns Promise resolving to an array of discovered devices
183
+ */
184
+ static async discover(ipLocal = "", ipMulticast = "224.0.23.12", port = 3671, timeout = 3000, useExtended = true) {
185
+ return new Promise((resolve, reject) => {
186
+ const socket = dgram_1.default.createSocket({ type: "udp4", reuseAddr: true });
187
+ const discoveredDevices = new Map();
188
+ socket.on("message", (msg) => {
189
+ try {
190
+ const header = KNXnetIPHeader_1.KNXnetIPHeader.fromBuffer(msg);
191
+ if (header.serviceType === KNXnetIPEnum_1.KNXnetIPServiceType.SEARCH_RESPONSE ||
192
+ header.serviceType === KNXnetIPEnum_1.KNXnetIPServiceType.SEARCH_RESPONSE_EXTENDED) {
193
+ const body = msg.subarray(KNXnetIPHeader_1.KNXnetIPHeader.HEADER_SIZE_10);
194
+ if (body.length < 8)
195
+ return;
196
+ const hpai = KNXnetIPStructures_1.HPAI.fromBuffer(body);
197
+ let offset = 8; // Size of HPAI
198
+ let deviceInfo = null;
199
+ while (offset < body.length) {
200
+ const dibLen = body.readUInt8(offset);
201
+ if (dibLen === 0)
202
+ break;
203
+ if (offset + dibLen > body.length)
204
+ break;
205
+ const dibBuffer = body.subarray(offset, offset + dibLen);
206
+ const dib = KNXnetIPStructures_1.DIB.fromBuffer(dibBuffer);
207
+ if (dib instanceof KNXnetIPStructures_1.DeviceInformationDIB) {
208
+ deviceInfo = dib;
209
+ }
210
+ offset += dibLen;
211
+ }
212
+ if (deviceInfo) {
213
+ const deviceKey = `${hpai.ipAddress}:${hpai.port}`; // Unique by IP/Port
214
+ if (!discoveredDevices.has(deviceKey)) {
215
+ let knxMediumStr = `Unknown (${deviceInfo.knxMedium})`;
216
+ if (deviceInfo.knxMedium === KNXnetIPEnum_1.KNXMedium.TP1)
217
+ knxMediumStr = "TP1";
218
+ else if (deviceInfo.knxMedium === KNXnetIPEnum_1.KNXMedium.PL110)
219
+ knxMediumStr = "PL110";
220
+ else if (deviceInfo.knxMedium === KNXnetIPEnum_1.KNXMedium.RF)
221
+ knxMediumStr = "RF";
222
+ else if (deviceInfo.knxMedium === KNXnetIPEnum_1.KNXMedium.KNXIP)
223
+ knxMediumStr = "KNXIP";
224
+ const deviceStatusStr = deviceInfo.deviceStatus === 1
225
+ ? "Programmed"
226
+ : deviceInfo.deviceStatus === 0
227
+ ? "Not Programmed"
228
+ : `Unknown (${deviceInfo.deviceStatus})`;
229
+ discoveredDevices.set(deviceKey, {
230
+ ip: hpai.ipAddress,
231
+ port: hpai.port,
232
+ knxMediumRaw: deviceInfo.knxMedium,
233
+ knxMedium: knxMediumStr,
234
+ deviceStatusRaw: deviceInfo.deviceStatus,
235
+ deviceStatus: deviceStatusStr,
236
+ individualAddress: deviceInfo.individualAddress,
237
+ projectInstallationId: deviceInfo.projectInstallationId,
238
+ serialNumber: deviceInfo.serialNumber,
239
+ routingMulticastAddress: deviceInfo.routingMulticastAddress,
240
+ macAddress: deviceInfo.macAddress,
241
+ friendlyName: deviceInfo.friendlyName,
242
+ });
243
+ }
244
+ }
245
+ }
246
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
247
+ }
248
+ catch (e) {
249
+ // Ignore parsing errors for individual packets
250
+ }
251
+ });
252
+ const timer = setTimeout(() => {
253
+ socket.close();
254
+ resolve(Array.from(discoveredDevices.values()));
255
+ }, timeout);
256
+ socket.on("error", (err) => {
257
+ clearTimeout(timer);
258
+ socket.close();
259
+ reject(err);
260
+ });
261
+ socket.bind(0, () => {
262
+ const netInfo = (0, localIp_1.getNetworkInfo)();
263
+ const serverHPAI = new KNXnetIPStructures_1.HPAI(KNXnetIPEnum_1.HostProtocolCode.IPV4_UDP, ipLocal !== "" ? ipLocal : netInfo.address, socket.address().port);
264
+ const sendRequest = (serviceType) => {
265
+ const hpaiBuf = serverHPAI.toBuffer();
266
+ const header = new KNXnetIPHeader_1.KNXnetIPHeader(serviceType, KNXnetIPHeader_1.KNXnetIPHeader.HEADER_SIZE_10 + hpaiBuf.length);
267
+ const packet = Buffer.concat([header.toBuffer(), hpaiBuf]);
268
+ socket.send(packet, port, ipMulticast);
269
+ };
270
+ try {
271
+ socket.setBroadcast(true);
272
+ socket.setMulticastTTL(128);
273
+ sendRequest(KNXnetIPEnum_1.KNXnetIPServiceType.SEARCH_REQUEST);
274
+ if (useExtended) {
275
+ sendRequest(KNXnetIPEnum_1.KNXnetIPServiceType.SEARCH_REQUEST_EXTENDED);
276
+ }
277
+ }
278
+ catch (e) {
279
+ clearTimeout(timer);
280
+ socket.close();
281
+ reject(e);
282
+ }
283
+ });
284
+ });
285
+ }
207
286
  // [MEJORA] Validación estricta Route Back (NAT Traversal) según Especificación 8.6.2.2
208
287
  resolveRouteBack(hpai, rinfo) {
209
288
  const isIpZero = hpai.ipAddress === "0.0.0.0";
@@ -235,22 +314,28 @@ class KNXnetIPServer extends KNXService_1.KNXService {
235
314
  conn.close();
236
315
  });
237
316
  this._tunnelConnections.clear();
238
- this.removeAllListeners("indication");
317
+ this.removeAllListeners();
239
318
  }
319
+ /**
320
+ * Send a CEMI message or buffer CEMI to the bus.
321
+ * @param data The CEMI message or buffer to send.
322
+ */
240
323
  async send(data) {
241
324
  let cemiBuffer;
242
- let cemi;
325
+ let cemi = undefined;
243
326
  if (Buffer.isBuffer(data)) {
244
327
  cemiBuffer = data;
245
328
  try {
246
329
  cemi = CEMI_1.CEMI.fromBuffer(data);
247
330
  }
248
- catch (e) { }
331
+ catch (e) {
332
+ this.logger.debug("Error parsing CEMI buffer" + e.message);
333
+ }
249
334
  }
250
335
  else {
251
336
  cemi = data;
252
- if (data.controlField2) {
253
- const cf2 = data.controlField2;
337
+ if ("controlField2" in cemi) {
338
+ const cf2 = cemi.controlField2;
254
339
  const hopCount = cf2.hopCount;
255
340
  if (hopCount === 0)
256
341
  return;
@@ -259,17 +344,71 @@ class KNXnetIPServer extends KNXService_1.KNXService {
259
344
  }
260
345
  cemiBuffer = data.toBuffer();
261
346
  }
262
- if (cemi) {
263
- this.emit("indication", cemi);
347
+ if (cemi && "destinationAddress" in cemi && "sourceAddress" in cemi) {
348
+ this.emit("send", cemi);
349
+ if (!this.isCacheDelegated) {
350
+ GroupAddressCache_1.GroupAddressCache.getInstance().processCEMI(cemi);
351
+ }
352
+ if (!this.isEventsDelegated && cemi.destinationAddress) {
353
+ this.emit(cemi.destinationAddress, cemi);
354
+ }
355
+ const body = cemiBuffer;
356
+ const srcIAStr = cemi.sourceAddress;
357
+ let busmonBody = null;
358
+ this._tunnelConnections.forEach((conn) => {
359
+ // Echo cancellation: Don't forward back to the client that originated this message
360
+ if (srcIAStr === conn.knxAddressStr) {
361
+ return;
362
+ }
363
+ if (conn.knxLayer === KNXnetIPEnum_1.KNXLayer.BUSMONITOR_LAYER) {
364
+ if (!busmonBody)
365
+ busmonBody = this.convertDataIndToBusmonInd(body);
366
+ conn.enqueue(busmonBody, KNXnetIPEnum_1.KNXnetIPServiceType.TUNNELLING_REQUEST);
367
+ }
368
+ else {
369
+ // Link Layer or Raw Layer
370
+ conn.enqueue(body, KNXnetIPEnum_1.KNXnetIPServiceType.TUNNELLING_REQUEST);
371
+ }
372
+ });
264
373
  }
265
374
  await this.enqueuePacket(cemiBuffer);
266
375
  }
376
+ /**
377
+ * Send a raw CEMI buffer to the bus.
378
+ * @param cemiBuffer The CEMI buffer to send.
379
+ */
267
380
  async sendRaw(cemiBuffer) {
381
+ let cemi = undefined;
268
382
  try {
269
- const cemi = CEMI_1.CEMI.fromBuffer(cemiBuffer);
270
- this.emit("indication", cemi);
383
+ cemi = CEMI_1.CEMI.fromBuffer(cemiBuffer);
271
384
  }
272
- catch (e) { }
385
+ catch (e) {
386
+ this.logger.debug("Error parsing CEMI buffer" + e.message);
387
+ }
388
+ if (!cemi || !("destinationAddress" in cemi) || !("sourceAddress" in cemi))
389
+ return;
390
+ this.emit("send", cemi);
391
+ if (!this.isEventsDelegated && cemi.destinationAddress) {
392
+ this.emit(cemi.destinationAddress, cemi);
393
+ }
394
+ const body = cemiBuffer;
395
+ const srcIAStr = cemi.sourceAddress;
396
+ let busmonBody = null;
397
+ this._tunnelConnections.forEach((conn) => {
398
+ // Echo cancellation: Don't forward back to the client that originated this message
399
+ if (srcIAStr === conn.knxAddressStr) {
400
+ return;
401
+ }
402
+ if (conn.knxLayer === KNXnetIPEnum_1.KNXLayer.BUSMONITOR_LAYER) {
403
+ if (!busmonBody)
404
+ busmonBody = this.convertDataIndToBusmonInd(body);
405
+ conn.enqueue(busmonBody, KNXnetIPEnum_1.KNXnetIPServiceType.TUNNELLING_REQUEST);
406
+ }
407
+ else {
408
+ // Link Layer or Raw Layer
409
+ conn.enqueue(body, KNXnetIPEnum_1.KNXnetIPServiceType.TUNNELLING_REQUEST);
410
+ }
411
+ });
273
412
  await this.enqueuePacket(cemiBuffer);
274
413
  }
275
414
  async enqueuePacket(cemiBuffer) {
@@ -362,7 +501,7 @@ class KNXnetIPServer extends KNXService_1.KNXService {
362
501
  if (rinfo.address === this.options.localIp && rinfo.port === ourAddress.port)
363
502
  return;
364
503
  switch (header.serviceType) {
365
- case KNXnetIPEnum_1.KNXnetIPServiceType.ROUTING_INDICATION:
504
+ case KNXnetIPEnum_1.KNXnetIPServiceType.ROUTING_INDICATION: {
366
505
  // [MEJORA] Filtro Anti-Eco Seguro leyendo la Individual Address (IA) origen del CEMI
367
506
  const addInfoLen = body[1];
368
507
  if (body.length >= 6 + addInfoLen) {
@@ -374,10 +513,37 @@ class KNXnetIPServer extends KNXService_1.KNXService {
374
513
  this.emit("raw_indication", body);
375
514
  try {
376
515
  const cemi = CEMI_1.CEMI.fromBuffer(body);
516
+ if (!("destinationAddress" in cemi) || !("sourceAddress" in cemi))
517
+ return;
377
518
  this.emit("indication", cemi);
519
+ if (!this.isCacheDelegated) {
520
+ GroupAddressCache_1.GroupAddressCache.getInstance().processCEMI(cemi);
521
+ }
522
+ this.emit(cemi.destinationAddress, cemi);
523
+ const srcIAStr = cemi.sourceAddress;
524
+ let busmonBody = null;
525
+ this._tunnelConnections.forEach((conn) => {
526
+ // Echo cancellation: Don't forward back to the client that originated this message
527
+ if (srcIAStr === conn.knxAddressStr) {
528
+ return;
529
+ }
530
+ if (conn.knxLayer === KNXnetIPEnum_1.KNXLayer.BUSMONITOR_LAYER) {
531
+ if (!busmonBody)
532
+ busmonBody = this.convertDataIndToBusmonInd(body);
533
+ conn.enqueue(busmonBody, KNXnetIPEnum_1.KNXnetIPServiceType.TUNNELLING_REQUEST);
534
+ }
535
+ else {
536
+ // Link Layer or Raw Layer
537
+ conn.enqueue(body, KNXnetIPEnum_1.KNXnetIPServiceType.TUNNELLING_REQUEST);
538
+ }
539
+ });
540
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
541
+ }
542
+ catch (e) {
543
+ /* empty */
378
544
  }
379
- catch (e) { }
380
545
  break;
546
+ }
381
547
  case KNXnetIPEnum_1.KNXnetIPServiceType.ROUTING_BUSY:
382
548
  this.handleRoutingBusy(KNXnetIPStructures_1.RoutingBusy.fromBuffer(body));
383
549
  break;
@@ -442,7 +608,9 @@ class KNXnetIPServer extends KNXService_1.KNXService {
442
608
  if (!this.resolveRouteBack(clientHPAI, rinfo)) {
443
609
  return; // Silently drop invalid HPAI
444
610
  }
445
- const responseType = isExtended ? KNXnetIPEnum_1.KNXnetIPServiceType.SEARCH_RESPONSE_EXTENDED : KNXnetIPEnum_1.KNXnetIPServiceType.SEARCH_RESPONSE;
611
+ const responseType = isExtended
612
+ ? KNXnetIPEnum_1.KNXnetIPServiceType.SEARCH_RESPONSE_EXTENDED
613
+ : KNXnetIPEnum_1.KNXnetIPServiceType.SEARCH_RESPONSE;
446
614
  const serverHPAI = this.getHPAI(rinfo);
447
615
  const localIp = serverHPAI.ipAddress;
448
616
  const localPort = serverHPAI.port;
@@ -477,12 +645,12 @@ class KNXnetIPServer extends KNXService_1.KNXService {
477
645
  // Ensure we report the local IP of the interface that can actually route back to the client.
478
646
  if (rinfo && rinfo.address) {
479
647
  const interfaces = node_os_1.default.networkInterfaces();
480
- const rinfoNum = rinfo.address.split('.').reduce((acc, octet) => (acc << 8) + parseInt(octet, 10), 0) >>> 0;
648
+ const rinfoNum = rinfo.address.split(".").reduce((acc, octet) => (acc << 8) + parseInt(octet, 10), 0) >>> 0;
481
649
  for (const name of Object.keys(interfaces)) {
482
650
  for (const net of interfaces[name]) {
483
651
  if (net.family === "IPv4" && !net.internal) {
484
- const netNum = net.address.split('.').reduce((acc, octet) => (acc << 8) + parseInt(octet, 10), 0) >>> 0;
485
- const maskNum = net.netmask.split('.').reduce((acc, octet) => (acc << 8) + parseInt(octet, 10), 0) >>> 0;
652
+ const netNum = net.address.split(".").reduce((acc, octet) => (acc << 8) + parseInt(octet, 10), 0) >>> 0;
653
+ const maskNum = net.netmask.split(".").reduce((acc, octet) => (acc << 8) + parseInt(octet, 10), 0) >>> 0;
486
654
  if ((rinfoNum & maskNum) === (netNum & maskNum)) {
487
655
  localIp = net.address;
488
656
  }
@@ -538,7 +706,7 @@ class KNXnetIPServer extends KNXService_1.KNXService {
538
706
  const body = Buffer.concat([
539
707
  Buffer.from([channelId, status]),
540
708
  serverDataHPAI.toBuffer(),
541
- Buffer.from([0x02, KNXnetIPEnum_1.ConnectionType.DEVICE_MGMT_CONNECTION])
709
+ Buffer.from([0x02, KNXnetIPEnum_1.ConnectionType.DEVICE_MGMT_CONNECTION]),
542
710
  ]);
543
711
  const responseHeader = new KNXnetIPHeader_1.KNXnetIPHeader(KNXnetIPEnum_1.KNXnetIPServiceType.CONNECT_RESPONSE, KNXnetIPHeader_1.KNXnetIPHeader.HEADER_SIZE_10 + body.length);
544
712
  if (this.socket)
@@ -549,7 +717,9 @@ class KNXnetIPServer extends KNXService_1.KNXService {
549
717
  if (knxAddress === null || knxAddress === 0) {
550
718
  knxAddress = this.clientAddrsStartInt + channelId - 1;
551
719
  }
552
- if (cri.knxLayer !== KNXnetIPEnum_1.KNXLayer.LINK_LAYER && cri.knxLayer !== KNXnetIPEnum_1.KNXLayer.BUSMONITOR_LAYER && cri.knxLayer !== KNXnetIPEnum_1.KNXLayer.RAW_LAYER) {
720
+ if (cri.knxLayer !== KNXnetIPEnum_1.KNXLayer.LINK_LAYER &&
721
+ cri.knxLayer !== KNXnetIPEnum_1.KNXLayer.BUSMONITOR_LAYER &&
722
+ cri.knxLayer !== KNXnetIPEnum_1.KNXLayer.RAW_LAYER) {
553
723
  this.logger.warn(`Connect Request refused: Invalid layer ${cri.knxLayer}`);
554
724
  status = KNXnetIPEnum_1.KNXnetIPErrorCodes.E_TUNNELLING_LAYER;
555
725
  }
@@ -655,11 +825,11 @@ class KNXnetIPServer extends KNXService_1.KNXService {
655
825
  return;
656
826
  }
657
827
  const { action, status } = conn.validateRequest(seq);
658
- if (action === 'retransmit_ack') {
828
+ if (action === "retransmit_ack") {
659
829
  this.sendTunnelACK(channelId, seq, status, rinfo);
660
830
  return;
661
831
  }
662
- if (action === 'discard')
832
+ if (action === "discard")
663
833
  return;
664
834
  this.sendTunnelACK(channelId, seq, status, rinfo);
665
835
  const msgCode = cemiBuffer[0];
@@ -694,8 +864,8 @@ class KNXnetIPServer extends KNXService_1.KNXService {
694
864
  const header = new KNXnetIPHeader_1.KNXnetIPHeader(KNXnetIPEnum_1.KNXnetIPServiceType.TUNNELLING_ACK, KNXnetIPHeader_1.KNXnetIPHeader.HEADER_SIZE_10 + body.length);
695
865
  const conn = this._tunnelConnections.get(channelId);
696
866
  if (this.socket) {
697
- const port = conn ? conn.dataHPAI.port : (rinfo ? rinfo.port : 0);
698
- const addr = conn ? conn.dataHPAI.ipAddress : (rinfo ? rinfo.address : "");
867
+ const port = conn ? conn.dataHPAI.port : rinfo ? rinfo.port : 0;
868
+ const addr = conn ? conn.dataHPAI.ipAddress : rinfo ? rinfo.address : "";
699
869
  if (port > 0 && addr !== "") {
700
870
  this.socket.send(Buffer.concat([header.toBuffer(), body]), port, addr);
701
871
  }
@@ -712,11 +882,11 @@ class KNXnetIPServer extends KNXService_1.KNXService {
712
882
  }
713
883
  const { action, status } = conn.validateRequest(seq);
714
884
  this.logger.debug(`Feature Get for channel ${channelId}, feat: ${featId}, seq: ${seq}`);
715
- if (action === 'retransmit_ack') {
885
+ if (action === "retransmit_ack") {
716
886
  this.sendTunnelACK(channelId, seq, status);
717
887
  return;
718
888
  }
719
- if (action === 'discard')
889
+ if (action === "discard")
720
890
  return;
721
891
  this.sendTunnelACK(channelId, seq, KNXnetIPEnum_1.KNXnetIPErrorCodes.E_NO_ERROR);
722
892
  let featVal;
@@ -748,16 +918,17 @@ class KNXnetIPServer extends KNXService_1.KNXService {
748
918
  return;
749
919
  }
750
920
  const { action, status } = conn.validateRequest(seq);
751
- if (action === 'retransmit_ack') {
921
+ if (action === "retransmit_ack") {
752
922
  this.sendDeviceConfigACK(channelId, seq, status);
753
923
  return;
754
924
  }
755
- if (action === 'discard')
925
+ if (action === "discard")
756
926
  return;
757
927
  this.sendDeviceConfigACK(channelId, seq, KNXnetIPEnum_1.KNXnetIPErrorCodes.E_NO_ERROR);
758
928
  try {
759
929
  const msgCode = cemiBuffer.readUInt8(0);
760
- if (msgCode === 0xFC) { // M_PropRead.req
930
+ if (msgCode === 0xfc) {
931
+ // M_PropRead.req
761
932
  const req = CEMI_1.CEMI.ManagementCEMI["M_PropRead.req"].fromBuffer(cemiBuffer);
762
933
  this.logger.debug(`Management PropRead: Obj=${req.interfaceObjectType}, Prop=${req.propertyId} on channel ${channelId}`);
763
934
  let data = Buffer.alloc(0);
@@ -809,12 +980,12 @@ class KNXnetIPServer extends KNXService_1.KNXService {
809
980
  }
810
981
  }
811
982
  // Spec says use TP1 (0x02) for gateway reporting
812
- const devInfo = new KNXnetIPStructures_1.DeviceInformationDIB(32 /* KNXMedium.KNXIP */, 0, KNXHelper_1.KNXHelper.GetAddress(routingOptions.individualAddress, ".").readUint16BE(), 0, routingOptions.serialNumber, this.options.ip, routingOptions.macAddress, routingOptions.friendlyName);
983
+ const devInfo = new KNXnetIPStructures_1.DeviceInformationDIB(KNXnetIPEnum_1.KNXMedium.KNXIP, 0, KNXHelper_1.KNXHelper.GetAddress(routingOptions.individualAddress, ".").readUint16BE(), 0, routingOptions.serialNumber, this.options.ip, routingOptions.macAddress, routingOptions.friendlyName);
813
984
  const suppSvc = new KNXnetIPStructures_1.SupportedServicesDIB([
814
- { family: 2 /* AllowedSupportedServiceFamilies.Core */, version: 1 },
815
- { family: 3 /* AllowedSupportedServiceFamilies.DeviceManagement */, version: 1 },
816
- { family: 4 /* AllowedSupportedServiceFamilies.Tunnelling */, version: 1 },
817
- { family: 5 /* AllowedSupportedServiceFamilies.Routing */, version: 1 },
985
+ { family: KNXnetIPEnum_1.AllowedSupportedServiceFamilies.Core, version: 1 },
986
+ { family: KNXnetIPEnum_1.AllowedSupportedServiceFamilies.DeviceManagement, version: 1 },
987
+ { family: KNXnetIPEnum_1.AllowedSupportedServiceFamilies.Tunnelling, version: 1 },
988
+ { family: KNXnetIPEnum_1.AllowedSupportedServiceFamilies.Routing, version: 1 },
818
989
  ]);
819
990
  if (serviceType === KNXnetIPEnum_1.KNXnetIPServiceType.SEARCH_RESPONSE) {
820
991
  return [devInfo, suppSvc];
@@ -832,7 +1003,7 @@ class KNXnetIPServer extends KNXService_1.KNXService {
832
1003
  status.free = !conn;
833
1004
  slots.push({
834
1005
  address: conn ? conn.knxAddress : this.clientAddrsStartInt + i - 1,
835
- status: status
1006
+ status: status,
836
1007
  });
837
1008
  }
838
1009
  return [devInfo, suppSvc, extDevInfo, ipConfig, ipCurrent, new KNXnetIPStructures_1.TunnellingInfoDIB(254, slots)];
@@ -889,7 +1060,14 @@ class KNXnetIPServer extends KNXService_1.KNXService {
889
1060
  const dst = cemiBuffer.subarray(baseOffset + 4, baseOffset + 6);
890
1061
  const dataLen = cemiBuffer[baseOffset + 6];
891
1062
  const tpdu = cemiBuffer.subarray(baseOffset + 7);
892
- const lpdu = Buffer.concat([Buffer.from([cf1]), src, dst, Buffer.from([(cf2 & 0xf0) | (dataLen + 1)]), tpdu, Buffer.alloc(1)]);
1063
+ const lpdu = Buffer.concat([
1064
+ Buffer.from([cf1]),
1065
+ src,
1066
+ dst,
1067
+ Buffer.from([(cf2 & 0xf0) | (dataLen + 1)]),
1068
+ tpdu,
1069
+ Buffer.alloc(1),
1070
+ ]);
893
1071
  let xor = 0;
894
1072
  for (let i = 0; i < lpdu.length - 1; i++)
895
1073
  xor ^= lpdu[i];