knx.ts 1.0.7 → 1.0.8

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.
package/README.md CHANGED
@@ -8,7 +8,7 @@ This project focuses on protocol strictness, reading and sending any kind of **E
8
8
 
9
9
  ## 🌟 Capabilities
10
10
 
11
- - **Robust UDP Tunneling**: Implements a strict *Stop-and-Wait* queue and sequence number management. This eliminates common "connection interrupted" issues in ETS or other applications during long sessions.
11
+ - **Robust UDP Tunneling**: Implements a strict _Stop-and-Wait_ queue and sequence number management. This eliminates common "connection interrupted" issues in ETS or other applications during long sessions.
12
12
  - **KNXnet/IP Routing**: Supports multicast routing (only in the **KNXnet/IP** server).
13
13
  - **Discovery in the KNXnet/IP server**: Supports `SEARCH_REQUEST`, `SEARCH_REQUEST_EXTENDED`, `DESCRIPTION_REQUEST`, `CONNECT_REQUEST`, and `CONNECTIONSTATE_REQUEST` so applications such as ETS can discover it without manual configuration.
14
14
  - **Direct Hardware Interfaces**: Native support for **KNX USB interfaces** (via `node-hid`) and **TPUART** serial chips (via `serialport`).
@@ -22,7 +22,7 @@ This project focuses on protocol strictness, reading and sending any kind of **E
22
22
  According to `TODO.md`, several features are currently **experimental** or under development:
23
23
 
24
24
  - **TCP Support**: The implementation is present, but testing is currently in an experimental phase.
25
- - **Device Parameterization**: Support for *Programming Mode* (`progMode`) is planned to allow full device configuration through ETS.
25
+ - **Device Parameterization**: Support for _Programming Mode_ (`progMode`) is planned to allow full device configuration through ETS.
26
26
  - **Source Filtering**: Filtering based on source addresses and selective routing is on the roadmap.
27
27
  - **Use of NPDU, TPDU, and APDU layers**: EMI still needs to use them for correct deserialization.
28
28
 
@@ -82,6 +82,7 @@ For developers building advanced monitoring or injection tools, the library expo
82
82
  - `TPCIType` is an enum that helps write or identify TPCI values according to the specification.
83
83
  - `DPTs`: the library exports interfaces such as `DPT5001` or `DPT1`. These interfaces are used by `KnxDataDecode` to return JavaScript objects and by `KnxDataEncoder` as parameter types to convert them into `Buffer`s.
84
84
  - `ServiceMessage` is an interface implemented by all CEMI and EMI messages, and also by NPDU, TPDU, and APCI layers. This is useful because they all expose two helpful methods: `toBuffer` to serialize the instance into a buffer and `describe` to provide a human-readable view of the instance. **Note**: most exported classes in this library that do not implement this interface, such as `APCI`, still provide a `describe` method.
85
+ - `CEMIInstance` is an type of all CEMI Instances.
85
86
 
86
87
  ## 🛠️ Quick Start
87
88
 
@@ -90,60 +91,170 @@ For developers building advanced monitoring or injection tools, the library expo
90
91
  Perfect for creating a bridge between your IP network and the KNX bus.
91
92
 
92
93
  ```typescript
93
- import { KNXnetIPServer, ServiceMessage, KnxDataDecode } from 'knx.ts';
94
+ import { KNXnetIPServer, CEMIInstance, KnxDataDecode } from "knx.ts";
94
95
 
95
96
  const server = new KNXnetIPServer({
96
- localIp: '192.168.1.50',
97
- individualAddress: '1.1.0', // Be careful not to create conflicts
98
- friendlyName: 'TypeScript KNX Gateway', // This name is shown in ETS
99
- clientAddrs: '1.1.10:5' // Provides 5 tunneling slots starting from 1.1.10
97
+ localIp: "192.168.1.50",
98
+ individualAddress: "1.1.0", // Be careful not to create conflicts
99
+ friendlyName: "TypeScript KNX Gateway", // This name is shown in ETS
100
+ clientAddrs: "1.1.10:5", // Provides 5 tunneling slots starting from 1.1.10
100
101
  });
101
102
 
102
103
  server.connect().then(() => {
103
- console.log('The KNXnet/IP server is running');
104
+ console.log("The KNXnet/IP server is running");
104
105
  });
105
106
 
106
107
  // Specific listener for a Group Address
107
- server.on('1/1/1', (cemi: ServiceMessage) => {
108
- console.log('New data on 1/1/1:', cemi.TPDU.apdu.data); // Raw APDU data
109
- console.log('Decoded data:', KnxDataDecode.decodeThis("1.001", cemi.TPDU.apdu.data)); // Converted JavaScript value
108
+ server.on("1/1/1", (cemi: CEMIInstance) => {
109
+ console.log("New data on 1/1/1:", cemi.TPDU.apdu.data); // Raw APDU data
110
+ console.log("Decoded data:", KnxDataDecode.decodeThis("1.001", cemi.TPDU.apdu.data)); // Converted JavaScript value
110
111
  });
111
112
  ```
112
113
 
113
114
  ### Direct USB Connection
114
115
 
