dsc-itv2-client 1.0.30 → 2.0.0

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.
@@ -91,11 +91,13 @@ export const CMD = {
91
91
  ZONE_STATUS: 0x0811,
92
92
  PARTITION_STATUS: 0x0812,
93
93
  ZONE_BYPASS_STATUS: 0x0813,
94
- SYSTEM_TROUBLE_STATUS: 0x0814,
94
+ SYSTEM_TROUBLE_STATUS: 0x0822, // New trouble status (0x0814 is deprecated)
95
+ SYSTEM_TROUBLE_STATUS_OLD: 0x0814, // Deprecated
95
96
  ALARM_MEMORY_INFO: 0x0815,
96
97
  BUS_STATUS: 0x0816,
97
- TROUBLE_DETAIL: 0x0817,
98
+ TROUBLE_DETAIL: 0x0823,
98
99
  DOOR_CHIME_STATUS: 0x0819,
100
+ MULTIPLE_MESSAGE: 0x0623,
99
101
  // Module Control commands
100
102
  PARTITION_ARM: 0x0900,
101
103
  PARTITION_DISARM: 0x0901,
@@ -298,6 +300,22 @@ function buildHeader(senderSeq, receiverSeq, command = null, appSeq = null, comm
298
300
  * - Type 2: Uses Access Code (32 hex digits) → 16 bytes directly
299
301
  */
300
302
  export class ITv2Session {
303
+ /**
304
+ * Arming mode values (from neohub ArmingMode enum)
305
+ */
306
+ static ARM_MODE = {
307
+ DISARM: 0,
308
+ STAY: 1,
309
+ AWAY: 2,
310
+ NO_ENTRY_DELAY: 3,
311
+ NIGHT: 4,
312
+ QUICK: 5,
313
+ USER: 6,
314
+ STAY_NO_ENTRY: 7,
315
+ AWAY_NO_ENTRY: 8,
316
+ NIGHT_NO_ENTRY: 9,
317
+ };
318
+
301
319
  constructor(integrationId, accessCode, log = console.log) {
302
320
  this.integrationId = integrationId; // 12-digit ID for packet header
303
321
  this.accessCode = accessCode; // 8-digit (Type 1) or 32-hex (Type 2) for encryption
@@ -368,12 +386,13 @@ export class ITv2Session {
368
386
  this.log(`[Session] Encrypted framed: ${framed.toString('hex')}`);
369
387
  }
370
388
 
371
- // Byte stuff the payload
389
+ // Byte stuff header and payload (neohub stuffs both independently)
390
+ const stuffedHeader = stuffBytes(Buffer.from(this.integrationId, 'ascii'));
372
391
  const stuffedPayload = stuffBytes(framed);
373
392
 
374
- // Build full packet WITHOUT integration ID
375
- // The panel sends WITH integration ID, but SDK sends WITHOUT it
393
+ // Full packet: [stuffed integration ID][0x7E][stuffed payload][0x7F]
376
394
  return Buffer.concat([
395
+ stuffedHeader,
377
396
  Buffer.from([0x7E]),
378
397
  stuffedPayload,
379
398
  Buffer.from([0x7F]),
@@ -513,21 +532,10 @@ export class ITv2Session {
513
532
 
514
533
  /**
515
534
  * Build OPEN_SESSION message (echo back panel's session info)
516
- * NOTE: OPEN_SESSION does NOT have an app sequence byte - it goes directly to payload
535
+ * OpenSession IS a CommandMessageBase in neohub - it has a CommandSequence byte
517
536
  */
518
537
  buildOpenSessionResponse(openSessionData) {
519
- this.localSequence = (this.localSequence + 1) & 0xFF;
520
-
521
- // OPEN_SESSION format: [sender][receiver][cmd 2B][payload...] - NO app sequence!
522
- const message = buildHeader(
523
- this.localSequence,
524
- this.remoteSequence,
525
- CMD.OPEN_SESSION,
526
- null, // NO app sequence for OPEN_SESSION
527
- openSessionData
528
- );
529
-
530
- return this.buildPacket(message);
538
+ return this.buildCommand(CMD.OPEN_SESSION, openSessionData);
531
539
  }
532
540
 
533
541
  /**
@@ -759,25 +767,44 @@ export class ITv2Session {
759
767
  // ==================== VarBytes Encoding ====================
760
768
 
761
769
  /**
762
- * Encode a value as VarBytes (DSC protocol variable-length integer)
763
- * Format: [length byte][value bytes in little-endian]
770
+ * Encode a value as CompactInteger (DSC protocol variable-length integer)
771
+ * Format: [length byte][value bytes in big-endian]
772
+ * Matches neohub CompactIntegerSerializer: strips leading zero bytes, stores BE
764
773
  * @param {number} value - Value to encode
765
- * @returns {Buffer} - VarBytes encoded buffer
774
+ * @returns {Buffer} - CompactInteger encoded buffer
766
775
  */
767
776
  encodeVarBytes(value) {
768
777
  if (value < 0x100) {
769
- // 1 byte value
770
778
  return Buffer.from([0x01, value & 0xFF]);
771
779
  } else if (value < 0x10000) {
772
- // 2 byte value (little-endian)
773
- return Buffer.from([0x02, value & 0xFF, (value >> 8) & 0xFF]);
780
+ return Buffer.from([0x02, (value >> 8) & 0xFF, value & 0xFF]);
774
781
  } else if (value < 0x1000000) {
775
- // 3 byte value (little-endian)
776
- return Buffer.from([0x03, value & 0xFF, (value >> 8) & 0xFF, (value >> 16) & 0xFF]);
782
+ return Buffer.from([0x03, (value >> 16) & 0xFF, (value >> 8) & 0xFF, value & 0xFF]);
777
783
  } else {
778
- // 4 byte value (little-endian)
779
- return Buffer.from([0x04, value & 0xFF, (value >> 8) & 0xFF, (value >> 16) & 0xFF, (value >> 24) & 0xFF]);
784
+ return Buffer.from([0x04, (value >> 24) & 0xFF, (value >> 16) & 0xFF, (value >> 8) & 0xFF, value & 0xFF]);
785
+ }
786
+ }
787
+
788
+ /**
789
+ * Decode a CompactInteger from a buffer
790
+ * Format: [length byte][value bytes in big-endian]
791
+ * @param {Buffer} buffer - Buffer to read from
792
+ * @param {number} offset - Starting offset
793
+ * @returns {{ value: number, bytesRead: number }}
794
+ */
795
+ static decodeVarBytes(buffer, offset = 0) {
796
+ if (offset >= buffer.length) {
797
+ throw new Error('Buffer too short for CompactInteger');
798
+ }
799
+ const length = buffer[offset];
800
+ if (offset + 1 + length > buffer.length) {
801
+ throw new Error(`Buffer too short for CompactInteger value (need ${length}, have ${buffer.length - offset - 1})`);
802
+ }
803
+ let value = 0;
804
+ for (let i = 0; i < length; i++) {
805
+ value = (value << 8) | buffer[offset + 1 + i];
780
806
  }
807
+ return { value, bytesRead: 1 + length };
781
808
  }
782
809
 
783
810
  /**
@@ -818,24 +845,20 @@ export class ITv2Session {
818
845
 
819
846
  /**
820
847
  * Build a 0x0800 Command Request wrapper
821
- * This wraps inner commands for status queries - required by the DSC protocol
822
- * Format: [AppSeqNum 1B][CommandToRequest 2B LE][InnerPayload...]
848
+ * Neohub wire format: [CommandSequence 1B][InnerCommand 2B BE][InnerPayload...]
849
+ * CommandSequence is provided by buildCommand as the appSequence byte - NOT duplicated here.
823
850
  * @param {number} innerCommand - The command code to request (e.g., 0x0811)
824
851
  * @param {Buffer} innerPayload - The payload for the inner command
825
852
  * @returns {Buffer} - Complete packet ready to send
826
853
  */
827
854
  buildCommandRequest(innerCommand, innerPayload = Buffer.alloc(0)) {
828
- // Get next application sequence number
829
- const appSeq = this.appSequence;
830
- this.appSequence = (this.appSequence + 1) & 0xFF;
831
-
832
- // Build 0x0800 payload: [AppSeqNum][CommandToRequest LE][InnerPayload]
833
- const payload = Buffer.alloc(3 + innerPayload.length);
834
- payload[0] = appSeq;
835
- payload.writeUInt16LE(innerCommand, 1); // Command in little-endian
836
- innerPayload.copy(payload, 3);
855
+ // Build payload: [InnerCommand 2B BE][InnerPayload]
856
+ // The CommandSequence is added by buildCommand as the appSequence byte
857
+ const payload = Buffer.alloc(2 + innerPayload.length);
858
+ payload.writeUInt16BE(innerCommand, 0);
859
+ innerPayload.copy(payload, 2);
837
860
 
838
- this.log(`[Session] Building 0x0800 Command Request: innerCmd=0x${innerCommand.toString(16)}, appSeq=${appSeq}`);
861
+ this.log(`[Session] Building 0x0800 Command Request: innerCmd=0x${innerCommand.toString(16)}`);
839
862
  this.log(`[Session] Inner payload: ${innerPayload.toString('hex')}`);
840
863
 
841
864
  return this.buildCommand(CMD.STATUS_REQUEST, payload);
@@ -843,6 +866,24 @@ export class ITv2Session {
843
866
 
844
867
  // ==================== Status Query Methods (0x0800 Wrapped) ====================
845
868
 
869
+ /**
870
+ * Build system capabilities request (0x0613 wrapped in 0x0800)
871
+ * Neohub queries this FIRST on connect to get max zones/partitions/etc.
872
+ */
873
+ buildCapabilitiesRequest() {
874
+ // ConnectionSystemCapabilities: 6 CompactInteger fields, all default 0
875
+ // Each CompactInt(0) = [0x01, 0x00]
876
+ const payload = Buffer.from([
877
+ 0x01, 0x00, // MaxZones = 0
878
+ 0x01, 0x00, // MaxUsers = 0
879
+ 0x01, 0x00, // MaxPartitions = 0
880
+ 0x01, 0x00, // MaxFOBs = 0
881
+ 0x01, 0x00, // MaxProxTags = 0
882
+ 0x01, 0x00, // MaxOutputs = 0
883
+ ]);
884
+ return this.buildCommandRequest(CMD.SYSTEM_CAPABILITIES, payload);
885
+ }
886
+
846
887
  /**
847
888
  * Build global status request (0x0810 wrapped in 0x0800)
848
889
  */
@@ -857,7 +898,8 @@ export class ITv2Session {
857
898
  * @param {number} numZones - Number of zones to query (default 128)
858
899
  */
859
900
  buildZoneStatusRequest(startZone = 0, numZones = 128) {
860
- // 0x0811 payload: [ZoneNumber VarBytes][NumberOfZones VarBytes]
901
+ // 0x0811 request payload: [ZoneStart CompactInt][ZoneCount CompactInt]
902
+ // StatusSizeInBytes and ZoneStatusBytes are response-only fields
861
903
  const zoneBytes = this.encodeVarBytes(startZone);
862
904
  const countBytes = this.encodeVarBytes(numZones);
863
905
  const payload = Buffer.concat([zoneBytes, countBytes]);
@@ -869,11 +911,12 @@ export class ITv2Session {
869
911
  * @param {number} partition - Partition number (0 = all partitions)
870
912
  * @param {number} numPartitions - Number of partitions to query (default 8)
871
913
  */
872
- buildPartitionStatusRequest(partition = 0, numPartitions = 8) {
873
- // 0x0812 payload: [Partition VarBytes][NumberOfPartitions VarBytes]
914
+ buildPartitionStatusRequest(partition = 1) {
915
+ // 0x0812 payload (from neohub ModulePartitionStatus):
916
+ // [Partition CompactInt][PartitionStatus LeadingLengthArray(empty)]
874
917
  const partBytes = this.encodeVarBytes(partition);
875
- const countBytes = this.encodeVarBytes(numPartitions);
876
- const payload = Buffer.concat([partBytes, countBytes]);
918
+ const emptyArray = Buffer.from([0x00]); // LeadingLengthArray with 0 length
919
+ const payload = Buffer.concat([partBytes, emptyArray]);
877
920
  return this.buildCommandRequest(CMD.PARTITION_STATUS, payload);
878
921
  }
879
922
 
@@ -964,16 +1007,22 @@ export class ITv2Session {
964
1007
 
965
1008
  /**
966
1009
  * Build POLL command for keepalive
1010
+ * ConnectionPoll is IMessageData (no CommandSequence) — just command code, no payload
967
1011
  */
968
1012
  buildPoll() {
969
- return this.buildCommand(CMD.POLL);
1013
+ this.localSequence = (this.localSequence + 1) & 0xFF;
1014
+ const message = buildHeader(this.localSequence, this.remoteSequence, CMD.POLL);
1015
+ return this.buildPacket(message);
970
1016
  }
971
1017
 
972
1018
  /**
973
1019
  * Build END_SESSION command to gracefully close the session
1020
+ * No CommandSequence, no payload — just the command code
974
1021
  */
975
1022
  buildEndSession() {
976
- return this.buildCommand(CMD.END_SESSION);
1023
+ this.localSequence = (this.localSequence + 1) & 0xFF;
1024
+ const message = buildHeader(this.localSequence, this.remoteSequence, CMD.END_SESSION);
1025
+ return this.buildPacket(message);
977
1026
  }
978
1027
 
979
1028
  /**
@@ -993,25 +1042,33 @@ export class ITv2Session {
993
1042
  }
994
1043
 
995
1044
  const partition = data[0];
996
- const statusByte = data[1];
1045
+ // From neohub ModulePartitionStatus PartitionStatus1/2/3:
1046
+ const status1 = data[1];
1047
+ const status2 = data.length > 2 ? data[2] : 0;
1048
+ const status3 = data.length > 3 ? data[3] : 0;
997
1049
 
998
- // Status byte bit meanings (based on DSC protocol):
1050
+ const armedValue = status1 & 0x1F;
999
1051
  const status = {
1000
1052
  partition,
1001
- armed: !!(statusByte & 0x01), // Bit 0: Armed
1002
- stayArmed: !!(statusByte & 0x02), // Bit 1: Stay Armed
1003
- awayArmed: !!(statusByte & 0x04), // Bit 2: Away Armed
1004
- alarm: !!(statusByte & 0x08), // Bit 3: Alarm
1005
- trouble: !!(statusByte & 0x10), // Bit 4: Trouble
1006
- ready: !!(statusByte & 0x20), // Bit 5: Ready
1007
- exitDelay: !!(statusByte & 0x40), // Bit 6: Exit Delay
1008
- entryDelay: !!(statusByte & 0x80), // Bit 7: Entry Delay
1009
- rawStatus: statusByte,
1053
+ armed: !!(status1 & 0x01),
1054
+ stayArmed: armedValue === 0x03,
1055
+ awayArmed: armedValue === 0x05,
1056
+ nightArmed: armedValue === 0x09,
1057
+ exitDelay: !!(status1 & 0x20),
1058
+ entryDelay: !!(status1 & 0x40),
1059
+ quickExit: !!(status1 & 0x80),
1060
+ alarm: !!(status2 & 0x01),
1061
+ trouble: !!(status2 & 0x02),
1062
+ bypass: !!(status2 & 0x04),
1063
+ alarmMemory: !!(status2 & 0x10),
1064
+ audibleBell: !!(status2 & 0x40),
1065
+ buzzer: !!(status2 & 0x80),
1066
+ firePreAlert: !!(status3 & 0x01),
1067
+ rawStatus: status1,
1010
1068
  };
1011
1069
 
1012
- // Additional bytes may contain more info
1013
- if (data.length > 2) {
1014
- status.additionalData = data.slice(2);
1070
+ if (data.length > 4) {
1071
+ status.additionalData = data.slice(4);
1015
1072
  }
1016
1073
 
1017
1074
  return status;
@@ -1029,17 +1086,17 @@ export class ITv2Session {
1029
1086
  const zone = data[0];
1030
1087
  const statusByte = data[1];
1031
1088
 
1032
- // Zone status bit meanings:
1089
+ // Zone status bit meanings (from neohub ModuleZoneStatus.ZoneStatusEnum):
1033
1090
  const status = {
1034
1091
  zone,
1035
- open: !!(statusByte & 0x01), // Bit 0: Zone Open
1036
- tamper: !!(statusByte & 0x02), // Bit 1: Tamper
1037
- fault: !!(statusByte & 0x04), // Bit 2: Fault
1038
- lowBattery: !!(statusByte & 0x08), // Bit 3: Low Battery
1039
- supervision: !!(statusByte & 0x10), // Bit 4: Supervision Loss
1040
- alarm: !!(statusByte & 0x20), // Bit 5: In Alarm
1041
- bypassed: !!(statusByte & 0x40), // Bit 6: Bypassed
1042
- armed: !!(statusByte & 0x80), // Bit 7: Armed
1092
+ open: !!(statusByte & 0x01), // Bit 0: Open
1093
+ tamper: !!(statusByte & 0x02), // Bit 1: Tamper
1094
+ fault: !!(statusByte & 0x04), // Bit 2: Fault
1095
+ lowBattery: !!(statusByte & 0x08), // Bit 3: Low Battery
1096
+ delinquency: !!(statusByte & 0x10), // Bit 4: Delinquency
1097
+ alarm: !!(statusByte & 0x20), // Bit 5: Alarm
1098
+ alarmInMemory: !!(statusByte & 0x40), // Bit 6: Alarm Memory
1099
+ bypassed: !!(statusByte & 0x80), // Bit 7: Bypass
1043
1100
  rawStatus: statusByte,
1044
1101
  };
1045
1102
 
@@ -1072,11 +1129,12 @@ export class ITv2Session {
1072
1129
  /**
1073
1130
  * Build PARTITION_ARM command
1074
1131
  * @param {number} partition - Partition number (1-8)
1075
- * @param {number} armMode - Arming mode:
1076
- * 0 = Stay (perimeter only)
1077
- * 1 = Away (full arm)
1078
- * 2 = No Entry Delay
1079
- * 3 = Force Arm (bypass open zones)
1132
+ * @param {number} armMode - Arming mode (use ITv2Session.ARM_MODE):
1133
+ * 1 = Stay (perimeter only)
1134
+ * 2 = Away (full arm)
1135
+ * 3 = No Entry Delay
1136
+ * 4 = Night
1137
+ * 5 = Quick Arm
1080
1138
  * @param {string} accessCode - Master/user code (e.g., "5555")
1081
1139
  */
1082
1140
  buildPartitionArm(partition, armMode, accessCode) {
@@ -46,41 +46,50 @@ export function formatZoneStatus(zone) {
46
46
  * Status is encoded across multiple bytes using bit fields
47
47
  */
48
48
  export function parsePartitionStatus(statusBytes) {
49
- if (!statusBytes || statusBytes.length < 3) {
49
+ if (!statusBytes || statusBytes.length < 1) {
50
50
  return null;
51
51
  }
52
52
 
53
- // Helper: Check bit at specific index
54
- const checkBit = (bitIndex) => {
55
- const byteOffset = Math.floor(bitIndex / 8);
56
- const bitMask = 1 << (bitIndex % 8);
57
- return byteOffset < statusBytes.length && (statusBytes[byteOffset] & bitMask) !== 0;
58
- };
53
+ // From neohub ModulePartitionStatus PartitionStatus1/2/3 enums:
54
+ const status1 = statusBytes[0] || 0;
55
+ const status2 = statusBytes.length > 1 ? statusBytes[1] : 0;
56
+ const status3 = statusBytes.length > 2 ? statusBytes[2] : 0;
59
57
 
60
- // Armed state is encoded in bits 0-3 (lower nibble of first byte)
61
- const armedValue = statusBytes[0] & 0x0F;
58
+ // Status1 byte: armed state is a composite value, not individual bits
59
+ // Armed=0x01, StayArmed=0x03, ArmedAway=0x05, NightArmed=0x09,
60
+ // ArmedNoEntry=0x11, ExitDelay=0x20, EntryDelay=0x40, QuickExit=0x80
61
+ const armedValue = status1 & 0x1F; // Lower 5 bits for armed state
62
62
  const armedStates = {
63
- 1: 'Disarmed',
64
- 2: 'Armed Away',
65
- 3: 'Armed Stay',
66
- 4: 'Armed Stay (No Entry)',
67
- 5: 'Armed Stay (Instant)',
68
- 6: 'Armed Away (Instant)'
63
+ 0x00: 'Disarmed',
64
+ 0x01: 'Armed',
65
+ 0x03: 'Armed Stay',
66
+ 0x05: 'Armed Away',
67
+ 0x09: 'Armed Night',
68
+ 0x11: 'Armed (No Entry Delay)',
69
69
  };
70
70
 
71
71
  return {
72
72
  partitionNumber: null, // Set by caller
73
- armedState: armedStates[armedValue] || `Unknown (${armedValue})`,
73
+ armedState: armedStates[armedValue] || `Unknown (0x${armedValue.toString(16)})`,
74
74
  armedValue: armedValue,
75
- ready: checkBit(0) || checkBit(1) || checkBit(2), // Bits 0-2: Ready state
76
- alarm: checkBit(8), // Bit 8: Alarm
77
- trouble: checkBit(9), // Bit 9: Trouble
78
- bypass: checkBit(10), // Bit 10: Bypass active
79
- busy: checkBit(11), // Bit 11: Busy (processing)
80
- alarmMemory: checkBit(12), // Bit 12: Alarm in memory
81
- audibleBell: checkBit(14), // Bit 14: Siren/bell
82
- buzzer: checkBit(15), // Bit 15: Buzzer
83
- blank: checkBit(19), // Bit 19: Display blank
75
+ armed: !!(status1 & 0x01),
76
+ stayArmed: armedValue === 0x03,
77
+ awayArmed: armedValue === 0x05,
78
+ nightArmed: armedValue === 0x09,
79
+ exitDelay: !!(status1 & 0x20),
80
+ entryDelay: !!(status1 & 0x40),
81
+ quickExit: !!(status1 & 0x80),
82
+ // Status2 byte flags
83
+ alarm: !!(status2 & 0x01),
84
+ trouble: !!(status2 & 0x02),
85
+ bypass: !!(status2 & 0x04),
86
+ programmingMode: !!(status2 & 0x08),
87
+ alarmMemory: !!(status2 & 0x10),
88
+ doorChime: !!(status2 & 0x20),
89
+ audibleBell: !!(status2 & 0x40),
90
+ buzzer: !!(status2 & 0x80),
91
+ // Status3 byte flags
92
+ firePreAlert: !!(status3 & 0x01),
84
93
  rawBytes: Buffer.from(statusBytes).toString('hex')
85
94
  };
86
95
  }