dsc-itv2-client 1.0.30 → 2.0.0
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 +143 -221
- package/package.json +4 -3
- package/src/ITV2Client.js +1408 -985
- package/src/constants.js +1 -1
- package/src/event-handler.js +8 -5
- package/src/examples/arm-disarm-example.js +1 -1
- package/src/examples/basic-monitoring.js +16 -6
- package/src/examples/interactive-cli.js +267 -1033
- package/src/itv2-session.js +133 -75
- package/src/response-parsers.js +34 -25
|
@@ -1,1033 +1,267 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* DSC
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import
|
|
9
|
-
import readline from 'readline';
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
console.log(
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
log(`[Time/Date] Received time/date broadcast: ${data.toString('hex')}`);
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
sendPacket(session.buildSimpleAck());
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
function handlePartitionArmingNotification(parsed) {
|
|
275
|
-
const data = parsed.commandData;
|
|
276
|
-
log(`\n[Partition Arming] Received partition arming notification`);
|
|
277
|
-
|
|
278
|
-
if (data && data.length >= 3) {
|
|
279
|
-
const partitionNum = data.readUInt16BE(0);
|
|
280
|
-
const armedState = data[2];
|
|
281
|
-
|
|
282
|
-
const status = eventHandler.handlePartitionArming(partitionNum, armedState);
|
|
283
|
-
|
|
284
|
-
console.log(`\n========================================`);
|
|
285
|
-
console.log(`PARTITION ARMING NOTIFICATION`);
|
|
286
|
-
console.log(`Partition: ${partitionNum}`);
|
|
287
|
-
console.log(`State: ${status.armedStateName}`);
|
|
288
|
-
console.log(`Armed: ${status.isArmed ? 'YES' : 'NO'}`);
|
|
289
|
-
console.log(`In Delay: ${status.inDelay ? 'YES' : 'NO'}`);
|
|
290
|
-
console.log(`========================================\n`);
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
sendPacket(session.buildSimpleAck());
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
function handlePartitionReadyNotification(parsed) {
|
|
297
|
-
const data = parsed.commandData;
|
|
298
|
-
log(`\n[Partition Ready] Received partition ready notification`);
|
|
299
|
-
|
|
300
|
-
if (data && data.length >= 3) {
|
|
301
|
-
const partitionNum = data.readUInt16BE(0);
|
|
302
|
-
const isReady = data[2] !== 0;
|
|
303
|
-
|
|
304
|
-
eventHandler.handlePartitionReady(partitionNum, isReady);
|
|
305
|
-
|
|
306
|
-
console.log(`\n========================================`);
|
|
307
|
-
console.log(`PARTITION READY NOTIFICATION`);
|
|
308
|
-
console.log(`Partition: ${partitionNum}`);
|
|
309
|
-
console.log(`Ready: ${isReady ? 'YES' : 'NO'}`);
|
|
310
|
-
console.log(`========================================\n`);
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
sendPacket(session.buildSimpleAck());
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
function handlePartitionTroubleNotification(parsed) {
|
|
317
|
-
const data = parsed.commandData;
|
|
318
|
-
log(`\n[Partition Trouble] Received partition trouble notification`);
|
|
319
|
-
|
|
320
|
-
if (data && data.length >= 3) {
|
|
321
|
-
const partitionNum = data.readUInt16BE(0);
|
|
322
|
-
const troubleFlags = data[2];
|
|
323
|
-
|
|
324
|
-
eventHandler.handlePartitionTrouble(partitionNum, troubleFlags);
|
|
325
|
-
|
|
326
|
-
console.log(`\n========================================`);
|
|
327
|
-
console.log(`PARTITION TROUBLE NOTIFICATION`);
|
|
328
|
-
console.log(`Partition: ${partitionNum}`);
|
|
329
|
-
console.log(`Trouble Flags: 0x${troubleFlags.toString(16).padStart(2, '0')}`);
|
|
330
|
-
console.log(`========================================\n`);
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
sendPacket(session.buildSimpleAck());
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
function handleOpenSession(parsed) {
|
|
337
|
-
// OPEN_SESSION does NOT have an app sequence - the "appSequence" field is actually the device type
|
|
338
|
-
// Re-parse: [sender][receiver][cmd 2B][device_type][payload...]
|
|
339
|
-
// parsed.appSequence is actually the device type (first payload byte)
|
|
340
|
-
// parsed.commandData starts at the second payload byte
|
|
341
|
-
|
|
342
|
-
const deviceType = parsed.appSequence; // This is actually the device type, not app seq
|
|
343
|
-
const data = parsed.commandData;
|
|
344
|
-
|
|
345
|
-
if (deviceType !== undefined && data && data.length >= 13) {
|
|
346
|
-
// Reconstruct full payload: [deviceType] + [rest of data]
|
|
347
|
-
const fullPayload = Buffer.concat([Buffer.from([deviceType]), data]);
|
|
348
|
-
|
|
349
|
-
const deviceId = fullPayload.slice(1, 3).toString('hex');
|
|
350
|
-
const firmware = fullPayload.slice(3, 5).toString('hex');
|
|
351
|
-
const protocol = fullPayload.slice(5, 7).toString('hex');
|
|
352
|
-
const txBuffer = fullPayload.readUInt16BE(7);
|
|
353
|
-
const rxBuffer = fullPayload.readUInt16BE(9);
|
|
354
|
-
const encType = fullPayload[13];
|
|
355
|
-
|
|
356
|
-
log(`[Session] OPEN_SESSION received:`);
|
|
357
|
-
log(` Device Type: ${deviceType}`);
|
|
358
|
-
log(` Device ID: ${deviceId}`);
|
|
359
|
-
log(` Firmware: ${firmware}`);
|
|
360
|
-
log(` Protocol: ${protocol}`);
|
|
361
|
-
log(` TX Buffer: ${txBuffer}`);
|
|
362
|
-
log(` RX Buffer: ${rxBuffer}`);
|
|
363
|
-
log(` Encryption Type: ${encType}`);
|
|
364
|
-
|
|
365
|
-
// Reset session if panel is starting a new handshake
|
|
366
|
-
if (handshakeState !== 'WAITING' && handshakeState !== 'SENT_CMD_RESPONSE_1') {
|
|
367
|
-
log(`[Session] Panel restarting handshake (was in state ${handshakeState}), resetting session`);
|
|
368
|
-
session = new ITv2Session(INTEGRATION_ID, ACCESS_CODE, log);
|
|
369
|
-
testCommandSent = false;
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
session.remoteOpenSession = fullPayload;
|
|
373
|
-
log(`[Handshake] Panel advertises encryption Type ${encType}`);
|
|
374
|
-
|
|
375
|
-
// Respond with COMMAND_RESPONSE success
|
|
376
|
-
// SDK shows: COMMAND_RESPONSE echoes the first byte (device type) from OPEN_SESSION
|
|
377
|
-
log(`[Handshake] Sending COMMAND_RESPONSE success (echoing device type ${deviceType})`);
|
|
378
|
-
const response = session.buildCommandResponseWithAppSeq(deviceType, 0x00);
|
|
379
|
-
sendPacket(response);
|
|
380
|
-
handshakeState = 'SENT_CMD_RESPONSE_1';
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
function handleRequestAccess(parsed) {
|
|
385
|
-
const data = parsed.commandData;
|
|
386
|
-
log(`[Handshake] Got panel's REQUEST_ACCESS`);
|
|
387
|
-
|
|
388
|
-
// If already established, just ACK and ignore the retry
|
|
389
|
-
if (handshakeState === 'ESTABLISHED') {
|
|
390
|
-
log(`[Handshake] Session already established, ACKing panel's REQUEST_ACCESS retry`);
|
|
391
|
-
sendPacket(session.buildSimpleAck());
|
|
392
|
-
return;
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
if (data && data.length > 0) {
|
|
396
|
-
const initializerLength = data[0];
|
|
397
|
-
const initializer = data.slice(1);
|
|
398
|
-
|
|
399
|
-
log(`[Handshake] Processing panel's REQUEST_ACCESS (panel goes first)`);
|
|
400
|
-
log(`[Handshake] Panel initializer (${initializer.length} bytes): ${initializer.toString('hex')}`);
|
|
401
|
-
|
|
402
|
-
if (initializer.length === 48) {
|
|
403
|
-
// Type 1 encryption
|
|
404
|
-
detectedEncryptionType = 1;
|
|
405
|
-
log(`[Handshake] Encryption Type 1 (based on initializer length)`);
|
|
406
|
-
log(`[Handshake] Type 1: Parsing 48-byte initializer from panel`);
|
|
407
|
-
log(`[Handshake] Using access code: ${ACCESS_CODE}, integration ID: ${INTEGRATION_ID}`);
|
|
408
|
-
|
|
409
|
-
try {
|
|
410
|
-
const sendKey = parseType1Initializer(ACCESS_CODE, initializer, INTEGRATION_ID);
|
|
411
|
-
log(`[Handshake] Type 1 SEND key derived: ${sendKey.toString('hex')}`);
|
|
412
|
-
|
|
413
|
-
// Store the SEND key for later (after handshake completes)
|
|
414
|
-
// Based on SDK trace: ALL handshake frames are PLAINTEXT
|
|
415
|
-
session.derivedSendKey = sendKey;
|
|
416
|
-
log(`[Handshake] SEND key stored (will enable after handshake): ${sendKey.toString('hex')}`);
|
|
417
|
-
|
|
418
|
-
// Send PLAINTEXT COMMAND_RESPONSE (echoing the app sequence from REQUEST_ACCESS)
|
|
419
|
-
// SDK trace shows: 08 03 02 05 02 66 00 9B A9 (plaintext)
|
|
420
|
-
log(`[Handshake] Sending PLAINTEXT COMMAND_RESPONSE to panel's REQUEST_ACCESS`);
|
|
421
|
-
const appSeq = parsed.appSequence; // Echo the app sequence from the request
|
|
422
|
-
const response = session.buildCommandResponseWithAppSeq(appSeq, 0x00);
|
|
423
|
-
sendPacket(response);
|
|
424
|
-
|
|
425
|
-
// Wait for panel to ACK our COMMAND_RESPONSE before sending our REQUEST_ACCESS
|
|
426
|
-
// Sending back-to-back was causing the panel to reset
|
|
427
|
-
handshakeState = 'WAITING_TO_SEND_REQUEST_ACCESS';
|
|
428
|
-
log(`[Handshake] Waiting for ACK before sending our REQUEST_ACCESS`);
|
|
429
|
-
} catch (err) {
|
|
430
|
-
log(`[Handshake] Error parsing Type 1 initializer: ${err.message}`);
|
|
431
|
-
}
|
|
432
|
-
} else if (initializer.length === 16) {
|
|
433
|
-
// Type 2 encryption
|
|
434
|
-
detectedEncryptionType = 2;
|
|
435
|
-
log(`[Handshake] Encryption Type 2 (based on initializer length)`);
|
|
436
|
-
log(`[Handshake] Type 2: Parsing 16-byte initializer from panel`);
|
|
437
|
-
|
|
438
|
-
try {
|
|
439
|
-
// Expand access code to 32 hex if it's 8 digits
|
|
440
|
-
let accessCode32 = ACCESS_CODE;
|
|
441
|
-
if (accessCode32.length === 8) {
|
|
442
|
-
accessCode32 = accessCode32 + accessCode32 + accessCode32 + accessCode32;
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
const sendKey = type2InitializerTransform(accessCode32, initializer);
|
|
446
|
-
log(`[Handshake] Type 2 SEND key derived: ${sendKey.toString('hex')}`);
|
|
447
|
-
|
|
448
|
-
session.derivedSendKey = sendKey;
|
|
449
|
-
log(`[Handshake] SEND key stored (will enable after handshake): ${sendKey.toString('hex')}`);
|
|
450
|
-
|
|
451
|
-
// Send PLAINTEXT COMMAND_RESPONSE
|
|
452
|
-
log(`[Handshake] Sending PLAINTEXT COMMAND_RESPONSE to panel's REQUEST_ACCESS`);
|
|
453
|
-
const appSeq = parsed.appSequence;
|
|
454
|
-
const response = session.buildCommandResponseWithAppSeq(appSeq, 0x00);
|
|
455
|
-
sendPacket(response);
|
|
456
|
-
|
|
457
|
-
handshakeState = 'WAITING_TO_SEND_REQUEST_ACCESS';
|
|
458
|
-
log(`[Handshake] Waiting for ACK before sending our REQUEST_ACCESS`);
|
|
459
|
-
} catch (err) {
|
|
460
|
-
log(`[Handshake] Error parsing Type 2 initializer: ${err.message}`);
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
function handleSimpleAck(parsed) {
|
|
467
|
-
const ackSeq = parsed.receiverSequence;
|
|
468
|
-
log(`[Handshake] Got ACK ${ackSeq}, state=${handshakeState}`);
|
|
469
|
-
|
|
470
|
-
if (handshakeState === 'SENT_CMD_RESPONSE_1') {
|
|
471
|
-
// Send our OPEN_SESSION (PLAINTEXT)
|
|
472
|
-
log(`[Handshake] Got ACK 1, sending our OPEN_SESSION`);
|
|
473
|
-
// SDK trace shows OPEN_SESSION payload does NOT have AppSeq prefix for server->panel direction
|
|
474
|
-
// SDK packet: 15 02 00 06 0A 01 80 00 00 02 01 02 41 02 00 02 00 00 01 01 A7 4B
|
|
475
|
-
// Payload starts with DeviceType=0x01, NOT ApplicationSequenceNumber
|
|
476
|
-
const openSessionPayload = Buffer.from([
|
|
477
|
-
0x01, // Device Type (0x01 = server, vs 0x02 for panel)
|
|
478
|
-
0x80, 0x00, // Device ID (our ID)
|
|
479
|
-
0x00, 0x02, // Firmware version (0.2)
|
|
480
|
-
0x01, 0x02, // Protocol version
|
|
481
|
-
0x41, 0x02, // TX Buffer (little-endian 0x0241 = 577)
|
|
482
|
-
0x00, 0x02, // RX Buffer (little-endian 0x0200 = 512)
|
|
483
|
-
0x00, 0x00, // Future/reserved
|
|
484
|
-
0x01, // Encryption Type (1 = Type 1)
|
|
485
|
-
0x01, // Extra byte (matches SDK exactly)
|
|
486
|
-
]);
|
|
487
|
-
const packet = session.buildOpenSessionResponse(openSessionPayload);
|
|
488
|
-
sendPacket(packet);
|
|
489
|
-
handshakeState = 'SENT_OPEN_SESSION';
|
|
490
|
-
} else if (handshakeState === 'WAITING_TO_SEND_REQUEST_ACCESS') {
|
|
491
|
-
// Panel ACKed our COMMAND_RESPONSE to their REQUEST_ACCESS
|
|
492
|
-
// Now send our REQUEST_ACCESS
|
|
493
|
-
log(`[Handshake] Got ACK, now sending our REQUEST_ACCESS`);
|
|
494
|
-
// Enable encryption with the SEND key derived from panel's initializer
|
|
495
|
-
session.sendAesActive = true;
|
|
496
|
-
session.sendAesKey = session.derivedSendKey;
|
|
497
|
-
log(`[Handshake] Encrypting REQUEST_ACCESS with SEND key: ${session.sendAesKey?.toString('hex')}`);
|
|
498
|
-
|
|
499
|
-
// Build REQUEST_ACCESS based on detected encryption type
|
|
500
|
-
let reqAccessPacket;
|
|
501
|
-
if (detectedEncryptionType === 2) {
|
|
502
|
-
log(`[Handshake] Using Type 2 REQUEST_ACCESS`);
|
|
503
|
-
reqAccessPacket = session.buildRequestAccessType2();
|
|
504
|
-
} else {
|
|
505
|
-
log(`[Handshake] Using Type 1 REQUEST_ACCESS`);
|
|
506
|
-
reqAccessPacket = session.buildRequestAccessType1();
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
log(`[Handshake] Pending receive key stored: ${session.pendingReceiveKey?.toString('hex')}`);
|
|
510
|
-
sendPacket(reqAccessPacket);
|
|
511
|
-
|
|
512
|
-
// Enable receive encryption immediately - panel will respond with encrypted COMMAND_RESPONSE
|
|
513
|
-
session.receiveAesActive = true;
|
|
514
|
-
session.receiveAesKey = session.pendingReceiveKey;
|
|
515
|
-
log(`[Handshake] Enabled receive encryption with RECV key: ${session.receiveAesKey?.toString('hex')}`);
|
|
516
|
-
|
|
517
|
-
handshakeState = 'SENT_REQUEST_ACCESS';
|
|
518
|
-
} else if (handshakeState === 'SENT_REQUEST_ACCESS') {
|
|
519
|
-
// Panel ACKed our REQUEST_ACCESS - session is established!
|
|
520
|
-
handshakeState = 'ESTABLISHED';
|
|
521
|
-
log(`[Handshake] *** SESSION ESTABLISHED ***`);
|
|
522
|
-
log(`[Handshake] Derived SEND key: ${session.derivedSendKey?.toString('hex')}`);
|
|
523
|
-
log(`[Handshake] Pending RECV key: ${session.pendingReceiveKey?.toString('hex')}`);
|
|
524
|
-
|
|
525
|
-
// NOW enable encryption for post-handshake communication
|
|
526
|
-
if (session.derivedSendKey) {
|
|
527
|
-
session.enableSendAes(session.derivedSendKey);
|
|
528
|
-
log(`[Handshake] SEND AES now enabled: ${session.derivedSendKey.toString('hex')}`);
|
|
529
|
-
}
|
|
530
|
-
if (session.pendingReceiveKey) {
|
|
531
|
-
session.enableReceiveAes(session.pendingReceiveKey);
|
|
532
|
-
log(`[Handshake] RECV AES now enabled: ${session.pendingReceiveKey.toString('hex')}`);
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
console.log(`\n[Ready] Session established!`);
|
|
536
|
-
console.log(` SEND key: ${session.derivedSendKey?.toString('hex')}`);
|
|
537
|
-
console.log(` RECV key: ${session.pendingReceiveKey?.toString('hex')}`);
|
|
538
|
-
console.log(` Commands: z (zone status), p (partition), g (global), auth (authenticate)\n`);
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
function handleCommandResponse(parsed) {
|
|
543
|
-
const data = parsed.commandData;
|
|
544
|
-
// COMMAND_RESPONSE payload format: [echoed_byte, response_code]
|
|
545
|
-
// Note: parseHeader treats byte 4 as appSequence, which for COMMAND_RESPONSE is actually the echoed byte
|
|
546
|
-
const appSeqAsEcho = parsed.appSequence; // This is actually the echoed byte from our request
|
|
547
|
-
const echoedByte = data && data.length >= 1 ? data[0] : (appSeqAsEcho !== undefined ? appSeqAsEcho : 0xFF);
|
|
548
|
-
const responseCode = data && data.length >= 1 ? data[0] : 0xFF;
|
|
549
|
-
log(`[Handshake] Got COMMAND_RESPONSE (echoed app_seq: ${appSeqAsEcho}, response_code: ${responseCode}), sending ACK`);
|
|
550
|
-
|
|
551
|
-
const ack = session.buildSimpleAck();
|
|
552
|
-
sendPacket(ack);
|
|
553
|
-
|
|
554
|
-
if (handshakeState === 'SENT_OPEN_SESSION') {
|
|
555
|
-
// Just waiting for panel's REQUEST_ACCESS now
|
|
556
|
-
handshakeState = 'WAITING_REQUEST_ACCESS';
|
|
557
|
-
} else if (handshakeState === 'SENT_REQUEST_ACCESS') {
|
|
558
|
-
// Session established!
|
|
559
|
-
handshakeState = 'ESTABLISHED';
|
|
560
|
-
log(`[Handshake] *** SESSION ESTABLISHED ***`);
|
|
561
|
-
log(`[Handshake] Derived SEND key: ${session.derivedSendKey?.toString('hex')}`);
|
|
562
|
-
log(`[Handshake] Pending RECV key: ${session.pendingReceiveKey?.toString('hex')}`);
|
|
563
|
-
|
|
564
|
-
// NOW enable send encryption for post-handshake commands
|
|
565
|
-
if (session.derivedSendKey) {
|
|
566
|
-
session.enableSendAes(session.derivedSendKey);
|
|
567
|
-
log(`[Handshake] Send AES ENABLED with key: ${session.derivedSendKey.toString('hex')}`);
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
// Enable receive AES decryption in case panel encrypts responses
|
|
571
|
-
if (session.pendingReceiveKey) {
|
|
572
|
-
session.enableReceiveAes(session.pendingReceiveKey);
|
|
573
|
-
log(`[Handshake] Receive AES ENABLED with key: ${session.pendingReceiveKey.toString('hex')}`);
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
console.log(`\n[Ready] Session established (Type ${detectedEncryptionType || 1} encryption)`);
|
|
577
|
-
console.log(` SEND key: ${session.derivedSendKey?.toString('hex')}`);
|
|
578
|
-
console.log(` RECV key: ${session.pendingReceiveKey?.toString('hex')}`);
|
|
579
|
-
console.log(` Commands: z (zone status), p (partition), g (global), auth (authenticate)\n`);
|
|
580
|
-
|
|
581
|
-
// Wait for session to stabilize, then send test zone query
|
|
582
|
-
// setTimeout(() => {
|
|
583
|
-
// sendTestZoneQuery();
|
|
584
|
-
// }, 3000);
|
|
585
|
-
}
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
function handleCommandError(parsed) {
|
|
589
|
-
const errorData = parsed.commandData;
|
|
590
|
-
log(`[Handshake] Received COMMAND_ERROR response`);
|
|
591
|
-
if (errorData) {
|
|
592
|
-
log(`[Handshake] Error data: ${errorData.toString('hex')}`);
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
// Send ACK anyway
|
|
596
|
-
const ack = session.buildSimpleAck();
|
|
597
|
-
sendPacket(ack);
|
|
598
|
-
|
|
599
|
-
if (handshakeState === 'SENT_REQUEST_ACCESS') {
|
|
600
|
-
// Session established despite error (we have keys)
|
|
601
|
-
handshakeState = 'ESTABLISHED';
|
|
602
|
-
log(`[Handshake] *** SESSION ESTABLISHED (despite error response) ***`);
|
|
603
|
-
log(`[Handshake] Derived SEND key: ${session.derivedSendKey?.toString('hex')}`);
|
|
604
|
-
log(`[Handshake] Pending RECV key: ${session.pendingReceiveKey?.toString('hex')}`);
|
|
605
|
-
console.log(`\n[Ready] Session established! (frames remain plaintext per SDK analysis)`);
|
|
606
|
-
console.log(` Commands: z (zone status), p (partition), g (global), auth (authenticate)\n`);
|
|
607
|
-
|
|
608
|
-
// Wait for session to stabilize, then send test zone query
|
|
609
|
-
// setTimeout(() => {
|
|
610
|
-
// sendTestZoneQuery();
|
|
611
|
-
// }, 3000);
|
|
612
|
-
}
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
server.on('message', (msg, rinfo) => {
|
|
616
|
-
log(`\n[UDP] RX from ${rinfo.address}:${rinfo.port} (${msg.length} bytes)`);
|
|
617
|
-
hexDump(msg);
|
|
618
|
-
|
|
619
|
-
// Track panel address
|
|
620
|
-
if (!panelAddress || panelAddress !== rinfo.address) {
|
|
621
|
-
panelAddress = rinfo.address;
|
|
622
|
-
panelPort = rinfo.port;
|
|
623
|
-
log(`[Session] Creating new session for ${panelAddress}`);
|
|
624
|
-
session = new ITv2Session(INTEGRATION_ID, ACCESS_CODE, log);
|
|
625
|
-
handshakeState = 'WAITING';
|
|
626
|
-
testCommandSent = false;
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
try {
|
|
630
|
-
const parsed = session.parsePacket(msg);
|
|
631
|
-
|
|
632
|
-
log(`[Packet] Integration ID: ${parsed.integrationId}`);
|
|
633
|
-
log(`[Packet] Sender Seq: ${parsed.senderSequence}, Receiver Seq: ${parsed.receiverSequence}`);
|
|
634
|
-
|
|
635
|
-
const cmdName = CMD_NAMES[parsed.command] || `0x${parsed.command?.toString(16)}`;
|
|
636
|
-
log(`[Packet] Command: ${cmdName}`);
|
|
637
|
-
|
|
638
|
-
if (parsed.appSequence !== undefined) {
|
|
639
|
-
log(`[Packet] App Seq: ${parsed.appSequence}`);
|
|
640
|
-
}
|
|
641
|
-
if (parsed.commandData && parsed.commandData.length > 0) {
|
|
642
|
-
log(`[Packet] Data (${parsed.commandData.length} bytes): ${parsed.commandData.toString('hex').toUpperCase().match(/.{2}/g).join(' ')}`);
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
// Handle by command
|
|
646
|
-
// Note: ACK packets have no command field (null), treat as SIMPLE_ACK
|
|
647
|
-
const cmd = parsed.command;
|
|
648
|
-
|
|
649
|
-
if (cmd === null || cmd === undefined || cmd === CMD.SIMPLE_ACK) {
|
|
650
|
-
handleSimpleAck(parsed);
|
|
651
|
-
return;
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
switch (cmd) {
|
|
655
|
-
case CMD.OPEN_SESSION:
|
|
656
|
-
handleOpenSession(parsed);
|
|
657
|
-
break;
|
|
658
|
-
case CMD.REQUEST_ACCESS:
|
|
659
|
-
handleRequestAccess(parsed);
|
|
660
|
-
break;
|
|
661
|
-
case CMD.COMMAND_RESPONSE:
|
|
662
|
-
handleCommandResponse(parsed);
|
|
663
|
-
break;
|
|
664
|
-
case CMD.COMMAND_ERROR:
|
|
665
|
-
handleCommandError(parsed);
|
|
666
|
-
break;
|
|
667
|
-
case CMD.POLL:
|
|
668
|
-
log(`[Session] Got POLL, sending ACK`);
|
|
669
|
-
sendPacket(session.buildSimpleAck());
|
|
670
|
-
break;
|
|
671
|
-
case CMD.ZONE_STATUS:
|
|
672
|
-
handleZoneStatusResponse(parsed);
|
|
673
|
-
break;
|
|
674
|
-
case CMD.PARTITION_STATUS:
|
|
675
|
-
handlePartitionStatusResponse(parsed);
|
|
676
|
-
break;
|
|
677
|
-
case CMD.GLOBAL_STATUS:
|
|
678
|
-
handleGlobalStatusResponse(parsed);
|
|
679
|
-
break;
|
|
680
|
-
case CMD.SYSTEM_CAPABILITIES:
|
|
681
|
-
handleSystemResponse('SYSTEM_CAPABILITIES', parsed);
|
|
682
|
-
break;
|
|
683
|
-
case CMD.SOFTWARE_VERSION:
|
|
684
|
-
handleSystemResponse('SOFTWARE_VERSION', parsed);
|
|
685
|
-
break;
|
|
686
|
-
case CMD.PANEL_STATUS:
|
|
687
|
-
handleSystemResponse('PANEL_STATUS', parsed);
|
|
688
|
-
break;
|
|
689
|
-
case CMD.BUFFER_SIZES:
|
|
690
|
-
handleSystemResponse('BUFFER_SIZES', parsed);
|
|
691
|
-
break;
|
|
692
|
-
// Notification events from panel
|
|
693
|
-
case CMD.NOTIFICATION_ARMING:
|
|
694
|
-
handlePartitionArmingNotification(parsed);
|
|
695
|
-
break;
|
|
696
|
-
case CMD.NOTIFICATION_PARTITION_READY:
|
|
697
|
-
handlePartitionReadyNotification(parsed);
|
|
698
|
-
break;
|
|
699
|
-
case CMD.NOTIFICATION_PARTITION_TROUBLE:
|
|
700
|
-
handlePartitionTroubleNotification(parsed);
|
|
701
|
-
break;
|
|
702
|
-
case CMD.ZONE_ALARM_STATUS:
|
|
703
|
-
handleZoneAlarmNotification(parsed);
|
|
704
|
-
break;
|
|
705
|
-
case CMD.LIFESTYLE_ZONE_STATUS:
|
|
706
|
-
handleLifestyleZoneNotification(parsed);
|
|
707
|
-
break;
|
|
708
|
-
case CMD.TIME_DATE_BROADCAST:
|
|
709
|
-
handleTimeDateNotification(parsed);
|
|
710
|
-
break;
|
|
711
|
-
case CMD.GENERAL_NOTIFICATION:
|
|
712
|
-
case CMD.DATA_UPDATE_NOTIFICATION:
|
|
713
|
-
log(`[Notification] Received ${CMD_NAMES[cmd] || cmd} notification`);
|
|
714
|
-
if (parsed.commandData) {
|
|
715
|
-
log(`[Notification] Data: ${parsed.commandData.toString('hex')}`);
|
|
716
|
-
}
|
|
717
|
-
sendPacket(session.buildSimpleAck());
|
|
718
|
-
break;
|
|
719
|
-
default:
|
|
720
|
-
if (handshakeState === 'ESTABLISHED') {
|
|
721
|
-
log(`[Session] Handling command ${CMD_NAMES[cmd] || '0x' + cmd.toString(16)} in established session`);
|
|
722
|
-
// Send ACK for unhandled commands
|
|
723
|
-
sendPacket(session.buildSimpleAck());
|
|
724
|
-
}
|
|
725
|
-
break;
|
|
726
|
-
}
|
|
727
|
-
} catch (err) {
|
|
728
|
-
log(`[Error] ${err.message}`);
|
|
729
|
-
|
|
730
|
-
// If we get a parse error, the panel might be sending encrypted data from a stale session
|
|
731
|
-
// Reset our state and wait for panel to timeout and restart with plaintext OPEN_SESSION
|
|
732
|
-
if (handshakeState !== 'WAITING') {
|
|
733
|
-
log(`[Recovery] Parse error during handshake state '${handshakeState}', resetting to wait for panel restart`);
|
|
734
|
-
handshakeState = 'WAITING';
|
|
735
|
-
session = new ITv2Session(INTEGRATION_ID, ACCESS_CODE, log);
|
|
736
|
-
log(`[Recovery] Session reset. Waiting for panel to timeout and send plaintext OPEN_SESSION...`);
|
|
737
|
-
} else {
|
|
738
|
-
log(`[Recovery] Ignoring unparseable packet, waiting for plaintext OPEN_SESSION...`);
|
|
739
|
-
}
|
|
740
|
-
}
|
|
741
|
-
});
|
|
742
|
-
|
|
743
|
-
server.on('listening', () => {
|
|
744
|
-
const addr = server.address();
|
|
745
|
-
log(`[UDP] Server listening on ${addr.address}:${addr.port}`);
|
|
746
|
-
});
|
|
747
|
-
|
|
748
|
-
server.on('error', (err) => {
|
|
749
|
-
log(`[UDP] Server error: ${err.message}`);
|
|
750
|
-
server.close();
|
|
751
|
-
});
|
|
752
|
-
|
|
753
|
-
// Interactive command handling
|
|
754
|
-
const rl = readline.createInterface({
|
|
755
|
-
input: process.stdin,
|
|
756
|
-
output: process.stdout
|
|
757
|
-
});
|
|
758
|
-
|
|
759
|
-
rl.on('line', (line) => {
|
|
760
|
-
const [cmd, ...args] = line.trim().split(/\s+/);
|
|
761
|
-
|
|
762
|
-
if (!session || handshakeState !== 'ESTABLISHED') {
|
|
763
|
-
console.log('[Error] Session not established');
|
|
764
|
-
return;
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
switch (cmd) {
|
|
768
|
-
case 'auth': {
|
|
769
|
-
const code = args[0] || MASTER_CODE;
|
|
770
|
-
const partition = parseInt(args[1] || '1', 10);
|
|
771
|
-
log(`[Command] Authenticating with code ${code} on partition ${partition}`);
|
|
772
|
-
const packet = session.buildAccessLevelEnter(code, partition, 0, 'bcd');
|
|
773
|
-
sendPacket(packet);
|
|
774
|
-
break;
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
// ============ Arm/Disarm Commands ============
|
|
778
|
-
case 'arm': {
|
|
779
|
-
const partition = parseInt(args[0] || '1', 10);
|
|
780
|
-
const mode = parseInt(args[1] || '1', 10); // 0=stay, 1=away, 2=no-entry, 3=force
|
|
781
|
-
const code = args[2] || MASTER_CODE;
|
|
782
|
-
|
|
783
|
-
const modeNames = ['STAY', 'AWAY', 'NO_ENTRY', 'FORCE'];
|
|
784
|
-
log(`[Command] Arming partition ${partition} in ${modeNames[mode] || mode} mode`);
|
|
785
|
-
const packet = session.buildPartitionArm(partition, mode, code);
|
|
786
|
-
sendPacket(packet);
|
|
787
|
-
break;
|
|
788
|
-
}
|
|
789
|
-
case 'disarm': {
|
|
790
|
-
const partition = parseInt(args[0] || '1', 10);
|
|
791
|
-
const code = args[1] || MASTER_CODE;
|
|
792
|
-
|
|
793
|
-
log(`[Command] Disarming partition ${partition}`);
|
|
794
|
-
const packet = session.buildPartitionDisarm(partition, code);
|
|
795
|
-
sendPacket(packet);
|
|
796
|
-
break;
|
|
797
|
-
}
|
|
798
|
-
case 'stay': {
|
|
799
|
-
// Shortcut for arm stay
|
|
800
|
-
const partition = parseInt(args[0] || '1', 10);
|
|
801
|
-
const code = args[1] || MASTER_CODE;
|
|
802
|
-
|
|
803
|
-
log(`[Command] Arming partition ${partition} in STAY mode`);
|
|
804
|
-
const packet = session.buildPartitionArm(partition, 0, code);
|
|
805
|
-
sendPacket(packet);
|
|
806
|
-
break;
|
|
807
|
-
}
|
|
808
|
-
case 'away': {
|
|
809
|
-
// Shortcut for arm away
|
|
810
|
-
const partition = parseInt(args[0] || '1', 10);
|
|
811
|
-
const code = args[1] || MASTER_CODE;
|
|
812
|
-
|
|
813
|
-
log(`[Command] Arming partition ${partition} in AWAY mode`);
|
|
814
|
-
const packet = session.buildPartitionArm(partition, 1, code);
|
|
815
|
-
sendPacket(packet);
|
|
816
|
-
break;
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
// ============ Status Display Commands ============
|
|
820
|
-
case 'zones': {
|
|
821
|
-
// Show all zones with status from EventHandler
|
|
822
|
-
const zones = eventHandler.getAllZones();
|
|
823
|
-
console.log(`\n========================================`);
|
|
824
|
-
console.log(`ALL ZONES (${zones.length} with status)`);
|
|
825
|
-
console.log(`========================================`);
|
|
826
|
-
if (zones.length === 0) {
|
|
827
|
-
console.log(' No zone status received yet');
|
|
828
|
-
console.log(' (Open/close a zone to trigger notification)');
|
|
829
|
-
} else {
|
|
830
|
-
zones.forEach(z => {
|
|
831
|
-
const flags = [];
|
|
832
|
-
if (z.open) flags.push('OPEN');
|
|
833
|
-
if (z.bypassed) flags.push('BYPASSED');
|
|
834
|
-
if (z.alarm) flags.push('ALARM');
|
|
835
|
-
if (z.trouble) flags.push('TROUBLE');
|
|
836
|
-
if (z.tamper) flags.push('TAMPER');
|
|
837
|
-
console.log(` Zone ${z.zoneNumber}: ${flags.length > 0 ? flags.join(', ') : 'OK/CLOSED'}`);
|
|
838
|
-
});
|
|
839
|
-
}
|
|
840
|
-
console.log(`========================================\n`);
|
|
841
|
-
break;
|
|
842
|
-
}
|
|
843
|
-
case 'partitions': {
|
|
844
|
-
// Show all partitions with status from EventHandler
|
|
845
|
-
const partitions = eventHandler.getAllPartitions();
|
|
846
|
-
console.log(`\n========================================`);
|
|
847
|
-
console.log(`ALL PARTITIONS (${partitions.length} with status)`);
|
|
848
|
-
console.log(`========================================`);
|
|
849
|
-
if (partitions.length === 0) {
|
|
850
|
-
console.log(' No partition status received yet');
|
|
851
|
-
console.log(' (Arm/disarm to trigger notification)');
|
|
852
|
-
} else {
|
|
853
|
-
partitions.forEach(p => {
|
|
854
|
-
console.log(` Partition ${p.partitionNumber}: ${p.armedStateName || 'UNKNOWN'} ${p.ready ? '[READY]' : '[NOT READY]'}`);
|
|
855
|
-
});
|
|
856
|
-
}
|
|
857
|
-
console.log(`========================================\n`);
|
|
858
|
-
break;
|
|
859
|
-
}
|
|
860
|
-
case 'status': {
|
|
861
|
-
// Show complete system status
|
|
862
|
-
eventHandler.printStatus();
|
|
863
|
-
break;
|
|
864
|
-
}
|
|
865
|
-
|
|
866
|
-
// ============ Query Commands ============
|
|
867
|
-
case 'p': {
|
|
868
|
-
const partition = parseInt(args[0] || '1', 10);
|
|
869
|
-
log(`[Command] Querying partition ${partition} status`);
|
|
870
|
-
const packet = session.buildPartitionStatusRequest(partition);
|
|
871
|
-
sendPacket(packet);
|
|
872
|
-
break;
|
|
873
|
-
}
|
|
874
|
-
case 'z': {
|
|
875
|
-
const zone = parseInt(args[0] || '0', 10);
|
|
876
|
-
log(`[Command] Querying zone ${zone} status`);
|
|
877
|
-
const packet = session.buildZoneStatusRequest(zone);
|
|
878
|
-
sendPacket(packet);
|
|
879
|
-
break;
|
|
880
|
-
}
|
|
881
|
-
case 'g': {
|
|
882
|
-
log(`[Command] Querying global status`);
|
|
883
|
-
const packet = session.buildGlobalStatusRequest();
|
|
884
|
-
sendPacket(packet);
|
|
885
|
-
break;
|
|
886
|
-
}
|
|
887
|
-
case 't': {
|
|
888
|
-
log(`[Command] Querying trouble status`);
|
|
889
|
-
const packet = session.buildTroubleStatusRequest();
|
|
890
|
-
sendPacket(packet);
|
|
891
|
-
break;
|
|
892
|
-
}
|
|
893
|
-
case 'b': {
|
|
894
|
-
const zone = parseInt(args[0] || '0', 10);
|
|
895
|
-
log(`[Command] Querying zone ${zone} bypass status`);
|
|
896
|
-
const packet = session.buildZoneBypassStatusRequest(zone);
|
|
897
|
-
sendPacket(packet);
|
|
898
|
-
break;
|
|
899
|
-
}
|
|
900
|
-
case 'cap': {
|
|
901
|
-
log(`[Command] Requesting system capabilities`);
|
|
902
|
-
const packet = session.buildCommand(CMD.SYSTEM_CAPABILITIES);
|
|
903
|
-
sendPacket(packet);
|
|
904
|
-
break;
|
|
905
|
-
}
|
|
906
|
-
case 'poll': {
|
|
907
|
-
log(`[Command] Sending POLL/keepalive`);
|
|
908
|
-
const packet = session.buildPoll();
|
|
909
|
-
sendPacket(packet);
|
|
910
|
-
break;
|
|
911
|
-
}
|
|
912
|
-
case 'exit':
|
|
913
|
-
case 'quit': {
|
|
914
|
-
console.log('[Shutdown] Closing session...');
|
|
915
|
-
if (session && handshakeState === 'ESTABLISHED') {
|
|
916
|
-
try {
|
|
917
|
-
const endSessionPacket = session.buildEndSession();
|
|
918
|
-
sendPacket(endSessionPacket);
|
|
919
|
-
console.log('[Shutdown] END_SESSION sent to panel');
|
|
920
|
-
} catch (err) {
|
|
921
|
-
console.log(`[Shutdown] Error: ${err.message}`);
|
|
922
|
-
}
|
|
923
|
-
}
|
|
924
|
-
setTimeout(() => {
|
|
925
|
-
console.log('[Shutdown] Goodbye!');
|
|
926
|
-
process.exit(0);
|
|
927
|
-
}, 100);
|
|
928
|
-
break;
|
|
929
|
-
}
|
|
930
|
-
case 'help':
|
|
931
|
-
console.log(`
|
|
932
|
-
Interactive Commands:
|
|
933
|
-
|
|
934
|
-
ARM/DISARM:
|
|
935
|
-
arm <part> <mode> [code] - Arm partition (mode: 0=stay, 1=away, 2=no-entry, 3=force)
|
|
936
|
-
disarm <part> [code] - Disarm partition
|
|
937
|
-
stay [part] [code] - Shortcut: Arm partition in STAY mode
|
|
938
|
-
away [part] [code] - Shortcut: Arm partition in AWAY mode
|
|
939
|
-
|
|
940
|
-
STATUS DISPLAY:
|
|
941
|
-
zones - Show all zones with current status
|
|
942
|
-
partitions - Show all partitions with current status
|
|
943
|
-
status - Show complete system status summary
|
|
944
|
-
|
|
945
|
-
QUERY PANEL:
|
|
946
|
-
auth [code] - Authenticate with access code (default=${MASTER_CODE})
|
|
947
|
-
p [n] - Query partition n status (default=1)
|
|
948
|
-
z [n] - Query zone n status (default=0=all)
|
|
949
|
-
g - Query global status
|
|
950
|
-
t - Query trouble status
|
|
951
|
-
b [n] - Query zone bypass status
|
|
952
|
-
cap - Request system capabilities
|
|
953
|
-
poll - Send poll/keepalive
|
|
954
|
-
|
|
955
|
-
OTHER:
|
|
956
|
-
help - Show this help
|
|
957
|
-
|
|
958
|
-
Examples:
|
|
959
|
-
arm 1 0 5555 - Arm partition 1 in STAY mode with code 5555
|
|
960
|
-
arm 1 1 - Arm partition 1 in AWAY mode with default code
|
|
961
|
-
disarm 1 - Disarm partition 1 with default code
|
|
962
|
-
stay - Arm partition 1 in STAY mode
|
|
963
|
-
away - Arm partition 1 in AWAY mode
|
|
964
|
-
zones - Show current zone status
|
|
965
|
-
status - Show system summary
|
|
966
|
-
`);
|
|
967
|
-
break;
|
|
968
|
-
default:
|
|
969
|
-
console.log(`[Error] Unknown command: ${cmd}. Type 'help' for available commands.`);
|
|
970
|
-
}
|
|
971
|
-
});
|
|
972
|
-
|
|
973
|
-
// Print banner
|
|
974
|
-
console.log(`
|
|
975
|
-
============================================================
|
|
976
|
-
DSC TL280R ITV2 Protocol Server (Enhanced)
|
|
977
|
-
============================================================
|
|
978
|
-
|
|
979
|
-
Configuration:
|
|
980
|
-
Integration ID: ${INTEGRATION_ID}
|
|
981
|
-
Access Code: ${ACCESS_CODE}
|
|
982
|
-
Master Code: ${MASTER_CODE}
|
|
983
|
-
UDP Port: ${UDP_PORT}
|
|
984
|
-
|
|
985
|
-
Set environment variables to override:
|
|
986
|
-
INTEGRATION_ID=${INTEGRATION_ID} ACCESS_CODE=12345678 MASTER_CODE=${MASTER_CODE} npm run server
|
|
987
|
-
|
|
988
|
-
Interactive Commands (type when session established):
|
|
989
|
-
|
|
990
|
-
ARM/DISARM:
|
|
991
|
-
arm <part> <mode> [code] - Arm partition (mode: 0=stay, 1=away)
|
|
992
|
-
disarm <part> [code] - Disarm partition
|
|
993
|
-
stay [part] [code] - Arm in STAY mode
|
|
994
|
-
away [part] [code] - Arm in AWAY mode
|
|
995
|
-
|
|
996
|
-
STATUS:
|
|
997
|
-
zones - Show all zones with status
|
|
998
|
-
partitions - Show all partitions with status
|
|
999
|
-
status - Show system status summary
|
|
1000
|
-
|
|
1001
|
-
QUERY:
|
|
1002
|
-
z [n], p [n], g, t, b [n] - Query zone/partition/global/trouble/bypass
|
|
1003
|
-
auth [code], cap, poll - Authenticate/capabilities/keepalive
|
|
1004
|
-
|
|
1005
|
-
OTHER:
|
|
1006
|
-
exit, quit - Close session and exit gracefully
|
|
1007
|
-
|
|
1008
|
-
Type 'help' for full command list.
|
|
1009
|
-
|
|
1010
|
-
Waiting for TL280R to connect...
|
|
1011
|
-
`);
|
|
1012
|
-
|
|
1013
|
-
server.bind(UDP_PORT, '0.0.0.0');
|
|
1014
|
-
|
|
1015
|
-
// Graceful shutdown handler
|
|
1016
|
-
process.on('SIGINT', () => {
|
|
1017
|
-
console.log('\n\n[Shutdown] Closing session gracefully...');
|
|
1018
|
-
|
|
1019
|
-
if (session && handshakeState === 'ESTABLISHED') {
|
|
1020
|
-
try {
|
|
1021
|
-
const endSessionPacket = session.buildEndSession();
|
|
1022
|
-
sendPacket(endSessionPacket);
|
|
1023
|
-
console.log('[Shutdown] END_SESSION sent to panel');
|
|
1024
|
-
} catch (err) {
|
|
1025
|
-
console.log(`[Shutdown] Error sending END_SESSION: ${err.message}`);
|
|
1026
|
-
}
|
|
1027
|
-
}
|
|
1028
|
-
|
|
1029
|
-
setTimeout(() => {
|
|
1030
|
-
console.log('[Shutdown] Goodbye!');
|
|
1031
|
-
process.exit(0);
|
|
1032
|
-
}, 100);
|
|
1033
|
-
});
|
|
1
|
+
/**
|
|
2
|
+
* DSC ITV2 Interactive CLI
|
|
3
|
+
*
|
|
4
|
+
* Real-time monitoring with interactive commands for querying
|
|
5
|
+
* zone/partition status and arming/disarming partitions.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { ITV2Client, ITv2Session } from '../index.js';
|
|
9
|
+
import readline from 'readline';
|
|
10
|
+
|
|
11
|
+
const config = {
|
|
12
|
+
integrationId: process.env.INTEGRATION_ID || '221005294611',
|
|
13
|
+
accessCode: process.env.ACCESS_CODE || '13268451',
|
|
14
|
+
masterCode: process.env.MASTER_CODE || '1234',
|
|
15
|
+
port: parseInt(process.env.PORT || '3072'),
|
|
16
|
+
logLevel: process.env.LOG_LEVEL || 'verbose',
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const client = new ITV2Client(config);
|
|
20
|
+
let ready = false;
|
|
21
|
+
|
|
22
|
+
// ==================== Readline Setup ====================
|
|
23
|
+
|
|
24
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
25
|
+
const prompt = () => { if (ready) rl.prompt(); };
|
|
26
|
+
rl.setPrompt('dsc> ');
|
|
27
|
+
|
|
28
|
+
// ==================== Event Handlers ====================
|
|
29
|
+
|
|
30
|
+
client.on('listening', (addr) => {
|
|
31
|
+
console.log(`Listening on ${addr.address}:${addr.port}`);
|
|
32
|
+
console.log('Waiting for panel...\n');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
client.on('session:established', () => {
|
|
36
|
+
console.log('Session established\n');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
client.on('status:ready', ({ zones, partition }) => {
|
|
40
|
+
ready = true;
|
|
41
|
+
const open = zones.filter(z => z.open);
|
|
42
|
+
console.log(`${zones.length} zones loaded, ${open.length} open`);
|
|
43
|
+
if (partition) {
|
|
44
|
+
const state = partition.awayArmed ? 'AWAY' :
|
|
45
|
+
partition.stayArmed ? 'STAY' :
|
|
46
|
+
partition.nightArmed ? 'NIGHT' :
|
|
47
|
+
partition.armed ? 'ARMED' : 'DISARMED';
|
|
48
|
+
console.log(`Partition 1: ${state}`);
|
|
49
|
+
}
|
|
50
|
+
console.log('\nType "help" for commands.\n');
|
|
51
|
+
prompt();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
client.on('session:closed', () => {
|
|
55
|
+
ready = false;
|
|
56
|
+
console.log('\nSession closed');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
client.on('zone:open', (z) => {
|
|
60
|
+
console.log(`\n Zone ${z} OPENED`);
|
|
61
|
+
prompt();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
client.on('zone:closed', (z) => {
|
|
65
|
+
console.log(`\n Zone ${z} CLOSED`);
|
|
66
|
+
prompt();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
client.on('partition:arming', ({ partition, modeName }) => {
|
|
70
|
+
console.log(`\n Partition ${partition}: ${modeName}`);
|
|
71
|
+
prompt();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
client.on('partition:ready', ({ partition, isReady }) => {
|
|
75
|
+
console.log(`\n Partition ${partition}: ${isReady ? 'READY' : 'NOT READY'}`);
|
|
76
|
+
prompt();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// command:error events are protocol-level noise (intermediate acks), not actionable
|
|
80
|
+
|
|
81
|
+
client.on('error', (err) => {
|
|
82
|
+
console.error(`Error: ${err.message}`);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// ==================== Command Handler ====================
|
|
86
|
+
|
|
87
|
+
rl.on('line', async (line) => {
|
|
88
|
+
const [cmd, ...args] = line.trim().split(/\s+/);
|
|
89
|
+
if (!cmd) { prompt(); return; }
|
|
90
|
+
|
|
91
|
+
if (!ready && cmd !== 'quit' && cmd !== 'exit' && cmd !== 'help') {
|
|
92
|
+
console.log('Session not ready');
|
|
93
|
+
prompt();
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
switch (cmd) {
|
|
99
|
+
// ---- Status queries ----
|
|
100
|
+
case 'zones':
|
|
101
|
+
case 'z': {
|
|
102
|
+
const zones = await client.queryZoneStatus();
|
|
103
|
+
const active = zones.filter(z => z.open || z.alarm || z.bypassed || z.tamper);
|
|
104
|
+
if (active.length === 0) {
|
|
105
|
+
console.log('All zones secure');
|
|
106
|
+
} else {
|
|
107
|
+
for (const z of active) {
|
|
108
|
+
const flags = [];
|
|
109
|
+
if (z.open) flags.push('OPEN');
|
|
110
|
+
if (z.alarm) flags.push('ALARM');
|
|
111
|
+
if (z.alarmInMemory) flags.push('ALARM-MEM');
|
|
112
|
+
if (z.bypassed) flags.push('BYPASS');
|
|
113
|
+
if (z.tamper) flags.push('TAMPER');
|
|
114
|
+
if (z.lowBattery) flags.push('LOW-BATT');
|
|
115
|
+
if (z.fault) flags.push('FAULT');
|
|
116
|
+
console.log(` Zone ${z.zoneNumber}: ${flags.join(', ')}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
console.log(`(${zones.length} zones total)`);
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
case 'partition':
|
|
124
|
+
case 'p': {
|
|
125
|
+
const part = parseInt(args[0] || '1');
|
|
126
|
+
const s = await client.queryPartitionStatus(part);
|
|
127
|
+
if (s) {
|
|
128
|
+
const state = s.awayArmed ? 'AWAY' :
|
|
129
|
+
s.stayArmed ? 'STAY' :
|
|
130
|
+
s.nightArmed ? 'NIGHT' :
|
|
131
|
+
s.noEntryDelay ? 'NO-ENTRY' :
|
|
132
|
+
s.armed ? 'ARMED' : 'DISARMED';
|
|
133
|
+
const flags = [];
|
|
134
|
+
if (s.exitDelay) flags.push('EXIT-DELAY');
|
|
135
|
+
if (s.entryDelay) flags.push('ENTRY-DELAY');
|
|
136
|
+
if (s.alarm) flags.push('ALARM');
|
|
137
|
+
if (s.trouble) flags.push('TROUBLE');
|
|
138
|
+
if (s.zonesBypassed) flags.push('ZONES-BYPASSED');
|
|
139
|
+
if (s.alarmInMemory) flags.push('ALARM-MEM');
|
|
140
|
+
if (s.doorChime) flags.push('CHIME');
|
|
141
|
+
console.log(`Partition ${part}: ${state}${flags.length ? ' [' + flags.join(', ') + ']' : ''}`);
|
|
142
|
+
}
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
case 'trouble':
|
|
147
|
+
case 't': {
|
|
148
|
+
const troubles = await client.queryTroubleStatus();
|
|
149
|
+
if (!troubles || troubles.length === 0) {
|
|
150
|
+
console.log('No active troubles');
|
|
151
|
+
} else {
|
|
152
|
+
for (const t of troubles) {
|
|
153
|
+
console.log(` Device ${t.deviceType}: troubles [${t.troubles.join(', ')}]`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
case 'status':
|
|
160
|
+
case 's': {
|
|
161
|
+
const zones = await client.queryZoneStatus();
|
|
162
|
+
const open = zones.filter(z => z.open);
|
|
163
|
+
const alarmed = zones.filter(z => z.alarm);
|
|
164
|
+
const bypassed = zones.filter(z => z.bypassed);
|
|
165
|
+
console.log(`Zones: ${zones.length} total, ${open.length} open, ${alarmed.length} alarm, ${bypassed.length} bypassed`);
|
|
166
|
+
if (open.length) console.log(` Open: ${open.map(z => z.zoneNumber).join(', ')}`);
|
|
167
|
+
|
|
168
|
+
const ps = await client.queryPartitionStatus(1);
|
|
169
|
+
if (ps) {
|
|
170
|
+
const state = ps.awayArmed ? 'AWAY' : ps.stayArmed ? 'STAY' :
|
|
171
|
+
ps.nightArmed ? 'NIGHT' : ps.armed ? 'ARMED' : 'DISARMED';
|
|
172
|
+
console.log(`Partition 1: ${state}${ps.alarm ? ' [ALARM]' : ''}${ps.trouble ? ' [TROUBLE]' : ''}`);
|
|
173
|
+
}
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ---- Arm/Disarm ----
|
|
178
|
+
case 'stay': {
|
|
179
|
+
const part = parseInt(args[0] || '1');
|
|
180
|
+
const code = args[1] || config.masterCode;
|
|
181
|
+
console.log(`Arming partition ${part} STAY...`);
|
|
182
|
+
client.armStay(part, code);
|
|
183
|
+
break;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
case 'away': {
|
|
187
|
+
const part = parseInt(args[0] || '1');
|
|
188
|
+
const code = args[1] || config.masterCode;
|
|
189
|
+
console.log(`Arming partition ${part} AWAY...`);
|
|
190
|
+
client.armAway(part, code);
|
|
191
|
+
break;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
case 'night': {
|
|
195
|
+
const part = parseInt(args[0] || '1');
|
|
196
|
+
const code = args[1] || config.masterCode;
|
|
197
|
+
console.log(`Arming partition ${part} NIGHT...`);
|
|
198
|
+
client.armNight(part, code);
|
|
199
|
+
break;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
case 'disarm': {
|
|
203
|
+
const part = parseInt(args[0] || '1');
|
|
204
|
+
const code = args[1] || config.masterCode;
|
|
205
|
+
console.log(`Disarming partition ${part}...`);
|
|
206
|
+
client.disarm(part, code);
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ---- Auth ----
|
|
211
|
+
case 'auth': {
|
|
212
|
+
const code = args[0] || config.masterCode;
|
|
213
|
+
console.log(`Authenticating with code ${code}...`);
|
|
214
|
+
client.authenticate(code);
|
|
215
|
+
break;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ---- Other ----
|
|
219
|
+
case 'help':
|
|
220
|
+
case '?':
|
|
221
|
+
console.log(`
|
|
222
|
+
STATUS
|
|
223
|
+
zones, z Query all zone statuses
|
|
224
|
+
partition [n], p Query partition status (default 1)
|
|
225
|
+
trouble, t Query system troubles
|
|
226
|
+
status, s Quick system overview
|
|
227
|
+
|
|
228
|
+
ARM/DISARM
|
|
229
|
+
stay [part] [code] Arm partition in STAY mode
|
|
230
|
+
away [part] [code] Arm partition in AWAY mode
|
|
231
|
+
night [part] [code] Arm partition in NIGHT mode
|
|
232
|
+
disarm [part] [code] Disarm partition
|
|
233
|
+
|
|
234
|
+
OTHER
|
|
235
|
+
auth [code] Authenticate with master/user code
|
|
236
|
+
help, ? Show this help
|
|
237
|
+
quit, exit Close session and exit
|
|
238
|
+
`);
|
|
239
|
+
break;
|
|
240
|
+
|
|
241
|
+
case 'quit':
|
|
242
|
+
case 'exit':
|
|
243
|
+
console.log('Closing...');
|
|
244
|
+
await client.stop();
|
|
245
|
+
process.exit(0);
|
|
246
|
+
break;
|
|
247
|
+
|
|
248
|
+
default:
|
|
249
|
+
console.log(`Unknown command: ${cmd}. Type "help" for commands.`);
|
|
250
|
+
}
|
|
251
|
+
} catch (err) {
|
|
252
|
+
console.log(`Error: ${err.message}`);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
prompt();
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
// ==================== Start ====================
|
|
259
|
+
|
|
260
|
+
console.log('DSC ITV2 Interactive CLI\n');
|
|
261
|
+
console.log(` Integration ID: ${config.integrationId}`);
|
|
262
|
+
console.log(` Port: ${config.port}\n`);
|
|
263
|
+
|
|
264
|
+
client.start().catch((err) => {
|
|
265
|
+
console.error('Failed to start:', err.message);
|
|
266
|
+
process.exit(1);
|
|
267
|
+
});
|