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
@@ -55,6 +55,10 @@ export declare abstract class DIB {
55
55
  }
56
56
  export declare class DeviceInformationDIB extends DIB {
57
57
  knxMedium: KNXMedium;
58
+ /**
59
+ * 1 = Device is programmed
60
+ * 0 = Device is not programmed
61
+ */
58
62
  deviceStatus: 1 | 0 | number;
59
63
  individualAddress: number;
60
64
  projectInstallationId: number;
@@ -62,7 +66,12 @@ export declare class DeviceInformationDIB extends DIB {
62
66
  routingMulticastAddress: string;
63
67
  macAddress: string;
64
68
  friendlyName: string;
65
- constructor(knxMedium: KNXMedium, deviceStatus: 1 | 0 | number, individualAddress: number, projectInstallationId: number, serialNumber: Buffer, routingMulticastAddress: string, macAddress: string, friendlyName: string);
69
+ constructor(knxMedium: KNXMedium,
70
+ /**
71
+ * 1 = Device is programmed
72
+ * 0 = Device is not programmed
73
+ */
74
+ deviceStatus: 1 | 0 | number, individualAddress: number, projectInstallationId: number, serialNumber: Buffer, routingMulticastAddress: string, macAddress: string, friendlyName: string);
66
75
  toBuffer(): Buffer;
67
76
  static fromBuffer(buffer: Buffer): DeviceInformationDIB;
68
77
  }
@@ -20,10 +20,7 @@ class HPAI {
20
20
  return this._hostProtocol;
21
21
  }
22
22
  set port(port) {
23
- if (isNaN(port) ||
24
- typeof port !== 'number' ||
25
- port < 0 ||
26
- port > 65535) {
23
+ if (isNaN(port) || typeof port !== "number" || port < 0 || port > 65535) {
27
24
  throw new Error(`Invalid port ${port}`);
28
25
  }
29
26
  this._port = port;
@@ -33,7 +30,7 @@ class HPAI {
33
30
  }
34
31
  set ipAddress(host) {
35
32
  if (host == null) {
36
- throw new Error('Host undefined');
33
+ throw new Error("Host undefined");
37
34
  }
38
35
  const m = host.match(/(\d+)\.(\d+)\.(\d+)\.(\d+)/);
39
36
  if (m === null) {
@@ -233,7 +230,12 @@ class DeviceInformationDIB extends DIB {
233
230
  routingMulticastAddress;
234
231
  macAddress;
235
232
  friendlyName;
236
- constructor(knxMedium, deviceStatus, individualAddress, projectInstallationId, serialNumber, routingMulticastAddress, macAddress, friendlyName) {
233
+ constructor(knxMedium,
234
+ /**
235
+ * 1 = Device is programmed
236
+ * 0 = Device is not programmed
237
+ */
238
+ deviceStatus, individualAddress, projectInstallationId, serialNumber, routingMulticastAddress, macAddress, friendlyName) {
237
239
  super(KNXnetIPEnum_2.DescriptionType.DEVICE_INFO);
238
240
  this.knxMedium = knxMedium;
239
241
  this.deviceStatus = deviceStatus;
@@ -259,6 +261,7 @@ class DeviceInformationDIB extends DIB {
259
261
  buffer.writeUInt8(mcast[1], 15);
260
262
  buffer.writeUInt8(mcast[2], 16);
261
263
  buffer.writeUInt8(mcast[3], 17);
264
+ // eslint-disable-next-line no-useless-escape
262
265
  const mac = this.macAddress.replace(/[:\-]/g, "");
263
266
  Buffer.from(mac, "hex").copy(buffer, 18);
264
267
  nameBuf.copy(buffer, 24, 0);
@@ -367,7 +370,7 @@ class IPCurrentConfigDIB extends DIB {
367
370
  exports.IPCurrentConfigDIB = IPCurrentConfigDIB;
368
371
  class StatusTunnelingSlot {
369
372
  _value;
370
- constructor(initialValue = 0xFFF8) {
373
+ constructor(initialValue = 0xfff8) {
371
374
  this._value = initialValue;
372
375
  }
373
376
  get value() {
@@ -435,7 +438,7 @@ class TunnellingInfoDIB extends DIB {
435
438
  for (let i = 4; i < buffer.length; i += 4) {
436
439
  slots.push({
437
440
  address: buffer.readUInt16BE(i),
438
- status: new StatusTunnelingSlot(buffer.readUInt16BE(i + 2))
441
+ status: new StatusTunnelingSlot(buffer.readUInt16BE(i + 2)),
439
442
  });
440
443
  }
441
444
  return new TunnellingInfoDIB(apduLength, slots);
@@ -589,7 +592,9 @@ class UnknownDIB extends DIB {
589
592
  super(type);
590
593
  this.rawData = rawData;
591
594
  }
592
- toBuffer() { return this.rawData; }
595
+ toBuffer() {
596
+ return this.rawData;
597
+ }
593
598
  }
594
599
  exports.UnknownDIB = UnknownDIB;
595
600
  class SRP {
@@ -616,7 +621,7 @@ class SRP {
616
621
  const len = buffer.readUInt8(0);
617
622
  const type = buffer.readUInt8(1);
618
623
  const data = buffer.subarray(2, len);
619
- return new SRP(type & 0x7F, data, (type & 0x80) !== 0);
624
+ return new SRP(type & 0x7f, data, (type & 0x80) !== 0);
620
625
  }
621
626
  }
622
627
  exports.SRP = SRP;
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * The default destination is the layer where the system shall direct the message to if no redirection is set.
3
3
  */
4
- declare enum DefaultDestination {
4
+ declare const enum DefaultDestination {
5
5
  Data_Link_Layer = "DLL",
6
6
  Network_Layer = "NL",
7
7
  Transport_Layer = "TL",
@@ -0,0 +1,57 @@
1
+ import { CEMIInstance } from "../CEMI";
2
+ import { KNXService } from "../../connection/KNXService";
3
+ export interface CacheEntry {
4
+ cemi: CEMIInstance;
5
+ timestamp: Date;
6
+ groupAddress: string;
7
+ decodedValue?: any;
8
+ }
9
+ export declare class GroupAddressCache {
10
+ private static instance;
11
+ private enabled;
12
+ /**
13
+ * Maximum number of group addresses to cache. Default is 65535 (all possible group addresses).
14
+ */
15
+ private maxAddresses;
16
+ /**
17
+ * Maximum number of messages to cache per group address. Default is 10.
18
+ */
19
+ private maxMessagesPerAddress;
20
+ private cache;
21
+ private dptConfig;
22
+ private constructor();
23
+ static getInstance(): GroupAddressCache;
24
+ setEnabled(enabled: boolean): void;
25
+ isEnabled(): boolean;
26
+ configure(maxAddresses: number, maxMessagesPerAddress?: number): void;
27
+ setAddressDPT(address: string, dpt: string | number): void;
28
+ getAddressDPT(address: string): string | number | undefined;
29
+ encodeValue(address: string, value: any): Buffer | null;
30
+ /**
31
+ * Processes an incoming CEMI message. If caching is enabled, saves the last messages
32
+ * indicating a GroupValue_Read or GroupValue_Response targeted to a Group Address.
33
+ */
34
+ processCEMI(cemi: CEMIInstance): void;
35
+ /**
36
+ * Clears the entire cache map.
37
+ */
38
+ clear(): void;
39
+ /**
40
+ * Deletes a specific address from the cache.
41
+ * @param address KNX Group Address e.g. '1/2/3'
42
+ */
43
+ deleteAddress(address: string): boolean;
44
+ /**
45
+ * Core query method to retrieve messages for given address(es) within a time range.
46
+ * If dates aren't specified, retrieves all currently cached items depending on the query payload.
47
+ * By default, it retrieves the absolute latest if neither startDate nor endDate are applied.
48
+ */
49
+ query(addresses: string | string[], startDate?: Date, endDate?: Date, returnOnlyLatest?: boolean): CacheEntry[];
50
+ /**
51
+ * Prompts the KNXService to issue an A_GroupValue_Read to the bus on the specified address
52
+ * and awaits an A_GroupValue_Response. The response is automatically processed via standard
53
+ * listeners (if processCEMI is hooked properly), but this method attaches an event
54
+ * one-off listener or handles resolving it.
55
+ */
56
+ readDirectAsync(address: string, serviceContext: KNXService, timeoutMs?: number): Promise<CacheEntry | null>;
57
+ }
@@ -0,0 +1,227 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.GroupAddressCache = void 0;
4
+ const APCIEnum_1 = require("../enum/APCIEnum");
5
+ const KNXDataDecode_1 = require("../data/KNXDataDecode");
6
+ const KNXDataEncode_1 = require("../data/KNXDataEncode");
7
+ class GroupAddressCache {
8
+ static instance;
9
+ enabled = false;
10
+ /**
11
+ * Maximum number of group addresses to cache. Default is 65535 (all possible group addresses).
12
+ */
13
+ maxAddresses = 65535;
14
+ /**
15
+ * Maximum number of messages to cache per group address. Default is 10.
16
+ */
17
+ maxMessagesPerAddress = 10;
18
+ cache = new Map();
19
+ dptConfig = new Map();
20
+ constructor() { }
21
+ static getInstance() {
22
+ if (!GroupAddressCache.instance) {
23
+ GroupAddressCache.instance = new GroupAddressCache();
24
+ }
25
+ return GroupAddressCache.instance;
26
+ }
27
+ setEnabled(enabled) {
28
+ this.enabled = enabled;
29
+ }
30
+ isEnabled() {
31
+ return this.enabled;
32
+ }
33
+ configure(maxAddresses, maxMessagesPerAddress = 10) {
34
+ this.maxAddresses = maxAddresses;
35
+ this.maxMessagesPerAddress = maxMessagesPerAddress;
36
+ }
37
+ setAddressDPT(address, dpt) {
38
+ this.dptConfig.set(address, dpt);
39
+ }
40
+ getAddressDPT(address) {
41
+ return this.dptConfig.get(address);
42
+ }
43
+ encodeValue(address, value) {
44
+ const dpt = this.dptConfig.get(address);
45
+ if (!dpt)
46
+ return null;
47
+ try {
48
+ return KNXDataEncode_1.KnxDataEncoder.encodeThis(dpt, value);
49
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
50
+ }
51
+ catch (err) {
52
+ return null;
53
+ }
54
+ }
55
+ /**
56
+ * Processes an incoming CEMI message. If caching is enabled, saves the last messages
57
+ * indicating a GroupValue_Read or GroupValue_Response targeted to a Group Address.
58
+ */
59
+ processCEMI(cemi) {
60
+ if (!("controlField2" in cemi) || !("TPDU" in cemi))
61
+ return;
62
+ if (!this.enabled)
63
+ return;
64
+ if (cemi.controlField2?.addressType !== 1) {
65
+ return; // Not a group address
66
+ }
67
+ const apciObj = cemi.TPDU.apdu.apci;
68
+ if (!apciObj)
69
+ return;
70
+ const apci = apciObj;
71
+ if (apci.value !== APCIEnum_1.APCIEnum.A_GroupValue_Read_Protocol_Data_Unit &&
72
+ apci.value !== APCIEnum_1.APCIEnum.A_GroupValue_Response_Protocol_Data_Unit) {
73
+ return;
74
+ }
75
+ const targetAddress = cemi.destinationAddress;
76
+ if (!targetAddress)
77
+ return;
78
+ // Initialize list if needed
79
+ if (!this.cache.has(targetAddress)) {
80
+ if (this.cache.size >= this.maxAddresses) {
81
+ // Enforce max addresses limit (naive approach: evict oldest entry by insertion order)
82
+ const firstKey = this.cache.keys().next().value;
83
+ if (firstKey !== undefined) {
84
+ this.cache.delete(firstKey);
85
+ }
86
+ }
87
+ this.cache.set(targetAddress, []);
88
+ }
89
+ const entries = this.cache.get(targetAddress);
90
+ let decodedValue = undefined;
91
+ const dpt = this.dptConfig.get(targetAddress);
92
+ if (dpt && cemi.TPDU.apdu.data) {
93
+ try {
94
+ decodedValue = KNXDataDecode_1.KnxDataDecode.decodeThis(dpt, cemi.TPDU.apdu.data);
95
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
96
+ }
97
+ catch (err) {
98
+ // failed to decode
99
+ }
100
+ }
101
+ // Create new entry
102
+ const entry = {
103
+ cemi: cemi,
104
+ timestamp: new Date(),
105
+ groupAddress: targetAddress,
106
+ decodedValue: decodedValue,
107
+ };
108
+ // Insert at front
109
+ entries.unshift(entry);
110
+ // Trim length
111
+ if (entries.length > this.maxMessagesPerAddress) {
112
+ entries.length = this.maxMessagesPerAddress;
113
+ }
114
+ }
115
+ /**
116
+ * Clears the entire cache map.
117
+ */
118
+ clear() {
119
+ this.cache.clear();
120
+ }
121
+ /**
122
+ * Deletes a specific address from the cache.
123
+ * @param address KNX Group Address e.g. '1/2/3'
124
+ */
125
+ deleteAddress(address) {
126
+ return this.cache.delete(address);
127
+ }
128
+ /**
129
+ * Core query method to retrieve messages for given address(es) within a time range.
130
+ * If dates aren't specified, retrieves all currently cached items depending on the query payload.
131
+ * By default, it retrieves the absolute latest if neither startDate nor endDate are applied.
132
+ */
133
+ query(addresses, startDate, endDate = new Date(), returnOnlyLatest = true) {
134
+ const addressArray = Array.isArray(addresses) ? addresses : [addresses];
135
+ const results = [];
136
+ for (const address of addressArray) {
137
+ const entries = this.cache.get(address) || [];
138
+ if (entries.length === 0)
139
+ continue;
140
+ if (!startDate) {
141
+ // If no start date, we return the very latest (index 0) if default or requested
142
+ if (returnOnlyLatest) {
143
+ results.push(entries[0]);
144
+ }
145
+ else {
146
+ // Or return everything relative to the endDate
147
+ results.push(...entries.filter((e) => e.timestamp <= endDate));
148
+ }
149
+ }
150
+ else {
151
+ // Filter by date range
152
+ const validEntries = entries.filter((e) => e.timestamp >= startDate && e.timestamp <= endDate);
153
+ if (returnOnlyLatest && validEntries.length > 0) {
154
+ results.push(validEntries[0]);
155
+ }
156
+ else {
157
+ results.push(...validEntries);
158
+ }
159
+ }
160
+ }
161
+ return results;
162
+ }
163
+ /**
164
+ * Prompts the KNXService to issue an A_GroupValue_Read to the bus on the specified address
165
+ * and awaits an A_GroupValue_Response. The response is automatically processed via standard
166
+ * listeners (if processCEMI is hooked properly), but this method attaches an event
167
+ * one-off listener or handles resolving it.
168
+ */
169
+ async readDirectAsync(address, serviceContext, timeoutMs = 3000) {
170
+ return new Promise((resolve, reject) => {
171
+ // eslint-disable-next-line prefer-const
172
+ let timer;
173
+ // Listener context reference to intercept indications specifically for this read call
174
+ const listener = (cemi) => {
175
+ if (cemi.controlField2?.addressType === 1 && cemi.destinationAddress === address) {
176
+ const apciObj = cemi.TPDU.apdu.apci;
177
+ if (apciObj && apciObj.value === APCIEnum_1.APCIEnum.A_GroupValue_Response_Protocol_Data_Unit) {
178
+ // Clean up
179
+ clearTimeout(timer);
180
+ serviceContext.off("indication", listener);
181
+ // Ensure caching happens if not processed previously (if processCEMI wasn't hooked at higher layer for some reason)
182
+ this.processCEMI(cemi);
183
+ // Find in cache
184
+ const entries = this.cache.get(address);
185
+ if (entries && entries.length > 0 && entries[0].cemi === cemi) {
186
+ resolve(entries[0]);
187
+ }
188
+ else {
189
+ // Return mock CacheEntry if cache processing was disabled or failed
190
+ let decodedValue = undefined;
191
+ const dpt = this.dptConfig.get(address);
192
+ if (dpt && cemi.TPDU.apdu.data) {
193
+ try {
194
+ decodedValue = KNXDataDecode_1.KnxDataDecode.decodeThis(dpt, cemi.TPDU.apdu.data);
195
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
196
+ }
197
+ catch (e) {
198
+ // empty
199
+ }
200
+ }
201
+ resolve({
202
+ cemi: cemi,
203
+ timestamp: new Date(),
204
+ groupAddress: address,
205
+ decodedValue: decodedValue,
206
+ });
207
+ }
208
+ }
209
+ }
210
+ };
211
+ // Set explicit listener on the service context
212
+ serviceContext.on("indication", listener);
213
+ // Start the timer to abort request
214
+ timer = setTimeout(() => {
215
+ serviceContext.off("indication", listener);
216
+ reject(new Error(`Timeout of ${timeoutMs}ms exceeded while waiting for GroupValue_Response on ${address}`));
217
+ }, timeoutMs);
218
+ // Issue the command to the bus
219
+ serviceContext.read(address).catch((err) => {
220
+ clearTimeout(timer);
221
+ serviceContext.off("indication", listener);
222
+ reject(err);
223
+ });
224
+ });
225
+ }
226
+ }
227
+ exports.GroupAddressCache = GroupAddressCache;
@@ -5,7 +5,7 @@ import { KNXData } from "./KNXData";
5
5
  */
6
6
  export declare class KnxDataDecode extends KNXData {
7
7
  private constructor();
8
- static decodeThis<T extends typeof KnxDataDecode.dptEnum[number] | string>(dpt: T, buffer: Buffer): DecodedDPTType<T>;
8
+ static decodeThis<T extends (typeof KnxDataDecode.dptEnum)[number] | string>(dpt: T, buffer: Buffer): DecodedDPTType<T>;
9
9
  static get dptEnum(): readonly [1, 2, 3007, 3008, 4001, 4002, 5, 5001, 5002, 6, 6001, 6010, 6020, 7, 7001, 7002, 7003, 7004, 7005, 7006, 7007, 7011, 7012, 7013, 8, 9, 10001, 11001, 12001, 12002, 13, 13001, 13002, 13010, 13011, 13012, 13013, 13014, 13015, 13016, 13100, 14, 15000, 16, 16002, 20, 20001, 20002, 20003, 20004, 20005, 20006, 20007, 20008, 20011, 20012, 20013, 20014, 20017, 20020, 20021, 20022, 27001, 28001, 29, 29010, 29011, 29012, 232600, 238600, 245600, 250600, 251600];
10
10
  private static toPercentage;
11
11
  private static toAngle;
@@ -187,7 +187,7 @@ export declare class KnxDataDecode extends KNXData {
187
187
  *
188
188
  * @returns Un objeto con el valor sin signo y la unidad seleccionada.
189
189
  */
190
- static asDpt12002(buffer: Buffer, variant?: 'sec' | 'min' | 'hrs'): {
190
+ static asDpt12002(buffer: Buffer, variant?: "sec" | "min" | "hrs"): {
191
191
  value: number;
192
192
  unit: string;
193
193
  };