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.
@@ -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.