dsc-itv2-client 1.0.22 → 1.0.24

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 (2) hide show
  1. package/package.json +1 -1
  2. package/src/ITV2Client.js +238 -16
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsc-itv2-client",
3
3
  "author": "fajitacat",
4
- "version": "1.0.22",
4
+ "version": "1.0.24",
5
5
  "description": "Reverse engineered DSC ITV2 Protocol Client Library for TL280R Communicator - Monitor and control DSC alarm panels",
6
6
  "main": "src/index.js",
7
7
  "type": "module",
package/src/ITV2Client.js CHANGED
@@ -5,6 +5,7 @@
5
5
 
6
6
  import {EventEmitter} from 'events';
7
7
  import dgram from 'dgram';
8
+ import net from 'net';
8
9
  import {ITv2Session, CMD, CMD_NAMES} from './itv2-session.js';
9
10
  import {parseType1Initializer, type2InitializerTransform} from './itv2-crypto.js';
10
11
  import {EventHandler} from './event-handler.js';
@@ -22,18 +23,23 @@ export class ITV2Client extends EventEmitter {
22
23
  this.integrationId = options.integrationId;
23
24
  this.accessCode = options.accessCode;
24
25
  this.masterCode = options.masterCode || '5555';
25
- this.port = options.port || 3073;
26
+ this.port = options.port || 3073; // UDP polling port
27
+ this.tcpPort = options.tcpPort || 3072; // TCP notification port
26
28
  this.logLevel = options.logLevel || (options.debug === true ? 'verbose' : 'minimal'); // 'silent', 'minimal', 'verbose'
27
29
  this.encryptionType = options.encryptionType || null; // null = auto-detect, 1 = Type 1, 2 = Type 2
28
30
 
29
31
  // Internal state
30
- this.server = null;
32
+ this.server = null; // UDP server
33
+ this.tcpServer = null; // TCP server
34
+ this.tcpClients = new Map(); // Active TCP connections
31
35
  this.session = null;
32
36
  this.eventHandler = new EventHandler();
33
37
  this.handshakeState = 'WAITING';
34
38
  this.panelAddress = null;
35
39
  this.panelPort = null;
36
40
  this.detectedEncryptionType = null; // Set during handshake
41
+ this._lastTransport = 'udp'; // Track last packet source
42
+ this._lastTcpSocket = null; // Track TCP socket for responses
37
43
 
38
44
  // Bind methods
39
45
  this._handlePacket = this._handlePacket.bind(this);
@@ -69,6 +75,9 @@ export class ITV2Client extends EventEmitter {
69
75
 
70
76
  this.server.bind(this.port, '0.0.0.0');
71
77
 
78
+ // Start TCP server for notification port
79
+ this._startTcpServer();
80
+
72
81
  // Register signal handlers (only once)
73
82
  if (!this._boundShutdown) {
74
83
  this._boundShutdown = async () => {
@@ -81,6 +90,97 @@ export class ITV2Client extends EventEmitter {
81
90
  });
82
91
  }
83
92
 
93
+ /**
94
+ * Start TCP server for notification port (3072)
95
+ */
96
+ _startTcpServer() {
97
+ this.tcpServer = net.createServer((socket) => {
98
+ const clientId = `${socket.remoteAddress}:${socket.remotePort}`;
99
+ this._logMinimal(`[TCP] Client connected: ${clientId}`);
100
+ this.tcpClients.set(clientId, socket);
101
+
102
+ // Store panel address from TCP connection too
103
+ if (!this.panelAddress) {
104
+ this.panelAddress = socket.remoteAddress;
105
+ }
106
+
107
+ let buffer = Buffer.alloc(0);
108
+
109
+ socket.on('data', (data) => {
110
+ this._logMinimal(`[TCP] Received ${data.length} bytes from ${clientId}`);
111
+ this._logMinimal(`[TCP] Raw data: ${data.toString('hex')}`);
112
+
113
+ // Accumulate data in buffer
114
+ buffer = Buffer.concat([buffer, data]);
115
+
116
+ // Try to parse complete packets (look for 0x7E...0x7F framing)
117
+ this._processTcpBuffer(buffer, socket, (remaining) => {
118
+ buffer = remaining;
119
+ });
120
+ });
121
+
122
+ socket.on('close', () => {
123
+ this._logMinimal(`[TCP] Client disconnected: ${clientId}`);
124
+ this.tcpClients.delete(clientId);
125
+ });
126
+
127
+ socket.on('error', (err) => {
128
+ this._log(`[TCP] Socket error for ${clientId}: ${err.message}`);
129
+ this.tcpClients.delete(clientId);
130
+ });
131
+ });
132
+
133
+ this.tcpServer.on('error', (err) => {
134
+ this._log(`[TCP] Server error: ${err.message}`);
135
+ this.emit('error', err);
136
+ });
137
+
138
+ this.tcpServer.listen(this.tcpPort, '0.0.0.0', () => {
139
+ this._logMinimal(`[TCP] Server listening on port ${this.tcpPort}`);
140
+ this.emit('tcp:listening', { port: this.tcpPort });
141
+ });
142
+ }
143
+
144
+ /**
145
+ * Process TCP buffer looking for complete ITV2 packets
146
+ */
147
+ _processTcpBuffer(buffer, socket, callback) {
148
+ // Look for Integration ID prefix followed by 0x7E
149
+ const idLength = 12; // Integration ID is 12 ASCII chars
150
+
151
+ while (buffer.length > idLength + 2) {
152
+ // Find start marker (0x7E) after integration ID
153
+ const startIdx = buffer.indexOf(0x7E, idLength - 1);
154
+ if (startIdx === -1) {
155
+ // No start marker found, keep last 12 bytes in case ID is split
156
+ callback(buffer.slice(Math.max(0, buffer.length - idLength)));
157
+ return;
158
+ }
159
+
160
+ // Find end marker (0x7F)
161
+ const endIdx = buffer.indexOf(0x7F, startIdx + 1);
162
+ if (endIdx === -1) {
163
+ // No end marker yet, wait for more data
164
+ callback(buffer);
165
+ return;
166
+ }
167
+
168
+ // Extract complete packet (including ID prefix)
169
+ const packetStart = Math.max(0, startIdx - idLength);
170
+ const packet = buffer.slice(packetStart, endIdx + 1);
171
+
172
+ this._logMinimal(`[TCP] Complete packet (${packet.length} bytes): ${packet.toString('hex')}`);
173
+
174
+ // Process the packet same as UDP
175
+ this._handlePacket(packet, 'tcp', socket);
176
+
177
+ // Continue with remaining buffer
178
+ buffer = buffer.slice(endIdx + 1);
179
+ }
180
+
181
+ callback(buffer);
182
+ }
183
+
84
184
  /**
85
185
  * Stop the client and close the session gracefully
86
186
  */
@@ -105,6 +205,23 @@ export class ITV2Client extends EventEmitter {
105
205
  this.server = null;
106
206
  }
107
207
 
208
+ // Close TCP server
209
+ if (this.tcpServer) {
210
+ // Close all TCP client connections
211
+ for (const [clientId, socket] of this.tcpClients) {
212
+ socket.destroy();
213
+ }
214
+ this.tcpClients.clear();
215
+
216
+ await new Promise((resolve) => {
217
+ this.tcpServer.close(() => {
218
+ this._log('[Shutdown] TCP server closed');
219
+ resolve();
220
+ });
221
+ });
222
+ this.tcpServer = null;
223
+ }
224
+
108
225
  // Clear session and encryption state to prevent stale keys on reconnect
109
226
  if (this.session) {
110
227
  this.session.disableAes();
@@ -277,19 +394,29 @@ export class ITV2Client extends EventEmitter {
277
394
  }
278
395
 
279
396
  _sendPacket(packet) {
280
- if (!this.panelAddress || !this.panelPort) {
281
- this._log('[Error] No panel address/port set');
282
- return;
283
- }
397
+ // Use the same transport that the last packet came from
398
+ if (this._lastTransport === 'tcp' && this._lastTcpSocket) {
399
+ this._log(`\n[TCP] TX (${packet.length} bytes)`);
400
+ if (this.debug) {
401
+ this._hexDump(packet);
402
+ }
403
+ this._lastTcpSocket.write(packet);
404
+ } else {
405
+ // UDP (default)
406
+ if (!this.panelAddress || !this.panelPort) {
407
+ this._log('[Error] No panel address/port set');
408
+ return;
409
+ }
284
410
 
285
- this._log(`\n[UDP] TX to ${this.panelAddress}:${this.panelPort} (${packet.length} bytes)`);
286
- if (this.debug) {
287
- this._hexDump(packet);
411
+ this._log(`\n[UDP] TX to ${this.panelAddress}:${this.panelPort} (${packet.length} bytes)`);
412
+ if (this.debug) {
413
+ this._hexDump(packet);
414
+ }
415
+ this.server.send(packet, this.panelPort, this.panelAddress);
288
416
  }
289
- this.server.send(packet, this.panelPort, this.panelAddress);
290
417
  }
291
418
 
292
- _handlePacket(data) {
419
+ _handlePacket(data, transport = 'udp', tcpSocket = null) {
293
420
  try {
294
421
  if (!this.session) {
295
422
  // Create session with appropriate logger based on log level
@@ -304,8 +431,13 @@ export class ITV2Client extends EventEmitter {
304
431
  return;
305
432
  }
306
433
 
307
- // Verbose logging only
308
- this._log(`\n[UDP] RX from ${this.panelAddress}:${this.panelPort} (${data.length} bytes)`);
434
+ // Store transport info for responses
435
+ this._lastTransport = transport;
436
+ this._lastTcpSocket = tcpSocket;
437
+
438
+ // Verbose logging
439
+ const source = transport === 'tcp' ? 'TCP' : `UDP ${this.panelAddress}:${this.panelPort}`;
440
+ this._log(`\n[${transport.toUpperCase()}] RX from ${source} (${data.length} bytes)`);
309
441
  this._hexDump(data);
310
442
  this._log(`[Packet] Integration ID: ${parsed.integrationId}`);
311
443
  this._log(`[Packet] Sender Seq: ${parsed.senderSequence}, Receiver Seq: ${parsed.receiverSequence}`);
@@ -379,9 +511,23 @@ export class ITV2Client extends EventEmitter {
379
511
  case CMD.TIME_DATE_BROADCAST:
380
512
  this._handleTimeDateBroadcast(parsed);
381
513
  break;
514
+ case CMD.ZONE_STATUS:
515
+ this._handleZoneStatusResponse(parsed);
516
+ break;
517
+ case CMD.PARTITION_STATUS:
518
+ this._handlePartitionStatusResponse(parsed);
519
+ break;
520
+ case CMD.GLOBAL_STATUS:
521
+ this._handleGlobalStatusResponse(parsed);
522
+ break;
382
523
  default:
383
524
  if (this.handshakeState === 'ESTABLISHED') {
384
- this._log(`[Session] Unhandled command ${CMD_NAMES[cmd] || '0x' + cmd?.toString(16)}`);
525
+ const cmdHex = cmd ? '0x' + cmd.toString(16).padStart(4, '0') : 'null';
526
+ const cmdName = CMD_NAMES[cmd] || 'UNKNOWN';
527
+ this._logMinimal(`[Session] Received command ${cmdName} (${cmdHex})`);
528
+ if (parsed.commandData && parsed.commandData.length > 0) {
529
+ this._logMinimal(`[Session] Data (${parsed.commandData.length} bytes): ${parsed.commandData.toString('hex')}`);
530
+ }
385
531
  this._sendPacket(this.session.buildSimpleAck());
386
532
  }
387
533
  break;
@@ -569,10 +715,17 @@ export class ITV2Client extends EventEmitter {
569
715
  }
570
716
 
571
717
  _handleCommandResponse(parsed) {
572
- const responseCode = parsed.commandData?.[0] || 0;
718
+ const data = parsed.commandData;
719
+ const responseCode = data?.[0] || 0;
573
720
  const appSeqAsEcho = parsed.appSequence;
574
721
 
575
- this._log(`[Handshake] Got COMMAND_RESPONSE (echoed app_seq: ${appSeqAsEcho}, response_code: ${responseCode}), sending ACK`);
722
+ this._log(`[Session] Got COMMAND_RESPONSE (app_seq: ${appSeqAsEcho}, response_code: ${responseCode})`);
723
+
724
+ // Log full data in established state for debugging query responses
725
+ if (this.handshakeState === 'ESTABLISHED' && data && data.length > 1) {
726
+ this._logMinimal(`[Command Response] Response code: ${responseCode}, data (${data.length} bytes): ${data.toString('hex')}`);
727
+ this.emit('command:response', { responseCode, appSequence: appSeqAsEcho, data });
728
+ }
576
729
 
577
730
  const ack = this.session.buildSimpleAck();
578
731
  this._sendPacket(ack);
@@ -655,6 +808,75 @@ export class ITV2Client extends EventEmitter {
655
808
  this._sendPacket(this.session.buildSimpleAck());
656
809
  }
657
810
 
811
+ // ==================== Status Query Response Handlers ====================
812
+
813
+ _handleZoneStatusResponse(parsed) {
814
+ const data = parsed.commandData;
815
+ this._logMinimal(`[Zone Status] Received zone status response`);
816
+ this._log(`[Zone Status] Raw data (${data?.length || 0} bytes): ${data?.toString('hex') || 'none'}`);
817
+
818
+ if (data && data.length >= 3) {
819
+ // Response format: [ZoneNumber 2B BE][StatusByte 1B][...more zones...]
820
+ const zoneNum = data.readUInt16BE(0);
821
+ const statusByte = data[2];
822
+
823
+ // Update internal state
824
+ const fullStatus = this.eventHandler.handleZoneStatus(zoneNum, statusByte);
825
+
826
+ this._logMinimal(`[Zone Status] Zone ${zoneNum}: ${fullStatus.open ? 'OPEN' : 'CLOSED'} (0x${statusByte.toString(16)})`);
827
+
828
+ // Emit event with full status
829
+ this.emit('zone:statusResponse', zoneNum, fullStatus);
830
+ this.emit('zone:status', zoneNum, fullStatus);
831
+ } else {
832
+ this._log(`[Zone Status] Empty or invalid response`);
833
+ }
834
+
835
+ this._sendPacket(this.session.buildSimpleAck());
836
+ }
837
+
838
+ _handlePartitionStatusResponse(parsed) {
839
+ const data = parsed.commandData;
840
+ this._logMinimal(`[Partition Status] Received partition status response`);
841
+ this._log(`[Partition Status] Raw data (${data?.length || 0} bytes): ${data?.toString('hex') || 'none'}`);
842
+
843
+ if (data && data.length >= 3) {
844
+ // Response format: [PartitionNumber 2B BE][StatusBytes...]
845
+ const partitionNum = data.readUInt16BE(0);
846
+ const statusBytes = data.slice(2);
847
+
848
+ this._logMinimal(`[Partition Status] Partition ${partitionNum}: status bytes ${statusBytes.toString('hex')}`);
849
+
850
+ // Emit raw event - let consumers parse the detailed status
851
+ this.emit('partition:statusResponse', partitionNum, statusBytes);
852
+ } else {
853
+ this._log(`[Partition Status] Empty or invalid response`);
854
+ }
855
+
856
+ this._sendPacket(this.session.buildSimpleAck());
857
+ }
858
+
859
+ _handleGlobalStatusResponse(parsed) {
860
+ const data = parsed.commandData;
861
+ this._logMinimal(`[Global Status] Received global status response`);
862
+ this._log(`[Global Status] Raw data (${data?.length || 0} bytes): ${data?.toString('hex') || 'none'}`);
863
+
864
+ if (data && data.length > 0) {
865
+ // Emit raw event with full data
866
+ this.emit('global:statusResponse', data);
867
+
868
+ // Log byte breakdown for debugging
869
+ this._log(`[Global Status] Byte breakdown:`);
870
+ for (let i = 0; i < data.length && i < 32; i++) {
871
+ this._log(` Byte ${i}: 0x${data[i].toString(16).padStart(2, '0')} (${data[i]})`);
872
+ }
873
+ } else {
874
+ this._log(`[Global Status] Empty response`);
875
+ }
876
+
877
+ this._sendPacket(this.session.buildSimpleAck());
878
+ }
879
+
658
880
  // ==================== Utility Methods ====================
659
881
 
660
882
  _handleError(error) {