dsc-itv2-client 1.0.7
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 +342 -0
- package/package.json +46 -0
- package/src/ITV2Client.js +502 -0
- package/src/constants.js +106 -0
- package/src/event-handler.js +323 -0
- package/src/examples/README.md +287 -0
- package/src/examples/arm-disarm-example.js +152 -0
- package/src/examples/basic-monitoring.js +82 -0
- package/src/examples/interactive-cli.js +1033 -0
- package/src/index.js +17 -0
- package/src/itv2-crypto.js +310 -0
- package/src/itv2-session.js +1069 -0
- package/src/response-parsers.js +185 -0
- package/src/utils.js +240 -0
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Event Handler for ITV2 Protocol
|
|
3
|
+
*
|
|
4
|
+
* Manages zone and partition state based on panel notifications.
|
|
5
|
+
* The DSC panel uses an event-driven model - status arrives via
|
|
6
|
+
* unsolicited notifications, not request-responses.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export class EventHandler {
|
|
10
|
+
constructor(log = console.log) {
|
|
11
|
+
this.log = log;
|
|
12
|
+
this.listeners = new Map();
|
|
13
|
+
this.zoneStates = new Map();
|
|
14
|
+
this.partitionStates = new Map();
|
|
15
|
+
this.systemInfo = {};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// ============ Event Listener Management ============
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Register an event listener
|
|
22
|
+
* @param {string} event - Event name (e.g., 'zone:status', 'partition:armed')
|
|
23
|
+
* @param {function} callback - Callback function
|
|
24
|
+
*/
|
|
25
|
+
on(event, callback) {
|
|
26
|
+
if (!this.listeners.has(event)) {
|
|
27
|
+
this.listeners.set(event, []);
|
|
28
|
+
}
|
|
29
|
+
this.listeners.get(event).push(callback);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Remove an event listener
|
|
34
|
+
*/
|
|
35
|
+
off(event, callback) {
|
|
36
|
+
const callbacks = this.listeners.get(event);
|
|
37
|
+
if (callbacks) {
|
|
38
|
+
const index = callbacks.indexOf(callback);
|
|
39
|
+
if (index !== -1) {
|
|
40
|
+
callbacks.splice(index, 1);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Emit an event to all listeners
|
|
47
|
+
*/
|
|
48
|
+
emit(event, data) {
|
|
49
|
+
const callbacks = this.listeners.get(event) || [];
|
|
50
|
+
callbacks.forEach(cb => {
|
|
51
|
+
try {
|
|
52
|
+
cb(data);
|
|
53
|
+
} catch (err) {
|
|
54
|
+
this.log(`[EventHandler] Error in listener for ${event}: ${err.message}`);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ============ Zone Status Handling ============
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Process zone status notification from panel
|
|
63
|
+
* @param {number} zoneNum - Zone number
|
|
64
|
+
* @param {number} statusByte - Status byte from panel
|
|
65
|
+
* @returns {object} Parsed zone status
|
|
66
|
+
*/
|
|
67
|
+
handleZoneStatus(zoneNum, statusByte) {
|
|
68
|
+
const status = {
|
|
69
|
+
zoneNumber: zoneNum,
|
|
70
|
+
open: !!(statusByte & 0x01),
|
|
71
|
+
tamper: !!(statusByte & 0x02),
|
|
72
|
+
fault: !!(statusByte & 0x04),
|
|
73
|
+
bypassed: !!(statusByte & 0x08),
|
|
74
|
+
trouble: !!(statusByte & 0x10),
|
|
75
|
+
alarm: !!(statusByte & 0x20),
|
|
76
|
+
lowBattery: !!(statusByte & 0x40),
|
|
77
|
+
supervisionLoss: !!(statusByte & 0x80),
|
|
78
|
+
rawStatus: statusByte,
|
|
79
|
+
timestamp: new Date()
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const previousState = this.zoneStates.get(zoneNum);
|
|
83
|
+
this.zoneStates.set(zoneNum, status);
|
|
84
|
+
|
|
85
|
+
// Log change
|
|
86
|
+
this.log(`[Zone ${zoneNum}] Status: ${status.open ? 'OPEN' : 'CLOSED'} ` +
|
|
87
|
+
`${status.bypassed ? '[BYPASSED]' : ''} ${status.alarm ? '[ALARM]' : ''} ` +
|
|
88
|
+
`${status.trouble ? '[TROUBLE]' : ''}`);
|
|
89
|
+
|
|
90
|
+
// Emit events
|
|
91
|
+
this.emit('zone:status', status);
|
|
92
|
+
|
|
93
|
+
if (previousState && previousState.open !== status.open) {
|
|
94
|
+
this.emit('zone:change', { zone: zoneNum, wasOpen: previousState.open, isOpen: status.open });
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (status.alarm) {
|
|
98
|
+
this.emit('zone:alarm', status);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return status;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Process zone alarm notification
|
|
106
|
+
*/
|
|
107
|
+
handleZoneAlarm(zoneNum, alarmType) {
|
|
108
|
+
const alarm = {
|
|
109
|
+
zoneNumber: zoneNum,
|
|
110
|
+
alarmType: alarmType,
|
|
111
|
+
timestamp: new Date()
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
this.log(`[Zone ${zoneNum}] ALARM! Type: ${alarmType}`);
|
|
115
|
+
this.emit('zone:alarm', alarm);
|
|
116
|
+
|
|
117
|
+
return alarm;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Get current zone state
|
|
122
|
+
*/
|
|
123
|
+
getZoneState(zoneNum) {
|
|
124
|
+
return this.zoneStates.get(zoneNum);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Get all zones with status
|
|
129
|
+
*/
|
|
130
|
+
getAllZones() {
|
|
131
|
+
return Array.from(this.zoneStates.values());
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Get all open zones
|
|
136
|
+
*/
|
|
137
|
+
getOpenZones() {
|
|
138
|
+
return this.getAllZones().filter(z => z.open);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ============ Partition Status Handling ============
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Process partition arming notification
|
|
145
|
+
* @param {number} partitionNum - Partition number
|
|
146
|
+
* @param {number} armedState - Armed state code
|
|
147
|
+
*/
|
|
148
|
+
handlePartitionArming(partitionNum, armedState) {
|
|
149
|
+
const armedStates = {
|
|
150
|
+
0x00: 'DISARMED',
|
|
151
|
+
0x01: 'ARMED_STAY',
|
|
152
|
+
0x02: 'ARMED_AWAY',
|
|
153
|
+
0x03: 'ARMED_NIGHT',
|
|
154
|
+
0x04: 'ARMED_NO_ENTRY',
|
|
155
|
+
0x05: 'ARMED_FORCE',
|
|
156
|
+
0x06: 'EXIT_DELAY',
|
|
157
|
+
0x07: 'ENTRY_DELAY'
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const status = {
|
|
161
|
+
partitionNumber: partitionNum,
|
|
162
|
+
armedState: armedState,
|
|
163
|
+
armedStateName: armedStates[armedState] || `UNKNOWN(${armedState})`,
|
|
164
|
+
isArmed: armedState >= 0x01 && armedState <= 0x05,
|
|
165
|
+
inDelay: armedState === 0x06 || armedState === 0x07,
|
|
166
|
+
timestamp: new Date()
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const previousState = this.partitionStates.get(partitionNum);
|
|
170
|
+
this.partitionStates.set(partitionNum, status);
|
|
171
|
+
|
|
172
|
+
this.log(`[Partition ${partitionNum}] ${status.armedStateName}`);
|
|
173
|
+
this.emit('partition:status', status);
|
|
174
|
+
|
|
175
|
+
if (previousState?.armedState !== status.armedState) {
|
|
176
|
+
this.emit('partition:change', {
|
|
177
|
+
partition: partitionNum,
|
|
178
|
+
previousState: previousState?.armedStateName,
|
|
179
|
+
newState: status.armedStateName
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return status;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Process partition ready status
|
|
188
|
+
*/
|
|
189
|
+
handlePartitionReady(partitionNum, isReady) {
|
|
190
|
+
const existing = this.partitionStates.get(partitionNum) || {};
|
|
191
|
+
const status = {
|
|
192
|
+
...existing,
|
|
193
|
+
partitionNumber: partitionNum,
|
|
194
|
+
ready: isReady,
|
|
195
|
+
timestamp: new Date()
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
this.partitionStates.set(partitionNum, status);
|
|
199
|
+
this.log(`[Partition ${partitionNum}] ${isReady ? 'READY' : 'NOT READY'}`);
|
|
200
|
+
this.emit('partition:ready', status);
|
|
201
|
+
|
|
202
|
+
return status;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Process partition trouble status
|
|
207
|
+
*/
|
|
208
|
+
handlePartitionTrouble(partitionNum, troubleFlags) {
|
|
209
|
+
const existing = this.partitionStates.get(partitionNum) || {};
|
|
210
|
+
const status = {
|
|
211
|
+
...existing,
|
|
212
|
+
partitionNumber: partitionNum,
|
|
213
|
+
trouble: troubleFlags !== 0,
|
|
214
|
+
troubleFlags: troubleFlags,
|
|
215
|
+
timestamp: new Date()
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
this.partitionStates.set(partitionNum, status);
|
|
219
|
+
if (troubleFlags !== 0) {
|
|
220
|
+
this.log(`[Partition ${partitionNum}] TROUBLE: 0x${troubleFlags.toString(16)}`);
|
|
221
|
+
this.emit('partition:trouble', status);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return status;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Get current partition state
|
|
229
|
+
*/
|
|
230
|
+
getPartitionState(partitionNum) {
|
|
231
|
+
return this.partitionStates.get(partitionNum);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Get all partitions with status
|
|
236
|
+
*/
|
|
237
|
+
getAllPartitions() {
|
|
238
|
+
return Array.from(this.partitionStates.values());
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// ============ System Info ============
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Store system capabilities
|
|
245
|
+
*/
|
|
246
|
+
setSystemCapabilities(caps) {
|
|
247
|
+
this.systemInfo.capabilities = caps;
|
|
248
|
+
this.emit('system:capabilities', caps);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Get system info
|
|
253
|
+
*/
|
|
254
|
+
getSystemInfo() {
|
|
255
|
+
return this.systemInfo;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// ============ Status Summary ============
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Get complete system status summary
|
|
262
|
+
*/
|
|
263
|
+
getStatusSummary() {
|
|
264
|
+
const zones = this.getAllZones();
|
|
265
|
+
const partitions = this.getAllPartitions();
|
|
266
|
+
|
|
267
|
+
return {
|
|
268
|
+
zones: {
|
|
269
|
+
total: zones.length,
|
|
270
|
+
open: zones.filter(z => z.open).length,
|
|
271
|
+
bypassed: zones.filter(z => z.bypassed).length,
|
|
272
|
+
alarm: zones.filter(z => z.alarm).length,
|
|
273
|
+
trouble: zones.filter(z => z.trouble).length,
|
|
274
|
+
list: zones
|
|
275
|
+
},
|
|
276
|
+
partitions: {
|
|
277
|
+
total: partitions.length,
|
|
278
|
+
armed: partitions.filter(p => p.isArmed).length,
|
|
279
|
+
ready: partitions.filter(p => p.ready).length,
|
|
280
|
+
list: partitions
|
|
281
|
+
},
|
|
282
|
+
lastUpdate: new Date()
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Print status summary to console
|
|
288
|
+
*/
|
|
289
|
+
printStatus() {
|
|
290
|
+
const summary = this.getStatusSummary();
|
|
291
|
+
|
|
292
|
+
console.log('\n========================================');
|
|
293
|
+
console.log('SYSTEM STATUS SUMMARY');
|
|
294
|
+
console.log('========================================');
|
|
295
|
+
|
|
296
|
+
console.log(`\nZones (${summary.zones.total} total):`);
|
|
297
|
+
if (summary.zones.list.length === 0) {
|
|
298
|
+
console.log(' No zone status received yet');
|
|
299
|
+
} else {
|
|
300
|
+
summary.zones.list.forEach(z => {
|
|
301
|
+
const flags = [];
|
|
302
|
+
if (z.open) flags.push('OPEN');
|
|
303
|
+
if (z.bypassed) flags.push('BYPASSED');
|
|
304
|
+
if (z.alarm) flags.push('ALARM');
|
|
305
|
+
if (z.trouble) flags.push('TROUBLE');
|
|
306
|
+
console.log(` Zone ${z.zoneNumber}: ${flags.length > 0 ? flags.join(', ') : 'OK'}`);
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
console.log(`\nPartitions (${summary.partitions.total} total):`);
|
|
311
|
+
if (summary.partitions.list.length === 0) {
|
|
312
|
+
console.log(' No partition status received yet');
|
|
313
|
+
} else {
|
|
314
|
+
summary.partitions.list.forEach(p => {
|
|
315
|
+
console.log(` Partition ${p.partitionNumber}: ${p.armedStateName || 'UNKNOWN'} ${p.ready ? '[READY]' : '[NOT READY]'}`);
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
console.log('========================================\n');
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
export default EventHandler;
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
# DSC ITV2 Client - Examples
|
|
2
|
+
|
|
3
|
+
This directory contains example applications demonstrating how to use the `dsc-itv2-client` library.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
### 1. Basic Zone Monitoring
|
|
8
|
+
|
|
9
|
+
The simplest example - just monitor when zones open and close:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm run example:basic
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
**Code:**
|
|
16
|
+
```javascript
|
|
17
|
+
import { ITV2Client } from 'dsc-itv2-client';
|
|
18
|
+
|
|
19
|
+
const client = new ITV2Client({
|
|
20
|
+
integrationId: '250228754343',
|
|
21
|
+
accessCode: '28754543'
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
client.on('zone:open', (zone) => console.log(`Zone ${zone} opened!`));
|
|
25
|
+
client.on('zone:closed', (zone) => console.log(`Zone ${zone} closed!`));
|
|
26
|
+
|
|
27
|
+
await client.start();
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### 2. Arm/Disarm Control
|
|
31
|
+
|
|
32
|
+
Interactive example for controlling partitions:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npm run example:control
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Features:
|
|
39
|
+
- Arm partition in STAY or AWAY mode
|
|
40
|
+
- Disarm partition
|
|
41
|
+
- View current status
|
|
42
|
+
- Real-time zone monitoring
|
|
43
|
+
|
|
44
|
+
### 3. Full Interactive CLI (server.js)
|
|
45
|
+
|
|
46
|
+
The original full-featured CLI with all commands:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
npm run server
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Library Usage
|
|
53
|
+
|
|
54
|
+
### Installation
|
|
55
|
+
|
|
56
|
+
```javascript
|
|
57
|
+
import { ITV2Client } from './src/index.js';
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Configuration
|
|
61
|
+
|
|
62
|
+
```javascript
|
|
63
|
+
const client = new ITV2Client({
|
|
64
|
+
integrationId: '250228754343', // Your panel's integration ID
|
|
65
|
+
accessCode: '28754543', // 8-digit code (Type 1) or 32-hex (Type 2)
|
|
66
|
+
masterCode: '5555', // Master/user code for arm/disarm
|
|
67
|
+
port: 3073, // UDP port (default: 3073)
|
|
68
|
+
encryptionType: null, // null=auto-detect, 1=Type 1, 2=Type 2
|
|
69
|
+
logLevel: 'minimal' // 'silent', 'minimal', 'verbose'
|
|
70
|
+
});
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
#### Log Levels
|
|
74
|
+
|
|
75
|
+
- **'silent'** - No console output
|
|
76
|
+
- **'minimal'** (default) - Key events only (session established, zone changes)
|
|
77
|
+
- **'verbose'** - Full protocol details (packets, crypto, handshake steps)
|
|
78
|
+
|
|
79
|
+
Use `LOG_LEVEL=verbose` environment variable for debugging.
|
|
80
|
+
|
|
81
|
+
#### Type 1 vs Type 2 Encryption
|
|
82
|
+
|
|
83
|
+
**Type 1 (most common):**
|
|
84
|
+
- Uses 8-digit access code: `'28754543'`
|
|
85
|
+
- 48-byte initializer exchange
|
|
86
|
+
- Asymmetric key derivation (panel uses Integration ID, client uses Access Code)
|
|
87
|
+
- Auto-detected from panel's REQUEST_ACCESS
|
|
88
|
+
|
|
89
|
+
**Type 2 (less common):**
|
|
90
|
+
- Uses 32-hex access code: `'25022875250228752502287525022875'`
|
|
91
|
+
- 16-byte initializer exchange
|
|
92
|
+
- Symmetric key derivation (same transform for both sides)
|
|
93
|
+
- Auto-detected from panel's REQUEST_ACCESS
|
|
94
|
+
|
|
95
|
+
**Auto-Detection (Recommended):**
|
|
96
|
+
```javascript
|
|
97
|
+
// Leave encryptionType as null (default)
|
|
98
|
+
const client = new ITV2Client({
|
|
99
|
+
integrationId: '250228754343',
|
|
100
|
+
accessCode: '28754543' // Will auto-detect Type 1 or Type 2
|
|
101
|
+
});
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
**Force Type 2:**
|
|
105
|
+
```javascript
|
|
106
|
+
const client = new ITV2Client({
|
|
107
|
+
integrationId: '250228754343',
|
|
108
|
+
accessCode: '25022875250228752502287525022875', // 32-hex for Type 2
|
|
109
|
+
encryptionType: 2 // Force Type 2
|
|
110
|
+
});
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Events
|
|
114
|
+
|
|
115
|
+
#### Session Events
|
|
116
|
+
|
|
117
|
+
```javascript
|
|
118
|
+
client.on('listening', (address) => {
|
|
119
|
+
// UDP server is listening
|
|
120
|
+
console.log(`Listening on ${address.port}`);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
client.on('session:connecting', () => {
|
|
124
|
+
// Panel has initiated connection
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
client.on('session:established', (info) => {
|
|
128
|
+
// Handshake complete, session ready
|
|
129
|
+
// info = { encryptionType, sendKey, recvKey }
|
|
130
|
+
console.log(`Connected! Using Type ${info.encryptionType} encryption`);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
client.on('session:closed', () => {
|
|
134
|
+
// Session ended
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
client.on('session:error', (error) => {
|
|
138
|
+
// Handshake or protocol error
|
|
139
|
+
console.error(error);
|
|
140
|
+
});
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
#### Zone Events
|
|
144
|
+
|
|
145
|
+
```javascript
|
|
146
|
+
client.on('zone:open', (zoneNumber) => {
|
|
147
|
+
// Zone opened (door/window opened, motion detected, etc.)
|
|
148
|
+
console.log(`Zone ${zoneNumber} is open`);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
client.on('zone:closed', (zoneNumber) => {
|
|
152
|
+
// Zone closed/restored
|
|
153
|
+
console.log(`Zone ${zoneNumber} is closed`);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
client.on('zone:status', (zoneNumber, status) => {
|
|
157
|
+
// Full zone status with all flags
|
|
158
|
+
// status = { open, tamper, fault, lowBattery, delinquency, alarm, alarmInMemory, bypassed }
|
|
159
|
+
if (status.alarm) {
|
|
160
|
+
console.log(`ALARM on zone ${zoneNumber}!`);
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
#### Partition Events
|
|
166
|
+
|
|
167
|
+
```javascript
|
|
168
|
+
client.on('partition:armed', (partition, mode) => {
|
|
169
|
+
// Partition armed (mode: 'stay', 'away', etc.)
|
|
170
|
+
console.log(`Partition ${partition} armed in ${mode} mode`);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
client.on('partition:disarmed', (partition) => {
|
|
174
|
+
// Partition disarmed
|
|
175
|
+
console.log(`Partition ${partition} disarmed`);
|
|
176
|
+
});
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
#### Error Events
|
|
180
|
+
|
|
181
|
+
```javascript
|
|
182
|
+
client.on('error', (error) => {
|
|
183
|
+
// General errors
|
|
184
|
+
console.error('Error:', error.message);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
client.on('command:error', (error) => {
|
|
188
|
+
// Command rejected by panel
|
|
189
|
+
// error = { code, message, rawData }
|
|
190
|
+
console.log(`Panel rejected command: ${error.message}`);
|
|
191
|
+
});
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### Control Methods
|
|
195
|
+
|
|
196
|
+
```javascript
|
|
197
|
+
// Arm partition in STAY mode
|
|
198
|
+
client.armStay(partition, code);
|
|
199
|
+
|
|
200
|
+
// Arm partition in AWAY mode
|
|
201
|
+
client.armAway(partition, code);
|
|
202
|
+
|
|
203
|
+
// Disarm partition
|
|
204
|
+
client.disarm(partition, code);
|
|
205
|
+
|
|
206
|
+
// Get current zone states
|
|
207
|
+
const zones = client.getZones();
|
|
208
|
+
// Returns: { '1': { open: false, ... }, '2': { open: true, ... } }
|
|
209
|
+
|
|
210
|
+
// Get current partition states
|
|
211
|
+
const partitions = client.getPartitions();
|
|
212
|
+
// Returns: { '1': { armedState: 'Disarmed', ready: true, ... } }
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### Lifecycle
|
|
216
|
+
|
|
217
|
+
```javascript
|
|
218
|
+
// Start listening for panel
|
|
219
|
+
await client.start();
|
|
220
|
+
|
|
221
|
+
// Gracefully close session
|
|
222
|
+
await client.stop();
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
## Protocol Notes
|
|
226
|
+
|
|
227
|
+
### Push Notification Model
|
|
228
|
+
|
|
229
|
+
The DSC panel uses a **push notification** architecture, not query/response:
|
|
230
|
+
|
|
231
|
+
- ✅ Panel **pushes** zone status changes automatically (0x0210 LIFESTYLE_ZONE_STATUS)
|
|
232
|
+
- ✅ Panel sends keepalive POLL messages every 10 seconds (0x0600)
|
|
233
|
+
- ❌ Status queries (0x0810, 0x0811, 0x0812) are **not supported** by most panels
|
|
234
|
+
- ❌ You cannot query current status - you must wait for notifications
|
|
235
|
+
|
|
236
|
+
### Session Establishment
|
|
237
|
+
|
|
238
|
+
1. Panel sends OPEN_SESSION (plaintext)
|
|
239
|
+
2. Client responds with COMMAND_RESPONSE
|
|
240
|
+
3. Handshake exchange (OPEN_SESSION, REQUEST_ACCESS)
|
|
241
|
+
4. Encryption keys exchanged via Type 1 initializers
|
|
242
|
+
5. Session established with AES-128-ECB encryption enabled
|
|
243
|
+
6. All post-handshake communication is encrypted
|
|
244
|
+
|
|
245
|
+
### Zone Status Values
|
|
246
|
+
|
|
247
|
+
From 0x0210 notifications:
|
|
248
|
+
- `0` or `1` = Zone CLOSED
|
|
249
|
+
- `2` = Zone OPEN
|
|
250
|
+
|
|
251
|
+
## Environment Variables
|
|
252
|
+
|
|
253
|
+
All examples support environment variable configuration:
|
|
254
|
+
|
|
255
|
+
```bash
|
|
256
|
+
INTEGRATION_ID=250228754343 \
|
|
257
|
+
ACCESS_CODE=28754543 \
|
|
258
|
+
MASTER_CODE=5555 \
|
|
259
|
+
UDP_PORT=3073 \
|
|
260
|
+
DEBUG=true \
|
|
261
|
+
npm run example:basic
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
## Troubleshooting
|
|
265
|
+
|
|
266
|
+
### No zone events
|
|
267
|
+
|
|
268
|
+
- Panel only sends notifications when zones **change state**
|
|
269
|
+
- Try opening/closing a door or window
|
|
270
|
+
- Panel must be connected and session established
|
|
271
|
+
|
|
272
|
+
### Authentication errors
|
|
273
|
+
|
|
274
|
+
- Verify `masterCode` is correct
|
|
275
|
+
- Some panels require specific access levels
|
|
276
|
+
- Try with a valid user code instead of master code
|
|
277
|
+
|
|
278
|
+
### Session won't establish
|
|
279
|
+
|
|
280
|
+
- Check `integrationId` and `accessCode` match panel configuration
|
|
281
|
+
- Ensure panel is configured to connect to your server IP
|
|
282
|
+
- Check firewall allows UDP port 3073
|
|
283
|
+
- Use `debug: true` to see detailed protocol messages
|
|
284
|
+
|
|
285
|
+
##Further Reading
|
|
286
|
+
|
|
287
|
+
For the full handshake implementation and protocol details, see the main `server.js` file which contains the complete CLI application with all debugging output.
|