dsc-itv2-client 1.0.21 → 1.0.23

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsc-itv2-client",
3
3
  "author": "fajitacat",
4
- "version": "1.0.21",
4
+ "version": "1.0.23",
5
5
  "description": "Reverse engineered DSC ITV2 Protocol Client Library for TL280R Communicator - Monitor and control DSC alarm panels",
6
6
  "main": "src/index.js",
7
7
  "type": "module",
package/src/ITV2Client.js CHANGED
@@ -379,9 +379,23 @@ export class ITV2Client extends EventEmitter {
379
379
  case CMD.TIME_DATE_BROADCAST:
380
380
  this._handleTimeDateBroadcast(parsed);
381
381
  break;
382
+ case CMD.ZONE_STATUS:
383
+ this._handleZoneStatusResponse(parsed);
384
+ break;
385
+ case CMD.PARTITION_STATUS:
386
+ this._handlePartitionStatusResponse(parsed);
387
+ break;
388
+ case CMD.GLOBAL_STATUS:
389
+ this._handleGlobalStatusResponse(parsed);
390
+ break;
382
391
  default:
383
392
  if (this.handshakeState === 'ESTABLISHED') {
384
- this._log(`[Session] Unhandled command ${CMD_NAMES[cmd] || '0x' + cmd?.toString(16)}`);
393
+ const cmdHex = cmd ? '0x' + cmd.toString(16).padStart(4, '0') : 'null';
394
+ const cmdName = CMD_NAMES[cmd] || 'UNKNOWN';
395
+ this._logMinimal(`[Session] Received command ${cmdName} (${cmdHex})`);
396
+ if (parsed.commandData && parsed.commandData.length > 0) {
397
+ this._logMinimal(`[Session] Data (${parsed.commandData.length} bytes): ${parsed.commandData.toString('hex')}`);
398
+ }
385
399
  this._sendPacket(this.session.buildSimpleAck());
386
400
  }
387
401
  break;
@@ -569,10 +583,17 @@ export class ITV2Client extends EventEmitter {
569
583
  }
570
584
 
571
585
  _handleCommandResponse(parsed) {
572
- const responseCode = parsed.commandData?.[0] || 0;
586
+ const data = parsed.commandData;
587
+ const responseCode = data?.[0] || 0;
573
588
  const appSeqAsEcho = parsed.appSequence;
574
589
 
575
- this._log(`[Handshake] Got COMMAND_RESPONSE (echoed app_seq: ${appSeqAsEcho}, response_code: ${responseCode}), sending ACK`);
590
+ this._log(`[Session] Got COMMAND_RESPONSE (app_seq: ${appSeqAsEcho}, response_code: ${responseCode})`);
591
+
592
+ // Log full data in established state for debugging query responses
593
+ if (this.handshakeState === 'ESTABLISHED' && data && data.length > 1) {
594
+ this._logMinimal(`[Command Response] Response code: ${responseCode}, data (${data.length} bytes): ${data.toString('hex')}`);
595
+ this.emit('command:response', { responseCode, appSequence: appSeqAsEcho, data });
596
+ }
576
597
 
577
598
  const ack = this.session.buildSimpleAck();
578
599
  this._sendPacket(ack);
@@ -655,6 +676,75 @@ export class ITV2Client extends EventEmitter {
655
676
  this._sendPacket(this.session.buildSimpleAck());
656
677
  }
657
678
 
679
+ // ==================== Status Query Response Handlers ====================
680
+
681
+ _handleZoneStatusResponse(parsed) {
682
+ const data = parsed.commandData;
683
+ this._logMinimal(`[Zone Status] Received zone status response`);
684
+ this._log(`[Zone Status] Raw data (${data?.length || 0} bytes): ${data?.toString('hex') || 'none'}`);
685
+
686
+ if (data && data.length >= 3) {
687
+ // Response format: [ZoneNumber 2B BE][StatusByte 1B][...more zones...]
688
+ const zoneNum = data.readUInt16BE(0);
689
+ const statusByte = data[2];
690
+
691
+ // Update internal state
692
+ const fullStatus = this.eventHandler.handleZoneStatus(zoneNum, statusByte);
693
+
694
+ this._logMinimal(`[Zone Status] Zone ${zoneNum}: ${fullStatus.open ? 'OPEN' : 'CLOSED'} (0x${statusByte.toString(16)})`);
695
+
696
+ // Emit event with full status
697
+ this.emit('zone:statusResponse', zoneNum, fullStatus);
698
+ this.emit('zone:status', zoneNum, fullStatus);
699
+ } else {
700
+ this._log(`[Zone Status] Empty or invalid response`);
701
+ }
702
+
703
+ this._sendPacket(this.session.buildSimpleAck());
704
+ }
705
+
706
+ _handlePartitionStatusResponse(parsed) {
707
+ const data = parsed.commandData;
708
+ this._logMinimal(`[Partition Status] Received partition status response`);
709
+ this._log(`[Partition Status] Raw data (${data?.length || 0} bytes): ${data?.toString('hex') || 'none'}`);
710
+
711
+ if (data && data.length >= 3) {
712
+ // Response format: [PartitionNumber 2B BE][StatusBytes...]
713
+ const partitionNum = data.readUInt16BE(0);
714
+ const statusBytes = data.slice(2);
715
+
716
+ this._logMinimal(`[Partition Status] Partition ${partitionNum}: status bytes ${statusBytes.toString('hex')}`);
717
+
718
+ // Emit raw event - let consumers parse the detailed status
719
+ this.emit('partition:statusResponse', partitionNum, statusBytes);
720
+ } else {
721
+ this._log(`[Partition Status] Empty or invalid response`);
722
+ }
723
+
724
+ this._sendPacket(this.session.buildSimpleAck());
725
+ }
726
+
727
+ _handleGlobalStatusResponse(parsed) {
728
+ const data = parsed.commandData;
729
+ this._logMinimal(`[Global Status] Received global status response`);
730
+ this._log(`[Global Status] Raw data (${data?.length || 0} bytes): ${data?.toString('hex') || 'none'}`);
731
+
732
+ if (data && data.length > 0) {
733
+ // Emit raw event with full data
734
+ this.emit('global:statusResponse', data);
735
+
736
+ // Log byte breakdown for debugging
737
+ this._log(`[Global Status] Byte breakdown:`);
738
+ for (let i = 0; i < data.length && i < 32; i++) {
739
+ this._log(` Byte ${i}: 0x${data[i].toString(16).padStart(2, '0')} (${data[i]})`);
740
+ }
741
+ } else {
742
+ this._log(`[Global Status] Empty response`);
743
+ }
744
+
745
+ this._sendPacket(this.session.buildSimpleAck());
746
+ }
747
+
658
748
  // ==================== Utility Methods ====================
659
749
 
660
750
  _handleError(error) {
@@ -756,67 +756,121 @@ export class ITv2Session {
756
756
  return this.buildCommand(CMD.REQUEST_ACCESS, payload);
757
757
  }
758
758
 
759
+ // ==================== VarBytes Encoding ====================
760
+
759
761
  /**
760
- * Build status request command
761
- * @param {number} statusType - Type of status to request:
762
- * 0x10 = Global Status (0x0810)
763
- * 0x11 = Zone Status (0x0811)
764
- * 0x12 = Partition Status (0x0812)
765
- * 0x13 = Zone Bypass Status (0x0813)
766
- * 0x14 = System Trouble Status (0x0814)
767
- * 0x15 = Alarm Memory Info (0x0815)
768
- * 0x16 = Bus Status (0x0816)
769
- * 0x17 = Trouble Detail (0x0817)
770
- * 0x19 = Door Chime Status (0x0819)
771
- * @param {number} partitionOrZone - Partition or zone number (0 = all)
762
+ * Encode a value as VarBytes (DSC protocol variable-length integer)
763
+ * Format: [length byte][value bytes in little-endian]
764
+ * @param {number} value - Value to encode
765
+ * @returns {Buffer} - VarBytes encoded buffer
772
766
  */
773
- buildStatusRequest(statusType, partitionOrZone = 0) {
774
- // Payload: [status type byte][partition/zone number]
775
- const payload = Buffer.from([statusType, partitionOrZone]);
767
+ encodeVarBytes(value) {
768
+ if (value < 0x100) {
769
+ // 1 byte value
770
+ return Buffer.from([0x01, value & 0xFF]);
771
+ } else if (value < 0x10000) {
772
+ // 2 byte value (little-endian)
773
+ return Buffer.from([0x02, value & 0xFF, (value >> 8) & 0xFF]);
774
+ } else if (value < 0x1000000) {
775
+ // 3 byte value (little-endian)
776
+ return Buffer.from([0x03, value & 0xFF, (value >> 8) & 0xFF, (value >> 16) & 0xFF]);
777
+ } else {
778
+ // 4 byte value (little-endian)
779
+ return Buffer.from([0x04, value & 0xFF, (value >> 8) & 0xFF, (value >> 16) & 0xFF, (value >> 24) & 0xFF]);
780
+ }
781
+ }
782
+
783
+ // ==================== 0x0800 Command Request Wrapper ====================
784
+
785
+ /**
786
+ * Build a 0x0800 Command Request wrapper
787
+ * This wraps inner commands for status queries - required by the DSC protocol
788
+ * Format: [AppSeqNum 1B][CommandToRequest 2B LE][InnerPayload...]
789
+ * @param {number} innerCommand - The command code to request (e.g., 0x0811)
790
+ * @param {Buffer} innerPayload - The payload for the inner command
791
+ * @returns {Buffer} - Complete packet ready to send
792
+ */
793
+ buildCommandRequest(innerCommand, innerPayload = Buffer.alloc(0)) {
794
+ // Get next application sequence number
795
+ const appSeq = this.appSequence;
796
+ this.appSequence = (this.appSequence + 1) & 0xFF;
797
+
798
+ // Build 0x0800 payload: [AppSeqNum][CommandToRequest LE][InnerPayload]
799
+ const payload = Buffer.alloc(3 + innerPayload.length);
800
+ payload[0] = appSeq;
801
+ payload.writeUInt16LE(innerCommand, 1); // Command in little-endian
802
+ innerPayload.copy(payload, 3);
803
+
804
+ this.log(`[Session] Building 0x0800 Command Request: innerCmd=0x${innerCommand.toString(16)}, appSeq=${appSeq}`);
805
+ this.log(`[Session] Inner payload: ${innerPayload.toString('hex')}`);
806
+
776
807
  return this.buildCommand(CMD.STATUS_REQUEST, payload);
777
808
  }
778
809
 
810
+ // ==================== Status Query Methods (0x0800 Wrapped) ====================
811
+
779
812
  /**
780
- * Build global status request
813
+ * Build global status request (0x0810 wrapped in 0x0800)
781
814
  */
782
815
  buildGlobalStatusRequest() {
783
- // Send GLOBAL_STATUS (0x0810) command directly, not wrapped in STATUS_REQUEST
784
- return this.buildCommand(CMD.GLOBAL_STATUS);
816
+ // 0x0810 has no payload
817
+ return this.buildCommandRequest(CMD.GLOBAL_STATUS, Buffer.alloc(0));
785
818
  }
786
819
 
787
820
  /**
788
- * Build zone status request
789
- * @param {number} zone - Zone number (0 = all zones)
821
+ * Build zone status request (0x0811 wrapped in 0x0800)
822
+ * @param {number} startZone - Starting zone number (0 = first zone)
823
+ * @param {number} numZones - Number of zones to query (default 128)
790
824
  */
791
- buildZoneStatusRequest(zone = 0) {
792
- // Send ZONE_STATUS (0x0811) command directly with zone number
793
- const payload = zone > 0 ? Buffer.from([zone]) : Buffer.alloc(0);
794
- return this.buildCommand(CMD.ZONE_STATUS, payload);
825
+ buildZoneStatusRequest(startZone = 0, numZones = 128) {
826
+ // 0x0811 payload: [ZoneNumber VarBytes][NumberOfZones VarBytes]
827
+ const zoneBytes = this.encodeVarBytes(startZone);
828
+ const countBytes = this.encodeVarBytes(numZones);
829
+ const payload = Buffer.concat([zoneBytes, countBytes]);
830
+ return this.buildCommandRequest(CMD.ZONE_STATUS, payload);
795
831
  }
796
832
 
797
833
  /**
798
- * Build partition status request
834
+ * Build partition status request (0x0812 wrapped in 0x0800)
799
835
  * @param {number} partition - Partition number (0 = all partitions)
836
+ * @param {number} numPartitions - Number of partitions to query (default 8)
800
837
  */
801
- buildPartitionStatusRequest(partition = 0) {
802
- // Send PARTITION_STATUS (0x0812) command directly with partition number
803
- const payload = partition > 0 ? Buffer.from([partition]) : Buffer.alloc(0);
804
- return this.buildCommand(CMD.PARTITION_STATUS, payload);
838
+ buildPartitionStatusRequest(partition = 0, numPartitions = 8) {
839
+ // 0x0812 payload: [Partition VarBytes][NumberOfPartitions VarBytes]
840
+ const partBytes = this.encodeVarBytes(partition);
841
+ const countBytes = this.encodeVarBytes(numPartitions);
842
+ const payload = Buffer.concat([partBytes, countBytes]);
843
+ return this.buildCommandRequest(CMD.PARTITION_STATUS, payload);
805
844
  }
806
845
 
807
846
  /**
808
- * Build zone bypass status request
847
+ * Build zone bypass status request (0x0813 wrapped in 0x0800)
809
848
  * @param {number} zone - Zone number (0 = all zones)
849
+ * @param {number} numZones - Number of zones (default 128)
810
850
  */
811
- buildZoneBypassStatusRequest(zone = 0) {
812
- return this.buildStatusRequest(0x13, zone);
851
+ buildZoneBypassStatusRequest(zone = 0, numZones = 128) {
852
+ const zoneBytes = this.encodeVarBytes(zone);
853
+ const countBytes = this.encodeVarBytes(numZones);
854
+ const payload = Buffer.concat([zoneBytes, countBytes]);
855
+ return this.buildCommandRequest(CMD.ZONE_BYPASS_STATUS, payload);
813
856
  }
814
857
 
815
858
  /**
816
- * Build system trouble status request
859
+ * Build system trouble status request (0x0814 wrapped in 0x0800)
817
860
  */
818
861
  buildTroubleStatusRequest() {
819
- return this.buildStatusRequest(0x14, 0);
862
+ // 0x0814 typically has no payload or minimal payload
863
+ return this.buildCommandRequest(CMD.SYSTEM_TROUBLE_STATUS, Buffer.alloc(0));
864
+ }
865
+
866
+ /**
867
+ * Build status request command (legacy - simple format)
868
+ * @deprecated Use buildCommandRequest() with specific inner commands instead
869
+ */
870
+ buildStatusRequest(statusType, partitionOrZone = 0) {
871
+ // Legacy format - kept for backwards compatibility
872
+ const payload = Buffer.from([statusType, partitionOrZone]);
873
+ return this.buildCommand(CMD.STATUS_REQUEST, payload);
820
874
  }
821
875
 
822
876
  /**