dsc-itv2-client 2.0.3 → 2.0.5
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/package.json +1 -1
- package/src/ITV2Client.js +127 -3
- package/src/constants.js +3 -0
- package/src/event-handler.js +27 -0
- package/src/examples/interactive-cli.js +29 -0
- package/src/itv2-session.js +40 -31
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsc-itv2-client",
|
|
3
3
|
"author": "fajitacat",
|
|
4
|
-
"version": "2.0.
|
|
4
|
+
"version": "2.0.5",
|
|
5
5
|
"description": "Reverse engineered DSC ITV2 Protocol Client Library for TL280/TL280E - Monitor and control DSC Neo alarm panels with real-time zone/partition status, arming, and trouble detail",
|
|
6
6
|
"main": "src/index.js",
|
|
7
7
|
"type": "module",
|
package/src/ITV2Client.js
CHANGED
|
@@ -270,6 +270,11 @@ export class ITV2Client extends EventEmitter {
|
|
|
270
270
|
} catch (_) {}
|
|
271
271
|
|
|
272
272
|
throw new Error('Arm/disarm not confirmed - panel may have open zones or troubles');
|
|
273
|
+
} finally {
|
|
274
|
+
// Arm/disarm clears zone bypass state — refresh zone status after a brief settle
|
|
275
|
+
setTimeout(() => {
|
|
276
|
+
this.queryZoneStatus().catch(() => {});
|
|
277
|
+
}, 2000);
|
|
273
278
|
}
|
|
274
279
|
}
|
|
275
280
|
|
|
@@ -309,9 +314,122 @@ export class ITV2Client extends EventEmitter {
|
|
|
309
314
|
return this._sendArmCommand(packet, partition, 'ARMED');
|
|
310
315
|
}
|
|
311
316
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
317
|
+
/**
|
|
318
|
+
* Bypass a zone (exclude from monitoring).
|
|
319
|
+
* Uses config mode: 0x0704 → 0x074A → 0x0701.
|
|
320
|
+
* @param {number} zone - Zone number to bypass
|
|
321
|
+
* @param {number} [partition=1] - Partition number
|
|
322
|
+
* @param {string} [code] - User/master code (defaults to masterCode)
|
|
323
|
+
* @returns {Promise<object>} Resolves with bypass confirmation
|
|
324
|
+
*/
|
|
325
|
+
async bypassZone(zone, partition = 1, code) {
|
|
326
|
+
return this._sendBypassCommand(zone, partition, true, code);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Unbypass a zone (restore monitoring).
|
|
331
|
+
* @param {number} zone - Zone number to unbypass
|
|
332
|
+
* @param {number} [partition=1] - Partition number
|
|
333
|
+
* @param {string} [code] - User/master code (defaults to masterCode)
|
|
334
|
+
* @returns {Promise<object>} Resolves with unbypass confirmation
|
|
335
|
+
*/
|
|
336
|
+
async unbypassZone(zone, partition = 1, code) {
|
|
337
|
+
return this._sendBypassCommand(zone, partition, false, code);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Send a zone bypass/unbypass command.
|
|
342
|
+
* Flow: 0x0704 (enter config mode) → wait for 0x0702 → 0x074A (bypass) → 0x0701 (exit)
|
|
343
|
+
* @private
|
|
344
|
+
*/
|
|
345
|
+
async _sendBypassCommand(zone, partition, bypass, code, timeout = 10000) {
|
|
346
|
+
if (!this._checkEstablished()) {
|
|
347
|
+
throw new Error('Session not established');
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const accessCode = code || this.masterCode;
|
|
351
|
+
const action = bypass ? 'BYPASS' : 'UNBYPASS';
|
|
352
|
+
|
|
353
|
+
// Step 1: Enter config mode (type=3 = zone bypass, mode=1 = user code)
|
|
354
|
+
const enterPacket = this.session.buildEnterConfigMode(partition, 3, accessCode, 1);
|
|
355
|
+
this._sendPacket(enterPacket);
|
|
356
|
+
|
|
357
|
+
// Step 2: Wait for 0x0702 config status confirmation (COMMAND_RESPONSE is intermediate)
|
|
358
|
+
await this._waitForConfigReady(5000);
|
|
359
|
+
|
|
360
|
+
// Step 3: Send zone bypass write
|
|
361
|
+
const bypassPacket = this.session.buildZoneBypass(partition, zone, bypass);
|
|
362
|
+
|
|
363
|
+
try {
|
|
364
|
+
const result = await new Promise((resolve, reject) => {
|
|
365
|
+
const timer = setTimeout(() => { cleanup(); reject(new Error('timeout')); }, timeout);
|
|
366
|
+
|
|
367
|
+
const onBypass = (data) => {
|
|
368
|
+
if (data.zone === zone) { cleanup(); resolve(data); }
|
|
369
|
+
};
|
|
370
|
+
const cleanup = () => {
|
|
371
|
+
clearTimeout(timer);
|
|
372
|
+
this.removeListener('zone:bypass', onBypass);
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
this.on('zone:bypass', onBypass);
|
|
376
|
+
this._sendPacket(bypassPacket);
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
// Step 4: Exit config mode
|
|
380
|
+
const exitPacket = this.session.buildExitConfigMode(partition);
|
|
381
|
+
this._sendPacket(exitPacket);
|
|
382
|
+
|
|
383
|
+
return result;
|
|
384
|
+
} catch (e) {
|
|
385
|
+
// Always try to exit config mode
|
|
386
|
+
try {
|
|
387
|
+
const exitPacket = this.session.buildExitConfigMode(partition);
|
|
388
|
+
this._sendPacket(exitPacket);
|
|
389
|
+
} catch (_) {}
|
|
390
|
+
|
|
391
|
+
if (e.message !== 'timeout') throw e;
|
|
392
|
+
|
|
393
|
+
// No 0x0820 confirmation — query zone status to check
|
|
394
|
+
try {
|
|
395
|
+
const zones = await this.queryZoneStatus();
|
|
396
|
+
const zoneStatus = zones.find(z => z.zoneNumber === zone);
|
|
397
|
+
if (zoneStatus && zoneStatus.bypassed === bypass) {
|
|
398
|
+
return { zone, bypassed: bypass, fromQuery: true };
|
|
399
|
+
}
|
|
400
|
+
} catch (_) {}
|
|
401
|
+
|
|
402
|
+
throw new Error(`Zone ${zone} ${action} not confirmed - check access code and zone number`);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Wait for 0x0702 (config status) notification confirming config mode is ready.
|
|
408
|
+
* COMMAND_RESPONSE to 0x0704 is an intermediate ack — the real confirmation is 0x0702.
|
|
409
|
+
* @private
|
|
410
|
+
*/
|
|
411
|
+
_waitForConfigReady(timeout = 10000) {
|
|
412
|
+
return new Promise((resolve) => {
|
|
413
|
+
const timer = setTimeout(() => { cleanup(); resolve(); }, timeout);
|
|
414
|
+
const origRoute = this._routePacket.bind(this);
|
|
415
|
+
|
|
416
|
+
const interceptor = (parsed, skipAck) => {
|
|
417
|
+
if (parsed.command === 0x0702) {
|
|
418
|
+
cleanup();
|
|
419
|
+
this._logMinimal('[Config] Config mode ready (0x0702)');
|
|
420
|
+
resolve();
|
|
421
|
+
}
|
|
422
|
+
origRoute(parsed, skipAck);
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
const cleanup = () => {
|
|
426
|
+
clearTimeout(timer);
|
|
427
|
+
this._routePacket = origRoute;
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
this._routePacket = interceptor;
|
|
431
|
+
});
|
|
432
|
+
}
|
|
315
433
|
|
|
316
434
|
/**
|
|
317
435
|
* Disarm partition
|
|
@@ -1328,6 +1446,12 @@ export class ITV2Client extends EventEmitter {
|
|
|
1328
1446
|
const bypassed = fullPayload[offset] === 1;
|
|
1329
1447
|
|
|
1330
1448
|
this._logMinimal(`[Zone ${zone.value}] ${bypassed ? 'BYPASSED' : 'UNBYPASSED'}`);
|
|
1449
|
+
|
|
1450
|
+
// Update cached zone state
|
|
1451
|
+
if (this.eventHandler) {
|
|
1452
|
+
this.eventHandler.handleZoneBypass(zone.value, bypassed);
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1331
1455
|
this.emit('zone:bypass', { zone: zone.value, bypassed });
|
|
1332
1456
|
} catch (e) {
|
|
1333
1457
|
this._log(`[Bypass Status] Parse error: ${e.message}`);
|
package/src/constants.js
CHANGED
|
@@ -47,6 +47,9 @@ export const Commands = {
|
|
|
47
47
|
PARTITION_BYPASS_STATUS: 0x0240,
|
|
48
48
|
|
|
49
49
|
// Configuration (0x07xx)
|
|
50
|
+
CONFIG_EXIT: 0x0701,
|
|
51
|
+
CONFIG_ENTER: 0x0704,
|
|
52
|
+
SINGLE_ZONE_BYPASS_WRITE: 0x074A,
|
|
50
53
|
ZONE_ASSIGNMENT_CONFIG: 0x0770,
|
|
51
54
|
CONFIGURATION: 0x0771,
|
|
52
55
|
PARTITION_ASSIGNMENT_CONFIG: 0x0772,
|
package/src/event-handler.js
CHANGED
|
@@ -104,6 +104,33 @@ export class EventHandler {
|
|
|
104
104
|
return status;
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
/**
|
|
108
|
+
* Update zone bypass state from 0x0820 notification
|
|
109
|
+
* @param {number} zoneNum - Zone number
|
|
110
|
+
* @param {boolean} bypassed - true if bypassed
|
|
111
|
+
*/
|
|
112
|
+
handleZoneBypass(zoneNum, bypassed) {
|
|
113
|
+
const existing = this.zoneStates.get(zoneNum);
|
|
114
|
+
if (existing) {
|
|
115
|
+
existing.bypassed = bypassed;
|
|
116
|
+
existing.rawStatus = bypassed
|
|
117
|
+
? (existing.rawStatus | 0x80)
|
|
118
|
+
: (existing.rawStatus & ~0x80);
|
|
119
|
+
existing.timestamp = new Date();
|
|
120
|
+
} else {
|
|
121
|
+
// Zone not yet seen — create minimal state
|
|
122
|
+
this.zoneStates.set(zoneNum, {
|
|
123
|
+
zoneNumber: zoneNum,
|
|
124
|
+
open: false, tamper: false, fault: false,
|
|
125
|
+
lowBattery: false, delinquency: false,
|
|
126
|
+
alarm: false, alarmInMemory: false,
|
|
127
|
+
bypassed: bypassed,
|
|
128
|
+
rawStatus: bypassed ? 0x80 : 0x00,
|
|
129
|
+
timestamp: new Date()
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
107
134
|
/**
|
|
108
135
|
* Process zone alarm notification
|
|
109
136
|
*/
|
|
@@ -237,6 +237,31 @@ rl.on('line', async (line) => {
|
|
|
237
237
|
break;
|
|
238
238
|
}
|
|
239
239
|
|
|
240
|
+
// ---- Zone Bypass ----
|
|
241
|
+
case 'bypass': {
|
|
242
|
+
const z = parseInt(args[0]);
|
|
243
|
+
if (!z) { console.log('Usage: bypass <zone> [part]'); break; }
|
|
244
|
+
const part = parseInt(args[1] || '1');
|
|
245
|
+
console.log(`Bypassing zone ${z}...`);
|
|
246
|
+
try {
|
|
247
|
+
const r = await client.bypassZone(z, part);
|
|
248
|
+
console.log(`Zone ${z}: BYPASSED${r.fromQuery ? ' (verified by query)' : ''}`);
|
|
249
|
+
} catch (e) { console.log(e.message); }
|
|
250
|
+
break;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
case 'unbypass': {
|
|
254
|
+
const z = parseInt(args[0]);
|
|
255
|
+
if (!z) { console.log('Usage: unbypass <zone> [part]'); break; }
|
|
256
|
+
const part = parseInt(args[1] || '1');
|
|
257
|
+
console.log(`Unbypassing zone ${z}...`);
|
|
258
|
+
try {
|
|
259
|
+
const r = await client.unbypassZone(z, part);
|
|
260
|
+
console.log(`Zone ${z}: UNBYPASSED${r.fromQuery ? ' (verified by query)' : ''}`);
|
|
261
|
+
} catch (e) { console.log(e.message); }
|
|
262
|
+
break;
|
|
263
|
+
}
|
|
264
|
+
|
|
240
265
|
// ---- Auth ----
|
|
241
266
|
case 'auth': {
|
|
242
267
|
const code = args[0] || config.masterCode;
|
|
@@ -261,6 +286,10 @@ rl.on('line', async (line) => {
|
|
|
261
286
|
night [part] [code] Arm partition in NIGHT mode
|
|
262
287
|
disarm [part] [code] Disarm partition
|
|
263
288
|
|
|
289
|
+
ZONE BYPASS
|
|
290
|
+
bypass <zone> [part] Bypass zone (exclude from monitoring)
|
|
291
|
+
unbypass <zone> [part] Unbypass zone (restore monitoring)
|
|
292
|
+
|
|
264
293
|
OTHER
|
|
265
294
|
auth [code] Authenticate with master/user code
|
|
266
295
|
help, ? Show this help
|
package/src/itv2-session.js
CHANGED
|
@@ -83,6 +83,7 @@ export const CMD = {
|
|
|
83
83
|
SYSTEM_CAPABILITIES: 0x0613,
|
|
84
84
|
PANEL_STATUS: 0x0614,
|
|
85
85
|
// Configuration commands (0x07xx)
|
|
86
|
+
CONFIG_EXIT: 0x0701,
|
|
86
87
|
CONFIG_ENTER: 0x0704,
|
|
87
88
|
CONFIG_ACCESS_CODE_WRAPPER: 0x0703,
|
|
88
89
|
// Module Status commands
|
|
@@ -1194,48 +1195,56 @@ export class ITv2Session {
|
|
|
1194
1195
|
}
|
|
1195
1196
|
|
|
1196
1197
|
/**
|
|
1197
|
-
* Build
|
|
1198
|
-
*
|
|
1199
|
-
* Wire format
|
|
1200
|
-
* OnHasAppSeqNum returns 0 — no CommandSequence in the payload.
|
|
1198
|
+
* Build ENTER_CONFIGURATION_MODE (0x0704)
|
|
1199
|
+
* Must be sent before config write commands like 0x074A.
|
|
1200
|
+
* Wire format: [Partition VarBytes][Type 1B][AccessCode ByteArray(bcd)][Mode 1B]
|
|
1201
1201
|
* @param {number} partition - Partition number (1-8)
|
|
1202
|
-
* @param {number}
|
|
1203
|
-
* @param {boolean} bypass - true to bypass, false to unbypass
|
|
1202
|
+
* @param {number} type - Programming type (3 = zone bypass)
|
|
1204
1203
|
* @param {string} accessCode - Master/user code (e.g., "1234")
|
|
1204
|
+
* @param {number} mode - Access mode (1 = user code)
|
|
1205
1205
|
*/
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1206
|
+
buildEnterConfigMode(partition, type, accessCode, mode = 1) {
|
|
1207
|
+
const codeStr = accessCode.toString();
|
|
1208
|
+
// Access code as raw digit values (each digit as one byte: "1234" → [0x01,0x02,0x03,0x04])
|
|
1209
|
+
const codeBytes = Buffer.from(codeStr.split('').map(d => parseInt(d, 10)));
|
|
1210
|
+
const payload = Buffer.concat([
|
|
1209
1211
|
this.encodeVarBytes(partition),
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
+
Buffer.from([type]),
|
|
1213
|
+
this.encodeByteArray(codeBytes),
|
|
1214
|
+
Buffer.from([mode]),
|
|
1212
1215
|
]);
|
|
1213
1216
|
|
|
1214
|
-
|
|
1215
|
-
|
|
1217
|
+
this.log(`[Session] ENTER_CONFIG_MODE: partition=${partition}, type=${type}, mode=${mode}`);
|
|
1218
|
+
return this.buildCommand(CMD.CONFIG_ENTER, payload);
|
|
1219
|
+
}
|
|
1216
1220
|
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1221
|
+
/**
|
|
1222
|
+
* Build EXIT_CONFIGURATION_MODE (0x0701)
|
|
1223
|
+
* Wire format: [Partition VarBytes]
|
|
1224
|
+
* @param {number} partition - Partition number (1-8)
|
|
1225
|
+
*/
|
|
1226
|
+
buildExitConfigMode(partition) {
|
|
1227
|
+
const payload = this.encodeVarBytes(partition);
|
|
1228
|
+
this.log(`[Session] EXIT_CONFIG_MODE: partition=${partition}`);
|
|
1229
|
+
return this.buildCommand(CMD.CONFIG_EXIT, payload);
|
|
1230
|
+
}
|
|
1220
1231
|
|
|
1232
|
+
/**
|
|
1233
|
+
* Build SINGLE_ZONE_BYPASS_WRITE (0x074A) — sent directly inside config mode.
|
|
1234
|
+
* Wire format: [Partition VarBytes][ZoneNumber VarBytes][BypassState 1B]
|
|
1235
|
+
* @param {number} partition - Partition number (1-8)
|
|
1236
|
+
* @param {number} zone - Zone number
|
|
1237
|
+
* @param {boolean} bypass - true to bypass, false to unbypass
|
|
1238
|
+
*/
|
|
1239
|
+
buildZoneBypass(partition, zone, bypass) {
|
|
1221
1240
|
const payload = Buffer.concat([
|
|
1222
|
-
this.
|
|
1223
|
-
|
|
1224
|
-
|
|
1241
|
+
this.encodeVarBytes(partition),
|
|
1242
|
+
this.encodeVarBytes(zone),
|
|
1243
|
+
Buffer.from([bypass ? 0x01 : 0x00]),
|
|
1225
1244
|
]);
|
|
1226
1245
|
|
|
1227
|
-
this.log(`[Session] ZONE_BYPASS
|
|
1228
|
-
|
|
1229
|
-
// 0x0703 has no AppSeqNum — send without appSequence
|
|
1230
|
-
this.localSequence = (this.localSequence + 1) & 0xFF;
|
|
1231
|
-
const message = buildHeader(
|
|
1232
|
-
this.localSequence,
|
|
1233
|
-
this.remoteSequence,
|
|
1234
|
-
CMD.CONFIG_ACCESS_CODE_WRAPPER,
|
|
1235
|
-
null, // No appSequence for 0x0703
|
|
1236
|
-
payload
|
|
1237
|
-
);
|
|
1238
|
-
return this.buildPacket(message);
|
|
1246
|
+
this.log(`[Session] ZONE_BYPASS 0x074A: partition=${partition}, zone=${zone}, bypass=${bypass}`);
|
|
1247
|
+
return this.buildCommand(CMD.SINGLE_ZONE_BYPASS_WRITE, payload);
|
|
1239
1248
|
}
|
|
1240
1249
|
|
|
1241
1250
|
/**
|