115
116
  ```typescript
116
- import { KNXUSBConnection } from 'knx.ts';
117
+ import { KNXUSBConnection } from "knx.ts";
117
118
 
118
119
  const usb = new KNXUSBConnection({
119
120
  // Omitting path/vendorId will automatically discover the first known KNX USB interface
120
121
  });
121
122
 
122
123
  usb.connect().then(() => {
123
- console.log('Connected directly to the KNX USB interface');
124
+ console.log("Connected directly to the KNX USB interface");
124
125
  });
125
126
 
126
- usb.on('indication', (cemi) => {
127
- console.log('USB telegram source:', cemi.sourceAddress);
127
+ usb.on("indication", (cemi) => {
128
+ console.log("USB telegram source:", cemi.sourceAddress);
128
129
  });
129
130
  ```
130
131
 
131
132
  ### Tunneling Client
132
133
 
133
134
  ```typescript
134
- import { KNXTunneling } from 'knx.ts';
135
+ import { KNXTunneling } from "knx.ts";
135
136
 
136
137
  const tunnel = new KNXTunneling({
137
- ip: '192.168.1.100',
138
+ ip: "192.168.1.100",
138
139
  port: 3671,
139
- localIp: '192.168.1.50'
140
+ localIp: "192.168.1.50",
140
141
  });
141
142
 
142
143
  tunnel.connect().then(() => {
143
- console.log('Connected to the KNX bus');
144
+ console.log("Connected to the KNX bus");
144
145
  });
145
146
  ```
146
147
 
148
+ ## 🌐 WebSocket & MQTT Gateways (API)
149
+
150
+ ### GroupAddressCache (Integrated Caching)
151
+
152
+ The gateways depend heavily on `GroupAddressCache` to know the corresponding DPT for each group address. This allows them to seamlessly decode values and encode short primitives without requiring payload metadata on every request.
153
+
154
+ Every `KNXService` (like `KNXnetIPServer`, `Router`, etc.) has an integrated `GroupAddressCache`. It listens to incoming telegrams and remembers the last known values as well as configured DPTs, which is critical for query actions or state tracking in the Gateways.
155
+
156
+ > **Important**: This cache is disabled by default to conserve memory, but if you plan to use the `KNXWebSocketGateway` or `KNXMQTTGateway` servers, it will be enabled.
157
+
158
+ To enable caching, you must explicitly enable it globally:
159
+
160
+ ```typescript
161
+ import { GroupAddressCache } from "knx.ts";
162
+
163
+ // Enable the cache singleton
164
+ GroupAddressCache.getInstance().setEnabled(true);
165
+
166
+ // You can optionally configure its limits (max addresses, max messages per address)
167
+ GroupAddressCache.getInstance().configure(65535, 10);
168
+ ```
169
+
170
+ If you create an instance of a router and register links with it, the router will handle cache management and do the same for destination address events.
171
+
172
+ ```typescript
173
+ import { Router, KNXUSBConnection } from "knx.ts";
174
+
175
+ const router = new Router();
176
+ const usb = new KNXUSBConnection();
177
+ // The router will manage the link address cache and events related to destination addresses, rather than the link itself.
178
+ router.addLink(usb);
179
+
180
+ router.on("1/1/1", (cemi: CEMIInstance) => {
181
+ // <--- it activates
182
+ console.log("New data on 1/1/1:", cemi.TPDU.apdu.data); // Raw APDU data
183
+ console.log("Decoded data:", KnxDataDecode.decodeThis("1.001", cemi.TPDU.apdu.data)); // Converted JavaScript value
184
+ });
185
+
186
+ usb.on("1/1/1", (cemi: CEMIInstance) => {
187
+ // <--- it doesn't activate
188
+ console.log("New data on 1/1/1:", cemi.TPDU.apdu.data); // Raw APDU data
189
+ console.log("Decoded data:", KnxDataDecode.decodeThis("1.001", cemi.TPDU.apdu.data)); // Converted JavaScript value
190
+ });
191
+ ```
192
+
193
+ The library provides built-in Gateway servers allowing easy integration with web interfaces, dashboards, or IoT platforms. Both gateways act as a bridge over an existing `Router` or `KNXService` (like `KNXnetIPServer` or `KNXUSBConnection`).
194
+
195
+ The messages API payloads are exported as `WSClientPayload`, `WSServerPayload`, `MQTTCommandPayload`, and `MQTTStatePayload` so you can use them directly in your TypeScript projects.
196
+
197
+ ### WebSocket Gateway
198
+
199
+ The `KNXWebSocketGateway` provides a simple, JSON-based bidirectional API over WebSockets.
200
+
201
+ ```typescript
202
+ import { KNXWebSocketGateway } from "knx.ts/server";
203
+ // Or use: import { KNXWebSocketGateway } from 'knx.ts' if exported from root.
204
+
205
+ const wsGateway = new KNXWebSocketGateway({
206
+ port: 8080,
207
+ knxContext: router, // Provide a Router or any connected KNXService instance
208
+ });
209
+
210
+ wsGateway.start();
211
+ ```
212
+
213
+ **WebSocket API JSON Payloads:**
214
+
215
+ - **Read / Querying**: Request to read from the KNX bus or query cached values.
216
+ - Request: `{ "action": "read", "groupAddress": "1/2/3" }`
217
+ - Response: `{ "action": "read_result", "groupAddress": "1/2/3", "data": ... }`
218
+ - Query Cache: `{ "action": "query", "groupAddress": "1/2/3", "onlyLatest": true }`
219
+
220
+ - **Write**: Send telegrams to the KNX bus.
221
+ - Command: `{ "action": "write", "groupAddress": "1/2/3", "value": 22.5, "dpt": "9.001" }`
222
+
223
+ - **Subscribe / Unsubscribe**: Listen to bus events.
224
+ - Subscribe: `{ "action": "subscribe", "groupAddress": "1/2/3" }` (Use `"*"` for all addresses).
225
+ - Event Response: `{ "action": "event", "groupAddress": "1/2/3", "decodedValue": ... }`
226
+
227
+ - **Configure DPT**: Let the cache know which DPT corresponds to an address.
228
+ - Command: `{ "action": "config_dpt", "groupAddress": "1/2/3", "dpt": "1.001" }`
229
+
230
+ ### MQTT Gateway
231
+
232
+ The `KNXMQTTGateway` connects to an existing MQTT broker or sets up an embedded one using `aedes`.
233
+
234
+ ```typescript
235
+ import { KNXMQTTGateway } from "knx.ts/server";
236
+
237
+ const mqttGateway = new KNXMQTTGateway({
238
+ embeddedBroker: { port: 1883 }, // Or use brokerUrl: "mqtt://your-broker:1883"
239
+ knxContext: router,
240
+ topicPrefix: "knx", // Default is "knx"
241
+ });
242
+
243
+ await mqttGateway.start();
244
+ ```
245
+
246
+ **MQTT API Structure:**
247
+
248
+ - **State Updates**: Whenever the KNX bus emits data, the MQTT gateway publishes the decoded payload to:
249
+ `[prefix]/state/[groupAddress]` (e.g. `knx/state/1/2/3`) with retained flag and JSON: `{ "decodedValue": ... }`.
250
+
251
+ - **Commands (Write / Read / Config DPT)**: Send JSON payloads to the following command topics.
252
+ - Send Write Command: `[prefix]/command/write/[groupAddress]`
253
+ - Payload: `{ "value": 22.5, "dpt": "9.001" }` or just the raw value if DPT is globally cached.
254
+ - Send Read Command: `[prefix]/command/read/[groupAddress]`
255
+ - Configure DPT: `[prefix]/command/config_dpt/[groupAddress]`
256
+ - Payload: `{ "dpt": "9.001" }`
257
+
147
258
  ## 📝 Logging
