dsc-itv2-client 2.0.7 → 2.0.10

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/ITV2Client.js +95 -17
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsc-itv2-client",
3
3
  "author": "fajitacat",
4
- "version": "2.0.7",
4
+ "version": "2.0.10",
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
@@ -5,6 +5,7 @@
5
5
 
6
6
  import {EventEmitter} from 'events';
7
7
  import net from 'net';
8
+ import dgram from 'dgram';
8
9
  import {ITv2Session, CMD, CMD_NAMES} from './itv2-session.js';
9
10
  import {parseType1Initializer, type2InitializerTransform} from './itv2-crypto.js';
10
11
  import {EventHandler} from './event-handler.js';
@@ -23,12 +24,16 @@ export class ITV2Client extends EventEmitter {
23
24
  this.accessCode = options.accessCode;
24
25
  this.masterCode = options.masterCode || '5555';
25
26
  this.port = options.port || 3072; // TCP port (panel connects to us)
27
+ this.udpPort = options.udpPort || null; // UDP port (optional, panel polls us)
26
28
  this.logLevel = options.logLevel || (options.debug === true ? 'verbose' : 'minimal'); // 'silent', 'minimal', 'verbose'
27
29
  this.encryptionType = options.encryptionType || null; // null = auto-detect, 1 = Type 1, 2 = Type 2
28
30
 
29
31
  // Internal state
30
32
  this.tcpServer = null; // TCP server
31
33
  this.tcpSocket = null; // Active panel TCP connection
34
+ this.udpSocket = null; // UDP socket
35
+ this._udpRemote = null; // { address, port } of panel over UDP
36
+ this._activeTransport = null; // 'tcp' or 'udp' — whichever the panel connected on
32
37
  this.session = null;
33
38
  this.eventHandler = new EventHandler();
34
39
  this.handshakeState = 'WAITING';
@@ -48,17 +53,20 @@ export class ITV2Client extends EventEmitter {
48
53
  }
49
54
 
50
55
  /**
51
- * Start the TCP server and listen for panel connection
56
+ * Start the TCP server (and optional UDP socket) and listen for panel connection
52
57
  * The panel initiates a TCP connection to us on port 3072 (default)
58
+ * If udpPort is configured, also listen for UDP datagrams
53
59
  */
54
60
  async start() {
55
- return new Promise((resolve, reject) => {
61
+ // Start TCP server
62
+ await new Promise((resolve, reject) => {
56
63
  this.tcpServer = net.createServer((socket) => {
57
64
  const clientId = `${socket.remoteAddress}:${socket.remotePort}`;
58
65
  this._logMinimal(`[TCP] Panel connected: ${clientId}`);
59
66
 
60
67
  this.panelAddress = socket.remoteAddress;
61
68
  this.tcpSocket = socket;
69
+ this._activeTransport = 'tcp';
62
70
 
63
71
  let buffer = Buffer.alloc(0);
64
72
 
@@ -75,12 +83,15 @@ export class ITV2Client extends EventEmitter {
75
83
  socket.on('close', () => {
76
84
  this._logMinimal(`[TCP] Panel disconnected: ${clientId}`);
77
85
  this.tcpSocket = null;
78
- this.handshakeState = 'WAITING';
79
- if (this.session) {
80
- this.session.disableAes();
81
- this.session = null;
86
+ if (this._activeTransport === 'tcp') {
87
+ this._activeTransport = null;
88
+ this.handshakeState = 'WAITING';
89
+ if (this.session) {
90
+ this.session.disableAes();
91
+ this.session = null;
92
+ }
93
+ this.emit('session:closed');
82
94
  }
83
- this.emit('session:closed');
84
95
  });
85
96
 
86
97
  socket.on('error', (err) => {
@@ -101,16 +112,60 @@ export class ITV2Client extends EventEmitter {
101
112
  this.emit('listening', address);
102
113
  resolve();
103
114
  });
115
+ });
104
116
 
105
- // Register signal handlers (only once)
106
- if (!this._boundShutdown) {
107
- this._boundShutdown = async () => {
108
- await this.stop();
109
- process.exit(0);
110
- };
111
- process.on('SIGINT', this._boundShutdown);
112
- process.on('SIGTERM', this._boundShutdown);
113
- }
117
+ // Start UDP socket if configured
118
+ if (this.udpPort) {
119
+ await this._startUdpSocket();
120
+ }
121
+
122
+ // Register signal handlers (only once)
123
+ if (!this._boundShutdown) {
124
+ this._boundShutdown = async () => {
125
+ await this.stop();
126
+ process.exit(0);
127
+ };
128
+ process.on('SIGINT', this._boundShutdown);
129
+ process.on('SIGTERM', this._boundShutdown);
130
+ }
131
+ }
132
+
133
+ /**
134
+ * Start UDP socket for panel communication
135
+ * UDP datagrams are self-contained packets (no stream reassembly needed)
136
+ */
137
+ async _startUdpSocket() {
138
+ return new Promise((resolve, reject) => {
139
+ this.udpSocket = dgram.createSocket('udp4');
140
+
141
+ this.udpSocket.on('message', (msg, rinfo) => {
142
+ this._log(`[UDP] Received ${msg.length} bytes from ${rinfo.address}:${rinfo.port}`);
143
+
144
+ // Track remote for responses
145
+ this._udpRemote = { address: rinfo.address, port: rinfo.port };
146
+ this.panelAddress = rinfo.address;
147
+
148
+ // If panel is using UDP, mark it as active transport
149
+ if (this._activeTransport !== 'tcp') {
150
+ this._activeTransport = 'udp';
151
+ }
152
+
153
+ // UDP datagrams are complete packets — process directly
154
+ this._handlePacket(msg);
155
+ });
156
+
157
+ this.udpSocket.on('error', (err) => {
158
+ this._log(`[Error] UDP socket error: ${err.message}`);
159
+ this.emit('error', err);
160
+ reject(err);
161
+ });
162
+
163
+ this.udpSocket.bind(this.udpPort, '0.0.0.0', () => {
164
+ const address = this.udpSocket.address();
165
+ this._logMinimal(`[UDP] Listening on port ${address.port}`);
166
+ this.emit('listening:udp', address);
167
+ resolve();
168
+ });
114
169
  });
115
170
  }
116
171
 
@@ -180,6 +235,18 @@ export class ITV2Client extends EventEmitter {
180
235
  this.tcpServer = null;
181
236
  }
182
237
 
238
+ // Close UDP socket
239
+ if (this.udpSocket) {
240
+ await new Promise((resolve) => {
241
+ this.udpSocket.close(() => {
242
+ this._log('[Shutdown] UDP socket closed');
243
+ resolve();
244
+ });
245
+ });
246
+ this.udpSocket = null;
247
+ this._udpRemote = null;
248
+ }
249
+
183
250
  // Clear heartbeat
184
251
  if (this._heartbeatInterval) {
185
252
  clearInterval(this._heartbeatInterval);
@@ -205,6 +272,7 @@ export class ITV2Client extends EventEmitter {
205
272
  }
206
273
  this.handshakeState = 'WAITING';
207
274
  this.panelAddress = null;
275
+ this._activeTransport = null;
208
276
  this.detectedEncryptionType = null;
209
277
  this._panelOpenSessionData = null;
210
278
 
@@ -627,6 +695,13 @@ export class ITV2Client extends EventEmitter {
627
695
  }
628
696
 
629
697
  _sendPacket(packet) {
698
+ if (this._activeTransport === 'udp' && this.udpSocket && this._udpRemote) {
699
+ this._log(`[UDP] TX ${packet.length} bytes`);
700
+ this._hexDump(packet);
701
+ this.udpSocket.send(packet, this._udpRemote.port, this._udpRemote.address);
702
+ return;
703
+ }
704
+
630
705
  if (!this.tcpSocket || this.tcpSocket.destroyed) {
631
706
  this._logMinimal('[Error] No panel connection');
632
707
  return;
@@ -654,9 +729,11 @@ export class ITV2Client extends EventEmitter {
654
729
  this._log(`[Packet] Integration ID: ${parsed.integrationId}`);
655
730
  this._log(`[Packet] Sender Seq: ${parsed.senderSequence}, Receiver Seq: ${parsed.receiverSequence}`);
656
731
  this._log(`[Packet] Command: ${CMD_NAMES[parsed.command] || parsed.command}`);
732
+
657
733
  if (parsed.appSequence !== null) {
658
734
  this._log(`[Packet] App Seq: ${parsed.appSequence}`);
659
735
  }
736
+
660
737
  if (parsed.commandData) {
661
738
  this._log(`[Packet] Data (${parsed.commandData.length} bytes): ${parsed.commandData.toString('hex').toUpperCase().match(/.{1,2}/g).join(' ')}`);
662
739
  }
@@ -1542,8 +1619,9 @@ export class ITV2Client extends EventEmitter {
1542
1619
  this.emit('zone:alarm', alarmData);
1543
1620
  this.emit('partition:alarm', alarmData);
1544
1621
  } else {
1622
+ // Zone alarm cleared — but partition stays in alarm until disarmed.
1623
+ // Only emit zone-level restore, not partition-level.
1545
1624
  this.emit('zone:alarm:restored', alarmData);
1546
- this.emit('partition:alarm:restored', alarmData);
1547
1625
  }
1548
1626
  }
1549
1627
  } catch (e) {