dsc-itv2-client 2.0.5 → 2.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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/ITV2Client.js +92 -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.5",
4
+ "version": "2.0.7",
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
@@ -758,6 +758,9 @@ export class ITV2Client extends EventEmitter {
758
758
  case CMD.SINGLE_ZONE_BYPASS_STATUS:
759
759
  this._handleZoneBypassNotification(parsed);
760
760
  break;
761
+ case CMD.ZONE_ALARM_STATUS:
762
+ this._handleZoneAlarmStatus(parsed);
763
+ break;
761
764
  case CMD.MULTIPLE_MESSAGE:
762
765
  this._handleMultipleMessagePacket(parsed);
763
766
  break;
@@ -1219,6 +1222,10 @@ export class ITV2Client extends EventEmitter {
1219
1222
  rawBytes: statusBytes.toString('hex'),
1220
1223
  };
1221
1224
 
1225
+ // Check alarm state change before updating event handler
1226
+ const prevPartState = this.eventHandler.getPartitionState(partitionNum);
1227
+ const wasInAlarm = prevPartState?.alarm || false;
1228
+
1222
1229
  // Update event handler
1223
1230
  const armedState = partStatus.awayArmed ? 0x02 :
1224
1231
  partStatus.stayArmed ? 0x01 :
@@ -1226,6 +1233,22 @@ export class ITV2Client extends EventEmitter {
1226
1233
  partStatus.armed ? 0x01 : 0x00;
1227
1234
  this.eventHandler.handlePartitionArming(partitionNum, armedState);
1228
1235
 
1236
+ // Store alarm flag in partition state
1237
+ const currentPartState = this.eventHandler.getPartitionState(partitionNum);
1238
+ if (currentPartState) {
1239
+ currentPartState.alarm = partStatus.alarm;
1240
+ currentPartState.alarmInMemory = partStatus.alarmInMemory;
1241
+ }
1242
+
1243
+ // Emit alarm events on state change
1244
+ if (partStatus.alarm && !wasInAlarm) {
1245
+ this._logMinimal(`[Partition ${partitionNum}] ALARM!`);
1246
+ this.emit('partition:alarm', { partition: partitionNum, ...partStatus });
1247
+ } else if (!partStatus.alarm && wasInAlarm) {
1248
+ this._logMinimal(`[Partition ${partitionNum}] Alarm restored`);
1249
+ this.emit('partition:alarm:restored', { partition: partitionNum, ...partStatus });
1250
+ }
1251
+
1229
1252
  this.emit('partition:statusResponse', partitionNum, partStatus);
1230
1253
  this._resolvePendingRequest(CMD.PARTITION_STATUS, partStatus);
1231
1254
  } catch (e) {
@@ -1460,6 +1483,75 @@ export class ITV2Client extends EventEmitter {
1460
1483
  this._ack();
1461
1484
  }
1462
1485
 
1486
+ _handleZoneAlarmStatus(parsed) {
1487
+ // 0x0840 ModuleStatus_Zone_Alarm_Status (from decompiled SDK):
1488
+ // [Partition CompactInt][Count CompactInt][repeated: Zone CompactInt, AlarmType 1B, AlarmState 1B]
1489
+ //
1490
+ // AlarmType: 1=Unknown, 2=Burglary, 3=24HR_Supervisory, 4=Fire, 5=Fire_Supervisory,
1491
+ // 6=CO, 7=Gas, 8=HighTemp, 9=LowTemp, 10=Medical, 11=Panic, 12=Waterflow,
1492
+ // 13=Water_Leakage, 14=Pendant, 15=Tamper, 16=RF_Jam, 17=Hardware_Fault,
1493
+ // 18=Duress, 19=Personal_Emergency, 20=Holdup, 21=Sprinkler
1494
+ // AlarmState: bit 0 = 0 → NotInAlarm, bit 0 = 1 → InAlarm
1495
+ const fullPayload = this._reconstructPayload(parsed);
1496
+ if (!fullPayload || fullPayload.length < 6) {
1497
+ this._ack();
1498
+ return;
1499
+ }
1500
+
1501
+ const ALARM_TYPES = {
1502
+ 1: 'Unknown', 2: 'Burglary', 3: '24HR_Supervisory', 4: 'Fire',
1503
+ 5: 'Fire_Supervisory', 6: 'CO', 7: 'Gas', 8: 'HighTemp',
1504
+ 9: 'LowTemp', 10: 'Medical', 11: 'Panic', 12: 'Waterflow',
1505
+ 13: 'Water_Leakage', 14: 'Pendant', 15: 'Tamper', 16: 'RF_Jam',
1506
+ 17: 'Hardware_Fault', 18: 'Duress', 19: 'Personal_Emergency',
1507
+ 20: 'Holdup', 21: 'Sprinkler',
1508
+ };
1509
+
1510
+ try {
1511
+ let offset = 0;
1512
+ const partition = ITv2Session.decodeVarBytes(fullPayload, offset);
1513
+ offset += partition.bytesRead;
1514
+
1515
+ const count = ITv2Session.decodeVarBytes(fullPayload, offset);
1516
+ offset += count.bytesRead;
1517
+
1518
+ while (offset < fullPayload.length) {
1519
+ const zone = ITv2Session.decodeVarBytes(fullPayload, offset);
1520
+ offset += zone.bytesRead;
1521
+ if (offset >= fullPayload.length) break;
1522
+ const alarmType = fullPayload[offset++];
1523
+ if (offset >= fullPayload.length) break;
1524
+ const alarmState = fullPayload[offset++];
1525
+
1526
+ const inAlarm = !!(alarmState & 0x01);
1527
+ const alarmTypeName = ALARM_TYPES[alarmType] || `Unknown(${alarmType})`;
1528
+
1529
+ this._logMinimal(`[Zone ${zone.value}] Alarm: ${inAlarm ? 'ACTIVE' : 'CLEARED'} (${alarmTypeName}, partition ${partition.value})`);
1530
+
1531
+ const alarmData = {
1532
+ zone: zone.value,
1533
+ partition: partition.value,
1534
+ alarmType,
1535
+ alarmTypeName,
1536
+ inAlarm,
1537
+ timestamp: new Date(),
1538
+ };
1539
+
1540
+ if (inAlarm) {
1541
+ this.eventHandler.handleZoneAlarm(zone.value, alarmTypeName);
1542
+ this.emit('zone:alarm', alarmData);
1543
+ this.emit('partition:alarm', alarmData);
1544
+ } else {
1545
+ this.emit('zone:alarm:restored', alarmData);
1546
+ this.emit('partition:alarm:restored', alarmData);
1547
+ }
1548
+ }
1549
+ } catch (e) {
1550
+ this._log(`[Zone Alarm Status] Parse error: ${e.message}`);
1551
+ }
1552
+ this._ack();
1553
+ }
1554
+
1463
1555
  _handleExitDelayNotification(parsed) {
1464
1556
  // 0x0230 NotificationExitDelay (from neohub):
1465
1557
  // [CompactInt:Partition][DelayFlags:1B][CompactInt:DurationInSeconds]