148
259
 
149
260
  The library uses a single global logger based on [Pino](https://getpino.io/). You can configure it at the beginning of your application using `setupLogger`.
@@ -151,13 +262,13 @@ The library uses a single global logger based on [Pino](https://getpino.io/). Yo
151
262
  This is important because you do not need to instantiate Pino yourself; the internal `knxLogger` manages its state to avoid the performance overhead of multiple instances.
152
263
 
153
264
  ```typescript
154
- import { setupLogger, knxLogger } from 'knx.ts';
265
+ import { setupLogger, knxLogger } from "knx.ts";
155
266
 
156
267
  // Configure the global logger
157
268
  setupLogger({
158
- level: 'debug', // e.g. 'info', 'warn', 'error', 'debug'
269
+ level: "debug", // e.g. 'info', 'warn', 'error', 'debug'
159
270
  logToFile: true,
160
- logDir: './logs',
271
+ logDir: "./logs",
161
272
  });
162
273
 
163
274
  // You can also use the global logger in your own application
@@ -174,13 +285,15 @@ The library is event-driven. Depending on the class you use, different events ar
174
285
 
175
286
  All connection classes (`KNXnetIPServer`, `KNXTunneling`, `KNXUSBConnection`, `TPUARTConnection`) inherit from `KNXService` and emit the following standard events:
176
287
 
177
- | Event | Description | Callback Arguments |
178
- |--------|-------------|-------------------------|
179
- | `connected` | Connection established and hardware/socket ready. | `void` (Server/USB/TPUART) / `{ channelId }` (Tunnel) |
180
- | `disconnected` | Connection lost or explicitly closed. | `void` |
181
- | `error` | A fatal error occurred during operation. | `err: Error` |
182
- | `indication` | Any incoming standard KNX telegram (cEMI / L_Data.ind). | `cemi: ServiceMessage` |
183
- | `raw_indication`| The raw `Buffer` before parsing (EMI/cEMI/payload). | `data: Buffer` |
288
+ | Event | Description | Callback Arguments |
289
+ | ---------------- | ----------------------------------------------------------------------------------------- | ----------------------------------------------------- |
290
+ | `connected` | Connection established and hardware/socket ready. | `void` (Server/USB/TPUART) / `{ channelId }` (Tunnel) |
291
+ | `disconnected` | Connection lost or explicitly closed. | `void` |
292
+ | `error` | A fatal error occurred during operation. | `err: Error` |
293
+ | `indication` | Any incoming standard KNX telegram (cEMI / L_Data.ind). | `cemi: CEMIInstance` |
294
+ | `raw_indication` | The raw `Buffer` before parsing (EMI/cEMI/payload). | `data: Buffer` |
295
+ | `indication_emi` | Emitted when an older EMI1/EMI2-formatted message is received from legacy USB interfaces. | `emi: EMIInstance` |
296
+ | `send` | Emitted when a message is sent to the bus. | `data: CEMIInstance` |
184
297
 
185
298
  ### Class-Specific Events
186
299
 
@@ -210,18 +323,18 @@ Depending on the connection type, some classes emit additional specific events:
210
323
 
211
324
  Because `Router` links multiple interfaces, it emits link-specific routing events instead of standard indications:
212
325
 
213
- - `indication_link`: Emitted when a packet is routed through the bridge. Argument: `{ src: string, msg: ServiceMessage }`, where `src` is the class name of the source connection.
326
+ - `indication_link`: Emitted when a packet is routed through the bridge. Argument: `{ src: string, msg: CEMIInstance }`, where `src` is the class name of the source connection.
214
327
  - `error`: Emitted when an underlying link fails. Argument: `{ link: KNXService, error: Error }`.
215
328
 
216
329
  ### Understanding the Telegram Object
217
330
 
218
331
  The `cemi` or `emi` object (implementing `ServiceMessage`) contains all the information about the KNX telegram. Here are the most relevant properties:
219
332
 
220
- | Property | Type | Description |
221
- |-----------|------|-------------|
222
- | `sourceAddress` | `string` | Physical address of the sender (e.g., `"1.1.5"`). |
223
- | `destinationAddress` | `string` | Group address (e.g., `"1/1/1"`) or physical address. |
224
- | `TPDU.apdu.data` | `Buffer` | The raw payload data. |
333
+ | Property | Type | Description |
334
+ | ------------------------ | -------- | --------------------------------------------------------------- |
335
+ | `sourceAddress` | `string` | Physical address of the sender (e.g., `"1.1.5"`). |
336
+ | `destinationAddress` | `string` | Group address (e.g., `"1/1/1"`) or physical address. |
337
+ | `TPDU.apdu.data` | `Buffer` | The raw payload data. |
225
338
  | `TPDU.apdu.apci.command` | `string` | Command type (`A_GroupValue_Write`, `A_GroupValue_Read`, etc.). |
226
339
 
227
340
  #### Handling Data Payloads
@@ -240,23 +353,23 @@ The library provides static utilities to handle conversion of KNX Data Point Typ
240
353
  Use `KnxDataDecode` to transform raw cEMI data into readable values. There are several methods with the `asDpt` prefix for specific cases; `decodeThis` is convenient if you do not want to deal with those directly:
241
354
 
242
355
  ```typescript
243
- import { KnxDataDecode } from 'knx.ts';
356
+ import { KnxDataDecode } from "knx.ts";
244
357
 
245
- server.on('1/1/1', (cemi) => {
358
+ server.on("1/1/1", (cemi) => {
246
359
  // Decode as DPT 1 (Boolean)
247
360
  const value = KnxDataDecode.decodeThis(1, cemi.TPDU.apdu.data);
248
- console.log('Decoded value:', value); // true or false
361
+ console.log("Decoded value:", value); // true or false
249
362
 
250
363
  // Decode only as DPT 1 (Boolean)
251
364
  const value1 = KnxDataDecode.asDpt1(cemi.TPDU.apdu.data);
252
365
 
253
366
  // Decode as DPT 9 (2-byte float, e.g. Temperature)
254
367
  const temp = KnxDataDecode.decodeThis(9, cemi.TPDU.apdu.data);
255
- console.log('Temperature:', temp, '°C');
368
+ console.log("Temperature:", temp, "°C");
256
369
 
257
370
  // The first parameter also accepts strings with the standard DPT numbering
258
- const temp1 = KnxDataDecode.decodeThis("9", cemi.TPDU.apdu.data)
259
- const percentage = KnxDataDecode.decodeThis("5.001", cemi.TPDU.apdu.data)
371
+ const temp1 = KnxDataDecode.decodeThis("9", cemi.TPDU.apdu.data);
372
+ const percentage = KnxDataDecode.decodeThis("5.001", cemi.TPDU.apdu.data);
260
373
  });
261
374
  ```
262
375
 
@@ -265,7 +378,7 @@ server.on('1/1/1', (cemi) => {
265
378
  Use `KnxDataEncoder` with the `encodeThis` method to prepare buffers for KNX telegrams; the second parameter is always an object. There are several `encodeDpt`-prefixed methods for specific cases, but `encodeThis` is convenient if you do not want to deal with those directly:
266
379
 
267
380
  ```typescript
268
- import { KnxDataEncoder } from 'knx.ts';
381
+ import { KnxDataEncoder } from "knx.ts";
269
382
 
270
383
  // Encode a Boolean (DPT 1)
271
384
  const buf1 = KnxDataEncoder.encodeThis(1, { value: true });
@@ -305,7 +418,7 @@ In the actual API of this project, you typically build the **APDU** and **TPDU**
305
418
  Defines the command (**APCI**) and the message data.
306
419
 
307
420
  ```typescript
308
- import { APDU, APCI, APCIEnum } from 'knx.ts';
421
+ import { APDU, APCI, APCIEnum } from "knx.ts";
309
422
 
310
423
  const apci = new APCI(APCIEnum.A_GroupValue_Write_Protocol_Data_Unit);
311
424
  const apdu = new APDU(undefined, apci, Buffer.from([0x01]), true);
@@ -316,13 +429,9 @@ const apdu = new APDU(undefined, apci, Buffer.from([0x01]), true);
316
429
  Wraps the APDU and defines the transport type.
317
430
 
318
431
  ```typescript
319
- import { TPDU, TPCI, TPCIType } from 'knx.ts';
432
+ import { TPDU, TPCI, TPCIType } from "knx.ts";
320
433
 
321
- const tpdu = new TPDU(
322
- new TPCI(TPCIType.T_DATA_GROUP_PDU),
323
- apdu,
324
- apdu.data,
325
- );
434
+ const tpdu = new TPDU(new TPCI(TPCIType.T_DATA_GROUP_PDU), apdu, apdu.data);
326
435
  ```
327
436
 
328
437
  #### 3. cEMI `L_Data.req`
@@ -330,13 +439,7 @@ const tpdu = new TPDU(
330
439
  In this library you do not build a generic `new CEMI()` for this case. You must instantiate the concrete cEMI service and pass its control fields, addresses, and `TPDU`.
331
440
 
332
441
  ```typescript
333
- import {
334
- AddressType,
335
- CEMI,
336
- ControlField,
337
- ExtendedControlField,
338
- Priority,
339
- } from 'knx.ts';
442
+ import { AddressType, CEMI, ControlField, ExtendedControlField, Priority } from "knx.ts";
340
443
 
341
444
  const controlField1 = new ControlField();
342
445
  controlField1.frameType = true;
@@ -346,14 +449,7 @@ const controlField2 = new ExtendedControlField();
346
449
  controlField2.addressType = AddressType.GROUP;
347
450
  controlField2.hopCount = 6;
348
451
 
349
- const cemi = new CEMI.DataLinkLayerCEMI["L_Data.req"](
350
- null,
351
- controlField1,
352
- controlField2,
353
- "1.1.1",
354
- "1/1/1",
355
- tpdu,
356
- );
452
+ const cemi = new CEMI.DataLinkLayerCEMI["L_Data.req"](null, controlField1, controlField2, "1.1.1", "1/1/1", tpdu);
357
453
  ```
358
454
 
359
455
  #### 4. Additional Information (optional)
@@ -361,10 +457,7 @@ const cemi = new CEMI.DataLinkLayerCEMI["L_Data.req"](
361
457
  `AdditionalInformationField` is not filled by assigning `type` and `data` manually. You must create instances of the concrete types defined in `KNXAddInfoTypes` and add them to the field.
362
458
 
363
459
  ```typescript
364
- import {
365
- AdditionalInformationField,
366
- ManufacturerSpecificData,
367
- } from 'knx.ts';
460
+ import { AdditionalInformationField, ManufacturerSpecificData } from "knx.ts";
368
461
 
369
462
  const addInfo = new AdditionalInformationField();
370
463
  const manufacturerInfo = new ManufacturerSpecificData();
@@ -376,17 +469,17 @@ cemi.additionalInfo = addInfo;
376
469
 
377
470
  #### 5. EMI (External Message Interface)
378
471
 
379
- `EMI` is also not used as a generic instance with `new EMI()`. The class acts as a service container and parser (`EMI.fromBuffer(...)`). In connections such as `KNXUSBConnection`, the library automatically converts a cEMI `ServiceMessage` to EMI when needed.
472
+ `EMI` is also not used as a generic instance with `new EMI()`. The class acts as a service container and parser (`EMI.fromBuffer(...)`). In connections such as `KNXUSBConnection`, the library automatically converts a cEMI `CEMIInstance` to EMI when needed.
380
473
 
381
474
  ### Final Assembly and Sending Example
382
475
 
383
- Once the structure is built, you can send it directly as a `ServiceMessage`, or serialize it with `toBuffer()` if you really need the raw buffer.
476
+ Once the structure is built, you can send it directly as a `CEMIInstance`, or serialize it with `toBuffer()` if you really need the raw buffer.
384
477
 
385
478
  ```typescript
386
- import { KNXTunneling } from 'knx.ts';
479
+ import { KNXTunneling } from "knx.ts";
387
480
 
388
481
  const tunnel = new KNXTunneling({
389
- ip: '192.168.1.10',
482
+ ip: "192.168.1.10",
390
483
  port: 3671,
391
484
  });
392
485
 
@@ -1,8 +1,6 @@
1
- import { KNXnetIPServer } from "../../connection/KNXnetIPServer";
2
1
  import { KNXService } from "../../connection/KNXService";
3
- import { Router } from "../../connection/Router";
4
2
  export interface MQTTGatewayOptions {
5
- knxContext: Router | KNXnetIPServer;
3
+ knxContext: KNXService;
6
4
  embeddedBroker?: {
7
5
  port: number;
8
6
  host?: string;
@@ -16,3 +14,28 @@ export interface WebSocketGatewayOptions {
16
14
  port: number;
17
15
  knxContext: KNXService;
18
16
  }
17
+ export interface WSClientPayload {
18
+ action: "read" | "query" | "write" | "config_dpt" | "subscribe" | "unsubscribe";
19
+ groupAddress?: string;
20
+ dpt?: string | number;
21
+ value?: any;
22
+ onlyLatest?: boolean;
23
+ startDate?: string | Date;
24
+ endDate?: string | Date;
25
+ }
26
+ export interface WSServerPayload {
27
+ action: "connected" | "error" | "event" | "config_dpt_ack" | "write_ack" | "subscribe_ack" | "unsubscribe_ack" | "read_result" | "query_result";
28
+ message?: string;
29
+ groupAddress?: string;
30
+ dpt?: string | number;
31
+ decodedValue?: any;
32
+ data?: any;
33
+ results?: any[];
34
+ }
35
+ export interface MQTTCommandPayload {
36
+ value?: any;
37
+ dpt?: string | number;
38
+ }
39
+ export interface MQTTStatePayload {
40
+ decodedValue: any;
41
+ }
@@ -12,6 +12,8 @@ export declare abstract class KNXService<TOptions extends AllConnectionOptions =
12
12
  protected _transport: "UDP" | "TCP";
13
13
  protected logger: Logger;
14
14
  individualAddress: string;
15
+ isCacheDelegated: boolean;
16
+ isEventsDelegated: boolean;
15
17
  constructor(options?: TOptions);
16
18
  /**
17
19
  * Start the connection
@@ -19,6 +19,8 @@ class KNXService extends events_1.EventEmitter {
19
19
  _transport = "UDP";
20
20
  logger;
21
21
  individualAddress = "1.0.1";
22
+ isCacheDelegated = false;
23
+ isEventsDelegated = false;
22
24
  constructor(options = {}) {
23
25
  super();
24
26
  this.options = {
@@ -12,6 +12,7 @@ const KNXnetIPStructures_1 = require("../core/KNXnetIPStructures");
12
12
  const KNXnetIPEnum_1 = require("../core/enum/KNXnetIPEnum");
13
13
  const CEMI_1 = require("../core/CEMI");
14
14
  const KNXHelper_1 = require("../utils/KNXHelper");
15
+ const GroupAddressCache_1 = require("../core/cache/GroupAddressCache");
15
16
  /**
16
17
  * Handles KNXnet/IP Tunneling connections for point-to-point communication with a KNX gateway.
17
18
  * This class manages the connection state, sequence numbering for reliable delivery,
@@ -172,6 +173,31 @@ class KNXTunneling extends KNXService_1.KNXService {
172
173
  if (this.msgQueue.length >= this.MAX_QUEUE_SIZE) {
173
174
  throw new Error("Outgoing queue full");
174
175
  }
176
+ let cemiObj = undefined;
177
+ if (Buffer.isBuffer(cemi)) {
178
+ try {
179
+ cemiObj = CEMI_1.CEMI.fromBuffer(cemi);
180
+ }
181
+ catch {
182
+ /* empty */
183
+ }
184
+ }
185
+ else {
186
+ cemiObj = cemi;
187
+ }
188
+ if (cemiObj && "destinationAddress" in cemiObj && "sourceAddress" in cemiObj) {
189
+ if (!this.isCacheDelegated) {
190
+ try {
191
+ GroupAddressCache_1.GroupAddressCache.getInstance().processCEMI(cemiObj);
192
+ }
193
+ catch {
194
+ /* empty */
195
+ }
196
+ }
197
+ if (!this.isEventsDelegated && cemiObj.destinationAddress) {
198
+ this.emit(cemiObj.destinationAddress, cemiObj);
199
+ }
200
+ }
175
201
  this.emit("send", cemi);
176
202
  const cemiBuffer = Buffer.isBuffer(cemi) ? cemi : cemi.toBuffer();
177
203
  const isDeviceMgmt = this.options.connectionType === KNXnetIPEnum_1.ConnectionType.DEVICE_MGMT_CONNECTION;
@@ -397,13 +423,22 @@ class KNXTunneling extends KNXService_1.KNXService {
397
423
  const data = body.subarray(len);
398
424
  const cemi = CEMI_1.CEMI.fromBuffer(data);
399
425
  this.emit("indication", cemi);
426
+ if (!this.isCacheDelegated && "destinationAddress" in cemi && "sourceAddress" in cemi) {
427
+ try {
428
+ GroupAddressCache_1.GroupAddressCache.getInstance().processCEMI(cemi);
429
+ }
430
+ catch {
431
+ /* empty */
432
+ }
433
+ }
400
434
  if (!("destinationAddress" in cemi))
401
435
  return;
402
- this.emit(cemi.destinationAddress, cemi);
436
+ if (!this.isEventsDelegated) {
437
+ this.emit(cemi.destinationAddress, cemi);
438
+ }
403
439
  this.emit("raw_indication", data);
404
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
405
440
  }
406
- catch (e) {
441
+ catch {
407
442
  /* empty */
408
443
  }
409
444
  }
@@ -38,6 +38,7 @@ const hid = __importStar(require("node-hid"));
38
38
  const KNXService_1 = require("./KNXService");
39
39
  const CEMIAdapter_1 = require("../utils/CEMIAdapter");
40
40
  const CEMI_1 = require("../core/CEMI");
41
+ const GroupAddressCache_1 = require("../core/cache/GroupAddressCache");
41
42
  class KNXUSBConnection extends KNXService_1.KNXService {
42
43
  device = null;
43
44
  isConnected = false;
@@ -239,6 +240,31 @@ class KNXUSBConnection extends KNXService_1.KNXService {
239
240
  }
240
241
  }
241
242
  if (frame) {
243
+ let cemiObj = undefined;
244
+ try {
245
+ if (this.supportedEmiType === 0x03) {
246
+ cemiObj = Buffer.isBuffer(data) ? CEMI_1.CEMI.fromBuffer(data) : data;
247
+ }
248
+ else {
249
+ cemiObj = Buffer.isBuffer(data) ? CEMIAdapter_1.CEMIAdapter.emiToCemi(data) || undefined : undefined;
250
+ }
251
+ }
252
+ catch {
253
+ /* empty */
254
+ }
255
+ if (cemiObj && "destinationAddress" in cemiObj && "sourceAddress" in cemiObj) {
256
+ if (!this.isCacheDelegated) {
257
+ try {
258
+ GroupAddressCache_1.GroupAddressCache.getInstance().processCEMI(cemiObj);
259
+ }
260
+ catch {
261
+ /* empty */
262
+ }
263
+ }
264
+ if (!this.isEventsDelegated && cemiObj.destinationAddress) {
265
+ this.emit(cemiObj.destinationAddress, cemiObj);
266
+ }
267
+ }
242
268
  this.emit("send", frame);
243
269
  await this.sendUSBTransfer(0x01, this.supportedEmiType, frame);
244
270
  }
@@ -316,6 +342,17 @@ class KNXUSBConnection extends KNXService_1.KNXService {
316
342
  const cemiMsg = CEMI_1.CEMI.fromBuffer(payload);
317
343
  if (cemiMsg) {
318
344
  this.emit("indication", cemiMsg);
345
+ if (!this.isCacheDelegated && "destinationAddress" in cemiMsg && "sourceAddress" in cemiMsg) {
346
+ try {
347
+ GroupAddressCache_1.GroupAddressCache.getInstance().processCEMI(cemiMsg);
348
+ }
349
+ catch {
350
+ /* empty */
351
+ }
352
+ }
353
+ if (!this.isEventsDelegated && "destinationAddress" in cemiMsg) {
354
+ this.emit(cemiMsg.destinationAddress, cemiMsg);
355
+ }
319
356
  this.emit("raw_indication", payload);
320
357
  try {
321
358
  const emiMsg = CEMIAdapter_1.CEMIAdapter.cemiToEmi(cemiMsg);
@@ -338,6 +375,17 @@ class KNXUSBConnection extends KNXService_1.KNXService {
338
375
  const cemiMsg = CEMIAdapter_1.CEMIAdapter.emiToCemi(payload);
339
376
  if (cemiMsg) {
340
377
  this.emit("indication", cemiMsg);
378
+ if (!this.isCacheDelegated && "destinationAddress" in cemiMsg && "sourceAddress" in cemiMsg) {
379
+ try {
380
+ GroupAddressCache_1.GroupAddressCache.getInstance().processCEMI(cemiMsg);
381
+ }
382
+ catch {
383
+ /* empty */
384
+ }
385
+ }
386
+ if (!this.isEventsDelegated && "destinationAddress" in cemiMsg) {
387
+ this.emit(cemiMsg.destinationAddress, cemiMsg);
388
+ }
341
389
  this.emit("raw_indication", payload);
342
390
  }
343
391
  }
@@ -20,8 +20,6 @@ export declare class KNXnetIPServer extends KNXService<KNXnetIPServerOptions> {
20
20
  private decrementInterval;
21
21
  private serverIAInt;
22
22
  private _tunnelConnections;
23
- isCacheDelegated: boolean;
24
- isEventsDelegated: boolean;
25
23
  private readonly MAX_QUEUE_SIZE;
26
24
  private readonly BUSY_THRESHOLD;
27
25
  private readonly HEARTBEAT_TIMEOUT;
@@ -37,8 +37,6 @@ 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;
42
40
  MAX_QUEUE_SIZE = 100;
43
41
  BUSY_THRESHOLD = 15;
44
42
  HEARTBEAT_TIMEOUT = KNXnetIPEnum_1.KNXTimeoutConstants.CONNECTION_ALIVE_TIME * 1000;
@@ -51,8 +51,6 @@ class Router extends events_1.EventEmitter {
51
51
  if (options.knxNetIpServer) {
52
52
  options.knxNetIpServer.individualAddress = this.routerAddress;
53
53
  const ipServer = new KNXnetIPServer_1.KNXnetIPServer(options.knxNetIpServer);
54
- ipServer.isCacheDelegated = true;
55
- ipServer.isEventsDelegated = true;
56
54
  this.registerLink(`IP KNXnet/IP Server: ${ipServer.options.localIp}:${ipServer.options.port}`, ipServer);
57
55
  }
58
56
  if (options.tpuart) {
@@ -77,6 +75,8 @@ class Router extends events_1.EventEmitter {
77
75
  registerLink(key, link) {
78
76
  if (this.links.has(key))
79
77
  return;
78
+ link.isCacheDelegated = true;
79
+ link.isEventsDelegated = true;
80
80
  this.links.set(key, link);
81
81
  this.logger.info(`Link registered: ${key}`);
82
82
  link.on("indication", (cemi) => {
@@ -6,6 +6,7 @@ const KNXService_1 = require("./KNXService");
6
6
  const CEMIAdapter_1 = require("../utils/CEMIAdapter");
7
7
  const KNXHelper_1 = require("../utils/KNXHelper");
8
8
  const CEMI_1 = require("../core/CEMI");
9
+ const GroupAddressCache_1 = require("../core/cache/GroupAddressCache");
9
10
  const UART_SERVICES = {
10
11
  RESET_REQ: 0x01,
11
12
  RESET_IND: 0x03,
@@ -94,6 +95,17 @@ class TPUARTConnection extends KNXService_1.KNXService {
94
95
  const cemi = CEMIAdapter_1.CEMIAdapter.emiToCemi(emiBuffer);
95
96
  if (cemi) {
96
97
  this.emit("indication", cemi);
98
+ if (!this.isCacheDelegated && "destinationAddress" in cemi && "sourceAddress" in cemi) {
99
+ try {
100
+ GroupAddressCache_1.GroupAddressCache.getInstance().processCEMI(cemi);
101
+ }
102
+ catch {
103
+ /* empty */
104
+ }
105
+ }
106
+ if (!this.isEventsDelegated && "destinationAddress" in cemi) {
107
+ this.emit(cemi.destinationAddress, cemi);
108
+ }
97
109
  this.emit("raw_indication", cemi.toBuffer());
98
110
  }
99
111
  }
@@ -228,6 +240,31 @@ class TPUARTConnection extends KNXService_1.KNXService {
228
240
  async send(data) {
229
241
  if (this.connectionState < TPUARTState.ONLINE)
230
242
  throw new Error("TPUART offline");
243
+ let cemiObj = undefined;
244
+ if (Buffer.isBuffer(data)) {
245
+ try {
246
+ cemiObj = CEMI_1.CEMI.fromBuffer(data);
247
+ }
248
+ catch {
249
+ /* empty */
250
+ }
251
+ }
252
+ else {
253
+ cemiObj = data;
254
+ }
255
+ if (cemiObj && "destinationAddress" in cemiObj && "sourceAddress" in cemiObj) {
256
+ if (!this.isCacheDelegated) {
257
+ try {
258
+ GroupAddressCache_1.GroupAddressCache.getInstance().processCEMI(cemiObj);
259
+ }
260
+ catch {
261
+ /* empty */
262
+ }
263
+ }
264
+ if (!this.isEventsDelegated && cemiObj.destinationAddress) {
265
+ this.emit(cemiObj.destinationAddress, cemiObj);
266
+ }
267
+ }
231
268
  const frame = Buffer.isBuffer(data) ? data : CEMIAdapter_1.CEMIAdapter.cemiToEmi(data)?.toBuffer();
232
269
  if (!frame)
233
270
  throw new Error("Invalid data");
package/dist/index.d.ts CHANGED
@@ -22,3 +22,8 @@ export * from "./core/layers/data/NPDU";
22
22
  export * from "./core/layers/data/TPDU";
23
23
  export * from "./core/layers/interfaces/APCI";
24
24
  export * from "./core/layers/interfaces/TPCI";
25
+ export * from "./core/cache/GroupAddressCache";
26
+ export * from "./@types/interfaces/servers";
27
+ export type { CEMIInstance } from "./core/CEMI";
28
+ export type { EMIInstance } from "./core/EMI";
29
+ export type { ServiceMessage } from "./@types/interfaces/ServiceMessage";
package/dist/index.js CHANGED
@@ -51,3 +51,5 @@ __exportStar(require("./core/layers/data/NPDU"), exports);
51
51
  __exportStar(require("./core/layers/data/TPDU"), exports);
52
52
  __exportStar(require("./core/layers/interfaces/APCI"), exports);
53
53
  __exportStar(require("./core/layers/interfaces/TPCI"), exports);
54
+ __exportStar(require("./core/cache/GroupAddressCache"), exports);
55
+ __exportStar(require("./@types/interfaces/servers"), exports);
@@ -47,6 +47,8 @@ class KNXMQTTGateway {
47
47
  constructor(options) {
48
48
  this.options = options;
49
49
  this.topicPrefix = options.topicPrefix || "knx";
50
+ // Enable the cache singleton
51
+ GroupAddressCache_1.GroupAddressCache.getInstance().setEnabled(true);
50
52
  }
51
53
  async start() {
52
54
  // 1. Setup embedded broker if requested
@@ -124,6 +126,8 @@ class KNXMQTTGateway {
124
126
  // simple value fallback
125
127
  payload = { value: message.toString() };
126
128
  }
129
+ if (!("value" in payload) || !("dpt" in payload))
130
+ return;
127
131
  const { value, dpt } = payload;
128
132
  if (action === "config_dpt" && dpt) {
129
133
  GroupAddressCache_1.GroupAddressCache.getInstance().setAddressDPT(groupAddress, dpt);
@@ -10,6 +10,8 @@ class KNXWebSocketGateway {
10
10
  activeSubscriptions = new Set();
11
11
  constructor(options) {
12
12
  this.options = options;
13
+ // Enable the cache singleton
14
+ GroupAddressCache_1.GroupAddressCache.getInstance().setEnabled(true);
13
15
  }
14
16
  start() {
15
17
  this.wss = new ws_1.WebSocketServer({ port: this.options.port });
@@ -46,6 +48,8 @@ class KNXWebSocketGateway {
46
48
  });
47
49
  }
48
50
  handleClientMessage(ws, payload) {
51
+ if (!("action" in payload) || !("groupAddress" in payload) || !("value" in payload) || !("dpt" in payload))
52
+ return;
49
53
  const { action, groupAddress, value, dpt } = payload;
50
54
  if (action === "config_dpt" && groupAddress && dpt) {
51
55
  GroupAddressCache_1.GroupAddressCache.getInstance().setAddressDPT(groupAddress, dpt);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "knx.ts",
3
- "version": "1.0.7",
3
+ "version": "1.0.8",
4
4
  "description": "A high-performance KNXnet/IP server and client library in TypeScript, focused on ETS stability.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",