dsc-itv2-client 2.0.1 → 2.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/ITV2Client.js +211 -10
- package/src/constants.js +3 -0
- package/src/event-handler.js +27 -0
- package/src/examples/interactive-cli.js +69 -10
- package/src/itv2-session.js +56 -0
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.4",
|
|
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
|
@@ -218,49 +218,221 @@ export class ITV2Client extends EventEmitter {
|
|
|
218
218
|
this.emit('session:closed');
|
|
219
219
|
}
|
|
220
220
|
|
|
221
|
+
/**
|
|
222
|
+
* Send an arm/disarm command and wait for confirmation.
|
|
223
|
+
* Resolves when partition:arming or partition:exitDelay event arrives.
|
|
224
|
+
* Rejects on timeout (panel silently refused, e.g. open zones).
|
|
225
|
+
* @returns {Promise<object>} The arming/exitDelay event data
|
|
226
|
+
*/
|
|
227
|
+
async _sendArmCommand(packet, partition, expectedMode, timeout = 5000) {
|
|
228
|
+
if (!this._checkEstablished()) {
|
|
229
|
+
throw new Error('Session not established');
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Wait for arming/exitDelay confirmation
|
|
233
|
+
try {
|
|
234
|
+
return await new Promise((resolve, reject) => {
|
|
235
|
+
const timer = setTimeout(() => { cleanup(); reject(new Error('timeout')); }, timeout);
|
|
236
|
+
|
|
237
|
+
const onArming = (data) => {
|
|
238
|
+
if (data.partition === partition) { cleanup(); resolve(data); }
|
|
239
|
+
};
|
|
240
|
+
const onDelay = (data) => {
|
|
241
|
+
if (data.partition === partition) { cleanup(); resolve(data); }
|
|
242
|
+
};
|
|
243
|
+
const cleanup = () => {
|
|
244
|
+
clearTimeout(timer);
|
|
245
|
+
this.removeListener('partition:arming', onArming);
|
|
246
|
+
this.removeListener('partition:exitDelay', onDelay);
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
this.on('partition:arming', onArming);
|
|
250
|
+
this.on('partition:exitDelay', onDelay);
|
|
251
|
+
this._sendPacket(packet);
|
|
252
|
+
});
|
|
253
|
+
} catch (e) {
|
|
254
|
+
if (e.message !== 'timeout') throw e;
|
|
255
|
+
|
|
256
|
+
// No confirmation — query partition status to check current state
|
|
257
|
+
try {
|
|
258
|
+
const status = await this.queryPartitionStatus(partition);
|
|
259
|
+
if (status) {
|
|
260
|
+
const state = status.awayArmed ? 'AWAY' :
|
|
261
|
+
status.stayArmed ? 'STAY' :
|
|
262
|
+
status.nightArmed ? 'NIGHT' :
|
|
263
|
+
status.armed ? 'ARMED' : 'DISARMED';
|
|
264
|
+
|
|
265
|
+
// If already in the requested state, treat as success
|
|
266
|
+
if (state === expectedMode) {
|
|
267
|
+
return { partition, modeName: state, fromQuery: true };
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
} catch (_) {}
|
|
271
|
+
|
|
272
|
+
throw new Error('Arm/disarm not confirmed - panel may have open zones or troubles');
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
221
276
|
/**
|
|
222
277
|
* Arm partition in stay mode (mode 1)
|
|
278
|
+
* @returns {Promise} Resolves with arming confirmation, rejects if panel refuses
|
|
223
279
|
*/
|
|
224
280
|
armStay(partition, code) {
|
|
225
|
-
if (!this._checkEstablished()) return;
|
|
226
281
|
const packet = this.session.buildPartitionArm(partition, ITv2Session.ARM_MODE.STAY, code || this.masterCode);
|
|
227
|
-
this.
|
|
282
|
+
return this._sendArmCommand(packet, partition, 'STAY');
|
|
228
283
|
}
|
|
229
284
|
|
|
230
285
|
/**
|
|
231
286
|
* Arm partition in away mode (mode 2)
|
|
287
|
+
* @returns {Promise} Resolves with arming confirmation, rejects if panel refuses
|
|
232
288
|
*/
|
|
233
289
|
armAway(partition, code) {
|
|
234
|
-
if (!this._checkEstablished()) return;
|
|
235
290
|
const packet = this.session.buildPartitionArm(partition, ITv2Session.ARM_MODE.AWAY, code || this.masterCode);
|
|
236
|
-
this.
|
|
291
|
+
return this._sendArmCommand(packet, partition, 'AWAY');
|
|
237
292
|
}
|
|
238
293
|
|
|
239
294
|
/**
|
|
240
295
|
* Arm partition in night mode (mode 4)
|
|
296
|
+
* @returns {Promise} Resolves with arming confirmation, rejects if panel refuses
|
|
241
297
|
*/
|
|
242
298
|
armNight(partition, code) {
|
|
243
|
-
if (!this._checkEstablished()) return;
|
|
244
299
|
const packet = this.session.buildPartitionArm(partition, ITv2Session.ARM_MODE.NIGHT, code || this.masterCode);
|
|
245
|
-
this.
|
|
300
|
+
return this._sendArmCommand(packet, partition, 'NIGHT');
|
|
246
301
|
}
|
|
247
302
|
|
|
248
303
|
/**
|
|
249
304
|
* Arm partition with no entry delay (mode 3)
|
|
305
|
+
* @returns {Promise} Resolves with arming confirmation, rejects if panel refuses
|
|
250
306
|
*/
|
|
251
307
|
armNoEntryDelay(partition, code) {
|
|
252
|
-
if (!this._checkEstablished()) return;
|
|
253
308
|
const packet = this.session.buildPartitionArm(partition, ITv2Session.ARM_MODE.NO_ENTRY_DELAY, code || this.masterCode);
|
|
254
|
-
this.
|
|
309
|
+
return this._sendArmCommand(packet, partition, 'ARMED');
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Bypass a zone (exclude from monitoring).
|
|
314
|
+
* Uses config mode: 0x0704 → 0x074A → 0x0701.
|
|
315
|
+
* @param {number} zone - Zone number to bypass
|
|
316
|
+
* @param {number} [partition=1] - Partition number
|
|
317
|
+
* @param {string} [code] - User/master code (defaults to masterCode)
|
|
318
|
+
* @returns {Promise<object>} Resolves with bypass confirmation
|
|
319
|
+
*/
|
|
320
|
+
async bypassZone(zone, partition = 1, code) {
|
|
321
|
+
return this._sendBypassCommand(zone, partition, true, code);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Unbypass a zone (restore monitoring).
|
|
326
|
+
* @param {number} zone - Zone number to unbypass
|
|
327
|
+
* @param {number} [partition=1] - Partition number
|
|
328
|
+
* @param {string} [code] - User/master code (defaults to masterCode)
|
|
329
|
+
* @returns {Promise<object>} Resolves with unbypass confirmation
|
|
330
|
+
*/
|
|
331
|
+
async unbypassZone(zone, partition = 1, code) {
|
|
332
|
+
return this._sendBypassCommand(zone, partition, false, code);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Send a zone bypass/unbypass command.
|
|
337
|
+
* Flow: 0x0704 (enter config mode) → wait for 0x0702 → 0x074A (bypass) → 0x0701 (exit)
|
|
338
|
+
* @private
|
|
339
|
+
*/
|
|
340
|
+
async _sendBypassCommand(zone, partition, bypass, code, timeout = 10000) {
|
|
341
|
+
if (!this._checkEstablished()) {
|
|
342
|
+
throw new Error('Session not established');
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const accessCode = code || this.masterCode;
|
|
346
|
+
const action = bypass ? 'BYPASS' : 'UNBYPASS';
|
|
347
|
+
|
|
348
|
+
// Step 1: Enter config mode (type=3 = zone bypass, mode=1 = user code)
|
|
349
|
+
const enterPacket = this.session.buildEnterConfigMode(partition, 3, accessCode, 1);
|
|
350
|
+
this._sendPacket(enterPacket);
|
|
351
|
+
|
|
352
|
+
// Step 2: Wait for 0x0702 config status confirmation (COMMAND_RESPONSE is intermediate)
|
|
353
|
+
await this._waitForConfigReady(5000);
|
|
354
|
+
|
|
355
|
+
// Step 3: Send zone bypass write
|
|
356
|
+
const bypassPacket = this.session.buildZoneBypass(partition, zone, bypass);
|
|
357
|
+
|
|
358
|
+
try {
|
|
359
|
+
const result = await new Promise((resolve, reject) => {
|
|
360
|
+
const timer = setTimeout(() => { cleanup(); reject(new Error('timeout')); }, timeout);
|
|
361
|
+
|
|
362
|
+
const onBypass = (data) => {
|
|
363
|
+
if (data.zone === zone) { cleanup(); resolve(data); }
|
|
364
|
+
};
|
|
365
|
+
const cleanup = () => {
|
|
366
|
+
clearTimeout(timer);
|
|
367
|
+
this.removeListener('zone:bypass', onBypass);
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
this.on('zone:bypass', onBypass);
|
|
371
|
+
this._sendPacket(bypassPacket);
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
// Step 4: Exit config mode
|
|
375
|
+
const exitPacket = this.session.buildExitConfigMode(partition);
|
|
376
|
+
this._sendPacket(exitPacket);
|
|
377
|
+
|
|
378
|
+
return result;
|
|
379
|
+
} catch (e) {
|
|
380
|
+
// Always try to exit config mode
|
|
381
|
+
try {
|
|
382
|
+
const exitPacket = this.session.buildExitConfigMode(partition);
|
|
383
|
+
this._sendPacket(exitPacket);
|
|
384
|
+
} catch (_) {}
|
|
385
|
+
|
|
386
|
+
if (e.message !== 'timeout') throw e;
|
|
387
|
+
|
|
388
|
+
// No 0x0820 confirmation — query zone status to check
|
|
389
|
+
try {
|
|
390
|
+
const zones = await this.queryZoneStatus();
|
|
391
|
+
const zoneStatus = zones.find(z => z.zoneNumber === zone);
|
|
392
|
+
if (zoneStatus && zoneStatus.bypassed === bypass) {
|
|
393
|
+
return { zone, bypassed: bypass, fromQuery: true };
|
|
394
|
+
}
|
|
395
|
+
} catch (_) {}
|
|
396
|
+
|
|
397
|
+
throw new Error(`Zone ${zone} ${action} not confirmed - check access code and zone number`);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Wait for 0x0702 (config status) notification confirming config mode is ready.
|
|
403
|
+
* COMMAND_RESPONSE to 0x0704 is an intermediate ack — the real confirmation is 0x0702.
|
|
404
|
+
* @private
|
|
405
|
+
*/
|
|
406
|
+
_waitForConfigReady(timeout = 10000) {
|
|
407
|
+
return new Promise((resolve) => {
|
|
408
|
+
const timer = setTimeout(() => { cleanup(); resolve(); }, timeout);
|
|
409
|
+
const origRoute = this._routePacket.bind(this);
|
|
410
|
+
|
|
411
|
+
const interceptor = (parsed, skipAck) => {
|
|
412
|
+
if (parsed.command === 0x0702) {
|
|
413
|
+
cleanup();
|
|
414
|
+
this._logMinimal('[Config] Config mode ready (0x0702)');
|
|
415
|
+
resolve();
|
|
416
|
+
}
|
|
417
|
+
origRoute(parsed, skipAck);
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
const cleanup = () => {
|
|
421
|
+
clearTimeout(timer);
|
|
422
|
+
this._routePacket = origRoute;
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
this._routePacket = interceptor;
|
|
426
|
+
});
|
|
255
427
|
}
|
|
256
428
|
|
|
257
429
|
/**
|
|
258
430
|
* Disarm partition
|
|
431
|
+
* @returns {Promise} Resolves with disarm confirmation, rejects if panel refuses
|
|
259
432
|
*/
|
|
260
433
|
disarm(partition, code) {
|
|
261
|
-
if (!this._checkEstablished()) return;
|
|
262
434
|
const packet = this.session.buildPartitionDisarm(partition, code || this.masterCode);
|
|
263
|
-
this.
|
|
435
|
+
return this._sendArmCommand(packet, partition, 'DISARMED');
|
|
264
436
|
}
|
|
265
437
|
|
|
266
438
|
// ==================== Status Query Methods ====================
|
|
@@ -578,6 +750,9 @@ export class ITV2Client extends EventEmitter {
|
|
|
578
750
|
case CMD.NOTIFICATION_PARTITION_TROUBLE:
|
|
579
751
|
this._handlePartitionTroubleNotification(parsed);
|
|
580
752
|
break;
|
|
753
|
+
case CMD.SINGLE_ZONE_BYPASS_STATUS:
|
|
754
|
+
this._handleZoneBypassNotification(parsed);
|
|
755
|
+
break;
|
|
581
756
|
case CMD.MULTIPLE_MESSAGE:
|
|
582
757
|
this._handleMultipleMessagePacket(parsed);
|
|
583
758
|
break;
|
|
@@ -1254,6 +1429,32 @@ export class ITV2Client extends EventEmitter {
|
|
|
1254
1429
|
|
|
1255
1430
|
// ==================== Notification Handlers ====================
|
|
1256
1431
|
|
|
1432
|
+
_handleZoneBypassNotification(parsed) {
|
|
1433
|
+
// 0x0820 SingleZoneBypassStatus (from neohub):
|
|
1434
|
+
// [CompactInt:ZoneNumber][BypassState:1B] (0=not bypassed, 1=bypassed)
|
|
1435
|
+
const fullPayload = this._reconstructPayload(parsed);
|
|
1436
|
+
if (fullPayload && fullPayload.length >= 3) {
|
|
1437
|
+
try {
|
|
1438
|
+
let offset = 0;
|
|
1439
|
+
const zone = ITv2Session.decodeVarBytes(fullPayload, offset);
|
|
1440
|
+
offset += zone.bytesRead;
|
|
1441
|
+
const bypassed = fullPayload[offset] === 1;
|
|
1442
|
+
|
|
1443
|
+
this._logMinimal(`[Zone ${zone.value}] ${bypassed ? 'BYPASSED' : 'UNBYPASSED'}`);
|
|
1444
|
+
|
|
1445
|
+
// Update cached zone state
|
|
1446
|
+
if (this.eventHandler) {
|
|
1447
|
+
this.eventHandler.handleZoneBypass(zone.value, bypassed);
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
this.emit('zone:bypass', { zone: zone.value, bypassed });
|
|
1451
|
+
} catch (e) {
|
|
1452
|
+
this._log(`[Bypass Status] Parse error: ${e.message}`);
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
this._ack();
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1257
1458
|
_handleExitDelayNotification(parsed) {
|
|
1258
1459
|
// 0x0230 NotificationExitDelay (from neohub):
|
|
1259
1460
|
// [CompactInt:Partition][DelayFlags:1B][CompactInt:DurationInSeconds]
|
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
|
*/
|
|
@@ -76,6 +76,23 @@ client.on('partition:ready', ({ partition, isReady }) => {
|
|
|
76
76
|
prompt();
|
|
77
77
|
});
|
|
78
78
|
|
|
79
|
+
client.on('zone:bypass', ({ zone, bypassed }) => {
|
|
80
|
+
console.log(`\n Zone ${zone}: ${bypassed ? 'BYPASSED' : 'UNBYPASSED'}`);
|
|
81
|
+
prompt();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
client.on('partition:exitDelay', ({ partition, duration, active }) => {
|
|
85
|
+
if (active) console.log(`\n Partition ${partition}: exit delay ${duration}s`);
|
|
86
|
+
prompt();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
client.on('trouble:detail', (troubles) => {
|
|
90
|
+
for (const t of troubles) {
|
|
91
|
+
console.log(`\n Trouble: ${t.deviceTypeName} #${t.deviceNumber}: ${t.troubleTypeName} (${t.troubleStateName})`);
|
|
92
|
+
}
|
|
93
|
+
prompt();
|
|
94
|
+
});
|
|
95
|
+
|
|
79
96
|
// command:error events are protocol-level noise (intermediate acks), not actionable
|
|
80
97
|
|
|
81
98
|
client.on('error', (err) => {
|
|
@@ -145,13 +162,14 @@ rl.on('line', async (line) => {
|
|
|
145
162
|
|
|
146
163
|
case 'trouble':
|
|
147
164
|
case 't': {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
165
|
+
// Trouble detail comes from 0x0823 notifications (not queryable)
|
|
166
|
+
// Check partition status for trouble flag
|
|
167
|
+
const ps = await client.queryPartitionStatus(1);
|
|
168
|
+
if (ps && ps.trouble) {
|
|
169
|
+
console.log('Partition 1 has active troubles');
|
|
170
|
+
console.log('(Detailed trouble info arrives via 0x0823 notifications - see trouble:detail event)');
|
|
151
171
|
} else {
|
|
152
|
-
|
|
153
|
-
console.log(` Device ${t.deviceType}: troubles [${t.troubles.join(', ')}]`);
|
|
154
|
-
}
|
|
172
|
+
console.log('No active troubles on partition 1');
|
|
155
173
|
}
|
|
156
174
|
break;
|
|
157
175
|
}
|
|
@@ -179,7 +197,10 @@ rl.on('line', async (line) => {
|
|
|
179
197
|
const part = parseInt(args[0] || '1');
|
|
180
198
|
const code = args[1] || config.masterCode;
|
|
181
199
|
console.log(`Arming partition ${part} STAY...`);
|
|
182
|
-
|
|
200
|
+
try {
|
|
201
|
+
const r = await client.armStay(part, code);
|
|
202
|
+
console.log(r.duration ? `Exit delay: ${r.duration}s` : `Partition ${part}: ${r.modeName}`);
|
|
203
|
+
} catch (e) { console.log(e.message); }
|
|
183
204
|
break;
|
|
184
205
|
}
|
|
185
206
|
|
|
@@ -187,7 +208,10 @@ rl.on('line', async (line) => {
|
|
|
187
208
|
const part = parseInt(args[0] || '1');
|
|
188
209
|
const code = args[1] || config.masterCode;
|
|
189
210
|
console.log(`Arming partition ${part} AWAY...`);
|
|
190
|
-
|
|
211
|
+
try {
|
|
212
|
+
const r = await client.armAway(part, code);
|
|
213
|
+
console.log(r.duration ? `Exit delay: ${r.duration}s` : `Partition ${part}: ${r.modeName}`);
|
|
214
|
+
} catch (e) { console.log(e.message); }
|
|
191
215
|
break;
|
|
192
216
|
}
|
|
193
217
|
|
|
@@ -195,7 +219,10 @@ rl.on('line', async (line) => {
|
|
|
195
219
|
const part = parseInt(args[0] || '1');
|
|
196
220
|
const code = args[1] || config.masterCode;
|
|
197
221
|
console.log(`Arming partition ${part} NIGHT...`);
|
|
198
|
-
|
|
222
|
+
try {
|
|
223
|
+
const r = await client.armNight(part, code);
|
|
224
|
+
console.log(r.duration ? `Exit delay: ${r.duration}s` : `Partition ${part}: ${r.modeName}`);
|
|
225
|
+
} catch (e) { console.log(e.message); }
|
|
199
226
|
break;
|
|
200
227
|
}
|
|
201
228
|
|
|
@@ -203,7 +230,35 @@ rl.on('line', async (line) => {
|
|
|
203
230
|
const part = parseInt(args[0] || '1');
|
|
204
231
|
const code = args[1] || config.masterCode;
|
|
205
232
|
console.log(`Disarming partition ${part}...`);
|
|
206
|
-
|
|
233
|
+
try {
|
|
234
|
+
const r = await client.disarm(part, code);
|
|
235
|
+
console.log(`Partition ${part}: ${r.modeName}`);
|
|
236
|
+
} catch (e) { console.log(e.message); }
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
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); }
|
|
207
262
|
break;
|
|
208
263
|
}
|
|
209
264
|
|
|
@@ -231,6 +286,10 @@ rl.on('line', async (line) => {
|
|
|
231
286
|
night [part] [code] Arm partition in NIGHT mode
|
|
232
287
|
disarm [part] [code] Disarm partition
|
|
233
288
|
|
|
289
|
+
ZONE BYPASS
|
|
290
|
+
bypass <zone> [part] Bypass zone (exclude from monitoring)
|
|
291
|
+
unbypass <zone> [part] Unbypass zone (restore monitoring)
|
|
292
|
+
|
|
234
293
|
OTHER
|
|
235
294
|
auth [code] Authenticate with master/user code
|
|
236
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
|
|
@@ -97,6 +98,8 @@ export const CMD = {
|
|
|
97
98
|
BUS_STATUS: 0x0816,
|
|
98
99
|
TROUBLE_DETAIL: 0x0823,
|
|
99
100
|
DOOR_CHIME_STATUS: 0x0819,
|
|
101
|
+
SINGLE_ZONE_BYPASS_STATUS: 0x0820,
|
|
102
|
+
SINGLE_ZONE_BYPASS_WRITE: 0x074A,
|
|
100
103
|
MULTIPLE_MESSAGE: 0x0623,
|
|
101
104
|
// Module Control commands
|
|
102
105
|
PARTITION_ARM: 0x0900,
|
|
@@ -1191,6 +1194,59 @@ export class ITv2Session {
|
|
|
1191
1194
|
return this.buildCommand(CMD.PARTITION_DISARM, payload);
|
|
1192
1195
|
}
|
|
1193
1196
|
|
|
1197
|
+
/**
|
|
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
|
+
* @param {number} partition - Partition number (1-8)
|
|
1202
|
+
* @param {number} type - Programming type (3 = zone bypass)
|
|
1203
|
+
* @param {string} accessCode - Master/user code (e.g., "1234")
|
|
1204
|
+
* @param {number} mode - Access mode (1 = user code)
|
|
1205
|
+
*/
|
|
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([
|
|
1211
|
+
this.encodeVarBytes(partition),
|
|
1212
|
+
Buffer.from([type]),
|
|
1213
|
+
this.encodeByteArray(codeBytes),
|
|
1214
|
+
Buffer.from([mode]),
|
|
1215
|
+
]);
|
|
1216
|
+
|
|
1217
|
+
this.log(`[Session] ENTER_CONFIG_MODE: partition=${partition}, type=${type}, mode=${mode}`);
|
|
1218
|
+
return this.buildCommand(CMD.CONFIG_ENTER, payload);
|
|
1219
|
+
}
|
|
1220
|
+
|
|
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
|
+
}
|
|
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) {
|
|
1240
|
+
const payload = Buffer.concat([
|
|
1241
|
+
this.encodeVarBytes(partition),
|
|
1242
|
+
this.encodeVarBytes(zone),
|
|
1243
|
+
Buffer.from([bypass ? 0x01 : 0x00]),
|
|
1244
|
+
]);
|
|
1245
|
+
|
|
1246
|
+
this.log(`[Session] ZONE_BYPASS 0x074A: partition=${partition}, zone=${zone}, bypass=${bypass}`);
|
|
1247
|
+
return this.buildCommand(CMD.SINGLE_ZONE_BYPASS_WRITE, payload);
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1194
1250
|
/**
|
|
1195
1251
|
* Build command output activation (PGM trigger)
|
|
1196
1252
|
* @param {number} partition - Partition number
|