zigbee-herdsman 4.3.2 → 4.4.1

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 (38) hide show
  1. package/.release-please-manifest.json +1 -1
  2. package/CHANGELOG.md +20 -0
  3. package/dist/adapter/deconz/driver/constants.d.ts +1 -2
  4. package/dist/adapter/deconz/driver/constants.d.ts.map +1 -1
  5. package/dist/adapter/deconz/driver/constants.js +1 -0
  6. package/dist/adapter/deconz/driver/constants.js.map +1 -1
  7. package/dist/adapter/deconz/driver/driver.d.ts +5 -1
  8. package/dist/adapter/deconz/driver/driver.d.ts.map +1 -1
  9. package/dist/adapter/deconz/driver/driver.js +92 -52
  10. package/dist/adapter/deconz/driver/driver.js.map +1 -1
  11. package/dist/adapter/deconz/driver/frameParser.d.ts.map +1 -1
  12. package/dist/adapter/deconz/driver/frameParser.js +115 -30
  13. package/dist/adapter/deconz/driver/frameParser.js.map +1 -1
  14. package/dist/controller/controller.d.ts.map +1 -1
  15. package/dist/controller/controller.js +5 -22
  16. package/dist/controller/controller.js.map +1 -1
  17. package/dist/controller/helpers/installCodes.d.ts +27 -0
  18. package/dist/controller/helpers/installCodes.d.ts.map +1 -0
  19. package/dist/controller/helpers/installCodes.js +90 -0
  20. package/dist/controller/helpers/installCodes.js.map +1 -0
  21. package/dist/zspec/utils.d.ts +0 -13
  22. package/dist/zspec/utils.d.ts.map +1 -1
  23. package/dist/zspec/utils.js +0 -49
  24. package/dist/zspec/utils.js.map +1 -1
  25. package/dist/zspec/zcl/definition/cluster.d.ts.map +1 -1
  26. package/dist/zspec/zcl/definition/cluster.js +6 -0
  27. package/dist/zspec/zcl/definition/cluster.js.map +1 -1
  28. package/package.json +1 -1
  29. package/src/adapter/deconz/driver/constants.ts +2 -2
  30. package/src/adapter/deconz/driver/driver.ts +104 -55
  31. package/src/adapter/deconz/driver/frameParser.ts +139 -32
  32. package/src/controller/controller.ts +5 -26
  33. package/src/controller/helpers/installCodes.ts +107 -0
  34. package/src/zspec/utils.ts +1 -60
  35. package/src/zspec/zcl/definition/cluster.ts +6 -0
  36. package/test/controller.test.ts +82 -5
  37. package/test/utils.test.ts +58 -7
  38. package/test/zspec/utils.test.ts +0 -64
package/package.json CHANGED
@@ -63,7 +63,7 @@
63
63
  "type": "git",
64
64
  "url": "git+https://github.com/Koenkk/zigbee-herdsman.git"
65
65
  },
66
- "version": "4.3.2",
66
+ "version": "4.4.1",
67
67
  "scripts": {
68
68
  "build": "tsc",
69
69
  "build:watch": "tsc -w",
@@ -127,6 +127,7 @@ export const stackParameters = [
127
127
  {id: ParamId.STK_PREDEFINED_PANID, type: DataType.U8},
128
128
  {id: ParamId.STK_NETWORK_KEY, type: [DataType.U8, DataType.SecKey], readArg: 1}, // index, key
129
129
  {id: ParamId.STK_LINK_KEY, type: [DataType.U64, DataType.SecKey], readArg: 1}, // mac addess, key
130
+ {id: ParamId.STK_ENDPOINT, type: DataType.Custom},
130
131
  {id: ParamId.DEV_WATCHDOG_TTL, type: DataType.U32},
131
132
  {id: ParamId.STK_PERMIT_JOIN, type: DataType.U8},
132
133
  {id: ParamId.NWK_EXTENDED_PANID, type: DataType.U64},
@@ -141,7 +142,7 @@ interface Request {
141
142
  commandId: FirmwareCommand;
142
143
  networkState: NetworkState;
143
144
  parameterId: ParamId;
144
- parameter?: Buffer | number | bigint;
145
+ parameter?: Buffer | number | bigint | undefined;
145
146
  seqNumber: number;
146
147
  // biome-ignore lint/suspicious/noExplicitAny: API
147
148
  resolve: (value: any) => void;
@@ -196,7 +197,6 @@ interface ReceivedDataResponse {
196
197
  }
197
198
 
198
199
  interface GpDataInd {
199
- rspId: number;
200
200
  seqNr: number;
201
201
  id: number;
202
202
  options: number;
@@ -4,6 +4,7 @@ import events from "node:events";
4
4
  import net from "node:net";
5
5
 
6
6
  import slip from "slip";
7
+ import {Buffalo} from "../../../buffalo";
7
8
  import type {Backup} from "../../../models";
8
9
  import {logger} from "../../../utils/logger";
9
10
  import {SerialPort} from "../../serialPort";
@@ -112,6 +113,10 @@ class Driver extends events.EventEmitter {
112
113
  public paramCurrentChannel = 0;
113
114
  public paramNwkPanid = 0;
114
115
  public paramNwkKey = Buffer.alloc(16);
116
+ public paramEndpoint0: Buffer | undefined;
117
+ public paramEndpoint1: Buffer | undefined;
118
+ public fixParamEndpoint0: Buffer;
119
+ public fixParamEndpoint1: Buffer;
115
120
  public paramNwkUpdateId = 0;
116
121
  public paramChannelMask = 0;
117
122
  public paramProtocolVersion = 0;
@@ -130,6 +135,50 @@ class Driver extends events.EventEmitter {
130
135
  this.writer = new Writer();
131
136
  this.parser = new Parser();
132
137
 
138
+ this.fixParamEndpoint0 = Buffer.from([
139
+ 0x00, // index
140
+ 0x01, // endpoint,
141
+ 0x04, // profileId
142
+ 0x01,
143
+ 0x05, // deviceId
144
+ 0x00,
145
+ 0x01, // deviceVersion
146
+ 0x05, // in cluster count
147
+ 0x00, // basic
148
+ 0x00,
149
+ 0x06, // on/off
150
+ 0x00,
151
+ 0x0a, // time
152
+ 0x00,
153
+ 0x19, // ota
154
+ 0x00,
155
+ 0x01, // ias ace
156
+ 0x05,
157
+ 0x04, // out cluster count
158
+ 0x01, // power configuration
159
+ 0x00,
160
+ 0x20, // poll control
161
+ 0x00,
162
+ 0x00, // ias zone
163
+ 0x05,
164
+ 0x02, // ias wd
165
+ 0x05,
166
+ ]);
167
+
168
+ this.fixParamEndpoint1 = Buffer.from([
169
+ 0x01, // index
170
+ 0xf2, // endpoint,
171
+ 0xe0, // profileId
172
+ 0xa1,
173
+ 0x64, // deviceId
174
+ 0x00,
175
+ 0x01, // deviceVersion
176
+ 0x00, // in cluster count
177
+ 0x01, // out cluster count
178
+ 0x21, // green power
179
+ 0x00,
180
+ ]);
181
+
133
182
  this.tickTimer = setInterval(() => {
134
183
  this.tick();
135
184
  }, 100);
@@ -366,6 +415,16 @@ class Driver extends events.EventEmitter {
366
415
  return false;
367
416
  }
368
417
 
418
+ if (!this.paramEndpoint0 || this.fixParamEndpoint0.compare(this.paramEndpoint0) !== 0) {
419
+ logger.debug("Endpoint[0] doesn't match configuration", NS);
420
+ return false;
421
+ }
422
+
423
+ if (!this.paramEndpoint1 || this.fixParamEndpoint1.compare(this.paramEndpoint1) !== 0) {
424
+ logger.debug("Endpoint[1] doesn't match configuration", NS);
425
+ return false;
426
+ }
427
+
369
428
  if ((this.deviceStatus & DEV_STATUS_NET_STATE_MASK) !== NetworkState.Connected) {
370
429
  return false;
371
430
  }
@@ -521,55 +580,19 @@ class Driver extends events.EventEmitter {
521
580
  await this.writeParameterRequest(ParamId.STK_NETWORK_KEY, Buffer.from([0x0, ...this.networkOptions.networkKey]));
522
581
  }
523
582
 
524
- // now reconnect, this will also store configuration in nvram
525
- await this.changeNetworkStateRequest(NetworkState.Connected);
583
+ // check current endpoint configuration
584
+ if (!this.paramEndpoint0 || this.fixParamEndpoint0.compare(this.paramEndpoint0) !== 0) {
585
+ this.paramEndpoint0 = this.fixParamEndpoint0;
586
+ await this.writeParameterRequest(ParamId.STK_ENDPOINT, this.paramEndpoint0);
587
+ }
526
588
 
527
- // TODO(mpi): Endpoint configuration should be written like other network parameters and subject to `changed`.
528
- // It should happen before NET_CONNECTED is set.
529
- // Therefore read endpoint configuration, compare, swap if needed.
530
-
531
- // TODO(mpi): The following code only adjusts first endpoint, with a fixed setting.
532
- // Ideally also second endpoint should be configured like deCONZ does.
533
-
534
- // write endpoints
535
- /* Format of the STK.Endpoint parameter:
536
- {
537
- u8 endpointIndex // index used by firmware 0..2
538
- u8 endpoint // actual zigbee endpoint
539
- u16 profileId
540
- u16 deviceId
541
- u8 deviceVersion
542
- u8 serverClusterCount
543
- u16 serverClusters[serverClusterCount]
544
- u8 clientClusterCount
545
- u16 clientClusters[clientClusterCount]
546
- }
547
- */
548
- //[ sd1 ep proId devId vers #inCl iCl1 iCl2 iCl3 iCl4 iCl5 #outC oCl1 oCl2 oCl3 oCl4 ]
549
- //
550
- // const sd = [
551
- // 0x00, // index
552
- // 0x01, // endpoint
553
- // 0x04, 0x01, // profileId
554
- // 0x05, 0x00, // deviceId
555
- // 0x01, // deviceVersion
556
- // 0x05, // serverClusterCount
557
- // 0x00, 0x00, // Basic
558
- // 0x00, 0x06, // On/Off TODO(mpi): This is wrong byte order! should be 0x06 0x00
559
- // 0x0a, 0x00, // Time
560
- // 0x19, 0x00, // OTA
561
- // 0x01, 0x05, // IAS ACE
562
- // 0x04, // clientClusterCount
563
- // 0x01, 0x00, // Power Configuration
564
- // 0x20, 0x00, // Poll Control
565
- // 0x00, 0x05, // IAS Zone
566
- // 0x02, 0x05 // IAS WD
567
- // ];
568
- // // TODO(mpi) why is it reversed? That result in invalid endpoint configuration.
569
- // // Likely the command gets discarded as it results in endpointIndex = 0x05.
570
- // const sd1 = sd.reverse();
571
- // await this.driver.writeParameterRequest(PARAM.PARAM.STK.Endpoint, Buffer.from(sd1));
589
+ if (!this.paramEndpoint1 || this.fixParamEndpoint1.compare(this.paramEndpoint1) !== 0) {
590
+ this.paramEndpoint1 = this.fixParamEndpoint1;
591
+ await this.writeParameterRequest(ParamId.STK_ENDPOINT, this.paramEndpoint1);
592
+ }
572
593
 
594
+ // now reconnect, this will also store configuration in nvram
595
+ await this.changeNetworkStateRequest(NetworkState.Connected);
573
596
  return;
574
597
  }
575
598
 
@@ -593,6 +616,8 @@ class Driver extends events.EventEmitter {
593
616
  this.readParameterRequest(ParamId.APS_CHANNEL_MASK),
594
617
  this.readParameterRequest(ParamId.STK_PROTOCOL_VERSION),
595
618
  this.readParameterRequest(ParamId.STK_FRAME_COUNTER),
619
+ this.readParameterRequest(ParamId.STK_ENDPOINT, Buffer.from([0])),
620
+ this.readParameterRequest(ParamId.STK_ENDPOINT, Buffer.from([1])),
596
621
  ])
597
622
  .then(
598
623
  ([
@@ -609,6 +634,8 @@ class Driver extends events.EventEmitter {
609
634
  channelMask,
610
635
  protocolVersion,
611
636
  frameCounter,
637
+ ep0,
638
+ ep1,
612
639
  ]) => {
613
640
  this.paramFirmwareVersion = fwVersion;
614
641
  this.paramCurrentChannel = currentChannel as number;
@@ -623,6 +650,13 @@ class Driver extends events.EventEmitter {
623
650
  if (frameCounter !== null) {
624
651
  this.paramFrameCounter = frameCounter as number;
625
652
  }
653
+ if (ep0 !== null) {
654
+ this.paramEndpoint0 = ep0 as Buffer;
655
+ }
656
+
657
+ if (ep1 !== null) {
658
+ this.paramEndpoint1 = ep1 as Buffer;
659
+ }
626
660
 
627
661
  // console.log({fwVersion, mac, panid, apsUseExtPanid, currentChannel, nwkKey, nwkUpdateId, channelMask, protocolVersion, frameCounter});
628
662
 
@@ -927,7 +961,7 @@ class Driver extends events.EventEmitter {
927
961
  });
928
962
  }
929
963
 
930
- public readParameterRequest(parameterId: ParamId, parameter?: Buffer | undefined): Promise<unknown> {
964
+ public readParameterRequest(parameterId: ParamId, parameter?: Buffer | number | bigint): Promise<unknown> {
931
965
  const seqNumber = this.nextSeqNumber();
932
966
  return new Promise((resolve, reject): void => {
933
967
  //logger.debug(`push read parameter request to queue. seqNr: ${seqNumber} paramId: ${parameterId}`, NS);
@@ -1027,14 +1061,29 @@ class Driver extends events.EventEmitter {
1027
1061
  });
1028
1062
  }
1029
1063
 
1030
- private sendReadParameterRequest(parameterId: ParamId, seqNumber: number): CommandResult {
1031
- /* command id, sequence number, 0, framelength(U16), payloadlength(U16), parameter id */
1032
- // TODO(mpi): refactor so this proper supports arguments
1033
- if (parameterId === ParamId.STK_NETWORK_KEY) {
1034
- return this.sendRequest(Buffer.from([FirmwareCommand.ReadParameter, seqNumber, 0x00, 0x09, 0x00, 0x02, 0x00, parameterId, 0x00]));
1064
+ private sendReadParameterRequest(parameterId: ParamId, seqNumber: number, arg?: Buffer | number | bigint): CommandResult {
1065
+ let frameLength = 8; // starts with min. frame length
1066
+ let payloadLength = 1; // min. parameterId
1067
+
1068
+ if (arg instanceof Buffer) {
1069
+ payloadLength += arg.byteLength;
1070
+ frameLength += arg.byteLength;
1071
+ }
1072
+
1073
+ const buf = new Buffalo(Buffer.alloc(frameLength));
1074
+
1075
+ buf.writeUInt8(FirmwareCommand.ReadParameter);
1076
+ buf.writeUInt8(seqNumber);
1077
+ buf.writeUInt8(0); // reserved, shall be 0
1078
+ buf.writeUInt16(frameLength);
1079
+ buf.writeUInt16(payloadLength);
1080
+ buf.writeUInt8(parameterId);
1081
+
1082
+ if (arg instanceof Buffer) {
1083
+ buf.writeBuffer(arg, arg.byteLength);
1035
1084
  }
1036
1085
 
1037
- return this.sendRequest(Buffer.from([FirmwareCommand.ReadParameter, seqNumber, 0x00, 0x08, 0x00, 0x01, 0x00, parameterId]));
1086
+ return this.sendRequest(buf.getBuffer());
1038
1087
  }
1039
1088
 
1040
1089
  private sendWriteParameterRequest(parameterId: ParamId, value: Buffer | number | bigint, seqNumber: number): CommandResult {
@@ -1184,7 +1233,7 @@ class Driver extends events.EventEmitter {
1184
1233
  switch (req.commandId) {
1185
1234
  case FirmwareCommand.ReadParameter:
1186
1235
  logger.debug(`send read parameter request from queue. parameter: ${ParamId[req.parameterId]} seq: ${req.seqNumber}`, NS);
1187
- this.sendReadParameterRequest(req.parameterId, req.seqNumber);
1236
+ this.sendReadParameterRequest(req.parameterId, req.seqNumber, req.parameter);
1188
1237
  break;
1189
1238
  case FirmwareCommand.WriteParameter:
1190
1239
  if (req.parameter === undefined) {
@@ -81,6 +81,13 @@ function parseReadParameterResponse(view: DataView): Command | null {
81
81
  result = view.getUint8(pos);
82
82
  break;
83
83
  }
84
+ case ParamId.STK_ENDPOINT: {
85
+ result = Buffer.alloc(view.byteLength - pos);
86
+ for (let i = 0; pos < view.byteLength; i++, pos++) {
87
+ result[i] = view.getUint8(pos);
88
+ }
89
+ break;
90
+ }
84
91
  case ParamId.APS_CHANNEL_MASK: {
85
92
  result = view.getUint32(pos, littleEndian);
86
93
  break;
@@ -438,43 +445,141 @@ function parseDeviceStateChangedNotification(view: DataView): number | null {
438
445
  }
439
446
  }
440
447
 
448
+ // The ApplicationID sub-field contains the information about the application used by the GPD.
449
+ // ApplicationID = 0b000 indicates the GPD_ID field has the length of 4B and contains the GPD SrcID.
450
+ // ApplicationID = 0b010 indicates the GPD_ID field has the length of 8B and contains the GPD IEEE address.
451
+ enum GpApplicationId {
452
+ SrcId4B = 0,
453
+ Lped = 1,
454
+ Ieee8B = 2,
455
+ }
456
+
457
+ enum GpSecurityLevel {
458
+ NoSecurity = 0,
459
+ // TODO(mpi): "Reserved" is noted in the available spec but the code defined it as follows:
460
+ Reserved = 1, // 0b01 1LSB of frame counter and short (2B) MIC only
461
+ FrameCounter4BMic4B = 2,
462
+ EncryptionFrameCounter4BMic4B = 3,
463
+ }
464
+
465
+ enum ZgpConstants {
466
+ GpNwkProtocolVersion = 3,
467
+ GpNwkDataFrame = 0,
468
+ GpNwkMaintenanceFrame = 1,
469
+ GpMinMsduSize = 1,
470
+ GpAutoCommissioningFlag = 1 << 6,
471
+ GpNwkFrameControlExtensionFlag = 1 << 7,
472
+ }
473
+
441
474
  function parseGreenPowerDataIndication(view: DataView): GpDataInd | null {
442
475
  try {
443
- let id: number;
444
- let rspId: number;
445
- let options: number;
446
- let srcId: number;
447
- let frameCounter: number;
448
- let commandId: number;
449
- let commandFrameSize: number;
450
- let commandFrame: Buffer;
451
- const seqNr = view.getUint8(1);
452
-
453
- if (view.byteLength < 30) {
454
- logger.debug("GP data notification", NS);
455
- id = 0x00; // 0 = notification, 4 = commissioning
456
- rspId = 0x01; // 1 = pairing, 2 = commissioning
457
- options = 0;
458
- view.getUint16(7, littleEndian); // frame ctrl field(7) ext.fcf(8)
459
- srcId = view.getUint32(9, littleEndian);
460
- frameCounter = view.getUint32(13, littleEndian);
461
- commandId = view.getUint8(17);
462
- commandFrameSize = view.byteLength - 18 - 6; // cut 18 from begin and 4 (sec mic) and 2 from end (cfc)
463
- commandFrame = Buffer.from(view.buffer.slice(18, commandFrameSize + 18));
464
- } else {
465
- logger.debug("GP commissioning notification", NS);
466
- id = 0x04; // 0 = notification, 4 = commissioning
467
- rspId = 0x01; // 1 = pairing, 2 = commissioning
468
- options = view.getUint16(14, littleEndian); // opt(14) ext.opt(15)
469
- srcId = view.getUint32(8, littleEndian);
470
- frameCounter = view.getUint32(36, littleEndian);
471
- commandId = view.getUint8(12);
472
- commandFrameSize = view.byteLength - 13 - 2; // cut 13 from begin and 2 from end (cfc)
473
- commandFrame = Buffer.from(view.buffer.slice(13, commandFrameSize + 13));
476
+ let srcId = 0;
477
+ let frameCounter = 0;
478
+ let commandId = 0;
479
+ let commandFrameSize = 0;
480
+ let commandFrame: Buffer | undefined;
481
+
482
+ const buf = new Buffalo(Buffer.from(view.buffer));
483
+
484
+ const _fwCommandId = buf.readUInt8();
485
+ const seqNr = buf.readUInt8();
486
+ const fwStatus = buf.readUInt8();
487
+
488
+ if (fwStatus !== CommandStatus.Success) {
489
+ return null;
490
+ }
491
+
492
+ const _frameLength = buf.readUInt16();
493
+ const _payloadLength = buf.readUInt16();
494
+
495
+ // payload
496
+
497
+ // implementation ported from deCONZ GP code
498
+ const nwkFrameControl = buf.readUInt8();
499
+
500
+ // check frame type
501
+ const frameType = nwkFrameControl & 0x03;
502
+
503
+ if (frameType !== ZgpConstants.GpNwkDataFrame && frameType !== ZgpConstants.GpNwkMaintenanceFrame) {
504
+ return null;
505
+ }
506
+
507
+ // check green power protocol version
508
+ if (((nwkFrameControl >> 2) & 0x03) !== ZgpConstants.GpNwkProtocolVersion) {
509
+ return null;
510
+ }
511
+
512
+ // extended frame control
513
+ let nwkExtFrameControl = 0;
514
+ const hasExtensionFlag = nwkFrameControl & ZgpConstants.GpNwkFrameControlExtensionFlag;
515
+ if (hasExtensionFlag) {
516
+ nwkExtFrameControl = buf.readUInt8();
517
+ }
518
+
519
+ const options = nwkExtFrameControl | (nwkFrameControl << 8);
520
+
521
+ const applicationId = nwkExtFrameControl & 7;
522
+ const securityLevel = (nwkExtFrameControl >> 3) & 3;
523
+
524
+ if (applicationId !== GpApplicationId.SrcId4B && applicationId !== GpApplicationId.Ieee8B) {
525
+ return null; // NOTE: GpApplicationId.Lped (1) should be dropped as per spec
526
+ }
527
+
528
+ // The GPDSrcID field is present if the Frame Type sub-field is set to 0b00 and the ApplicationID sub-
529
+ // field of the Extended NWK Frame Control field is set to 0b000 (or not present).
530
+ if (
531
+ applicationId === GpApplicationId.SrcId4B &&
532
+ frameType === ZgpConstants.GpNwkDataFrame /*|| (frameType === ZgpConstants.GpNwkMaintenanceFrame && hasExtensionFlag) */
533
+ ) {
534
+ srcId = buf.readUInt32();
535
+ }
536
+ // TODO(mpi): for applicationId == GpApplicationId.Ieee8B:
537
+ // currently Ieee addresses aren't supported, do they actually appear?!
538
+ // these need be extracted from MAC header which we don't have here (this is only the NWK payload).
539
+
540
+ // frame counter filed
541
+ frameCounter = 0;
542
+ let micSize = 0;
543
+
544
+ if (hasExtensionFlag && frameType === ZgpConstants.GpNwkDataFrame) {
545
+ if (applicationId === GpApplicationId.Ieee8B) {
546
+ const _endpoint = buf.readUInt8();
547
+ }
548
+ // If the SecurityLevel is set to 0b00, the SecurityKey sub-field is ignored on reception, and the
549
+ // fields Security frame counter and MIC are not present.
550
+ if (securityLevel === GpSecurityLevel.Reserved) {
551
+ micSize = 2; // TODO(mpi) does this actually exists? Check recent specs!
552
+ } else if (securityLevel === GpSecurityLevel.FrameCounter4BMic4B || securityLevel === GpSecurityLevel.EncryptionFrameCounter4BMic4B) {
553
+ frameCounter = buf.readUInt32();
554
+ micSize = 4;
555
+ }
556
+ }
557
+
558
+ if (!buf.isMore()) {
559
+ return null;
560
+ }
561
+
562
+ if (applicationId === GpApplicationId.SrcId4B || applicationId === GpApplicationId.Ieee8B) {
563
+ commandId = buf.readUInt8();
564
+ commandFrameSize = buf.getBuffer().length - buf.getPosition() - micSize;
565
+ //logger.debug(`GPD payload length: ${commandFrameSize}, mic size: ${micSize}`, NS);
566
+ if (commandFrameSize < 0) {
567
+ logger.error(`GPD payload length < 0: ${commandFrameSize}`, NS);
568
+ return null;
569
+ }
570
+ commandFrame = Buffer.from(buf.readBuffer(commandFrameSize)); // copy
571
+ }
572
+
573
+ // NOTE(mpi): The old adapter treated (view.byteLength < 30) as notification, larger as commissioning?!
574
+ // The controller thus rejected commissioning frames.
575
+ const id = 0; // 0 = notification, 4 = commissioning
576
+
577
+ if (commandFrame === undefined) {
578
+ logger.debug("GPD discard frame since commandFrame is null?!", NS);
579
+ return null;
474
580
  }
475
581
 
476
582
  const ind: GpDataInd = {
477
- rspId,
478
583
  seqNr,
479
584
  id,
480
585
  options,
@@ -485,6 +590,7 @@ function parseGreenPowerDataIndication(view: DataView): GpDataInd | null {
485
590
  commandFrame,
486
591
  };
487
592
 
593
+ // TODO(mpi): This only tracks one frame, might be a bit optimistic
488
594
  if (!(lastReceivedGpInd.srcId === srcId && lastReceivedGpInd.commandId === commandId && lastReceivedGpInd.frameCounter === frameCounter)) {
489
595
  lastReceivedGpInd.srcId = srcId;
490
596
  lastReceivedGpInd.commandId = commandId;
@@ -503,6 +609,7 @@ function parseGreenPowerDataIndication(view: DataView): GpDataInd | null {
503
609
  return null;
504
610
  }
505
611
  }
612
+
506
613
  function parseMacPollCommand(_view: DataView): number {
507
614
  //logger.debug("Received command MAC_POLL", NS);
508
615
  return FirmwareCommand.MacPollIndication;
@@ -18,6 +18,7 @@ import Database from "./database";
18
18
  import type * as Events from "./events";
19
19
  import GreenPower from "./greenPower";
20
20
  import {ZclFrameConverter} from "./helpers";
21
+ import {checkInstallCode, parseInstallCode} from "./helpers/installCodes";
21
22
  import {Device, Entity} from "./model";
22
23
  import {InterviewState} from "./model/device";
23
24
  import Group from "./model/group";
@@ -246,34 +247,12 @@ export class Controller extends events.EventEmitter<ControllerEventMap> {
246
247
  }
247
248
 
248
249
  public async addInstallCode(installCode: string): Promise<void> {
249
- const aqaraMatch = installCode.match(/^G\$M:.+\$A:(.+)\$I:(.+)$/);
250
- const pipeMatch = installCode.match(/^(.+)\|(.+)$/);
251
- let ieeeAddr: string;
252
- let keyStr: string;
253
-
254
- if (aqaraMatch) {
255
- ieeeAddr = aqaraMatch[1];
256
- keyStr = aqaraMatch[2];
257
- } else if (pipeMatch) {
258
- ieeeAddr = pipeMatch[1];
259
- keyStr = pipeMatch[2];
260
- } else {
261
- assert(
262
- installCode.length === 95 || installCode.length === 91,
263
- `Unsupported install code, got ${installCode.length} chars, expected 95 or 91`,
264
- );
265
- const keyStart = installCode.length - (installCode.length === 95 ? 36 : 32);
266
- ieeeAddr = installCode.substring(keyStart - 19, keyStart - 3);
267
- keyStr = installCode.substring(keyStart, installCode.length);
268
- }
269
-
270
- ieeeAddr = `0x${ieeeAddr}`;
271
- // match valid else asserted above
272
- // biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress`
250
+ // will throw if code cannot be parsed
251
+ const [ieeeAddr, keyStr] = parseInstallCode(installCode);
252
+ // biome-ignore lint/style/noNonNullAssertion: valid from above parsing
273
253
  const key = Buffer.from(keyStr.match(/.{1,2}/g)!.map((d) => Number.parseInt(d, 16)));
274
-
275
254
  // will throw if code cannot be fixed and is invalid
276
- const [adjustedKey, adjusted] = ZSpec.Utils.checkInstallCode(key, true);
255
+ const [adjustedKey, adjusted] = checkInstallCode(key, true);
277
256
 
278
257
  if (adjusted) {
279
258
  logger.info(`Install code was adjusted for reason '${adjusted}'.`, NS);
@@ -0,0 +1,107 @@
1
+ import {INSTALL_CODE_CRC_SIZE, INSTALL_CODE_SIZES} from "../../zspec/consts";
2
+ import {crc16X25} from "../../zspec/utils";
3
+
4
+ /**
5
+ * Parse the given code using known formats:
6
+ * - 95 or 91-length
7
+ * - Widely adopted (Ubisys, Danfoss, Inovelli, Ledvance): ...Z:<ieee>$I:<key>...
8
+ * - Pipe-separated (Muller-Licht, Innr): <ieee>|<key>
9
+ * - Aqara: G$M:...$A:<ieee>$I:<key>
10
+ * - Hue: HUE:Z:<key> M:<ieee>...
11
+ * @param installCode
12
+ * @returns
13
+ * - the IEEE address
14
+ * - the raw key
15
+ */
16
+ export function parseInstallCode(installCode: string): [ieeeAddr: string, key: string] {
17
+ const widelyAdoptedMatch = installCode.match(/Z:([a-zA-Z0-9]{16})\$I:([a-zA-Z0-9]+)/);
18
+
19
+ if (widelyAdoptedMatch) {
20
+ return [`0x${widelyAdoptedMatch[1].toLowerCase()}`, widelyAdoptedMatch[2]];
21
+ }
22
+
23
+ const pipeMatch = installCode.match(/^([a-zA-Z0-9]{16})\|([a-zA-Z0-9]+)$/);
24
+
25
+ if (pipeMatch) {
26
+ return [`0x${pipeMatch[1].toLowerCase()}`, pipeMatch[2]];
27
+ }
28
+
29
+ const aqaraMatch = installCode.match(/^G\$M:.+\$A:([a-zA-Z0-9]{16})\$I:([a-zA-Z0-9]+)$/);
30
+
31
+ if (aqaraMatch) {
32
+ return [`0x${aqaraMatch[1].toLowerCase()}`, aqaraMatch[2]];
33
+ }
34
+
35
+ const hueMatch = installCode.match(/^HUE:Z:([a-zA-Z0-9]+) M:([a-zA-Z0-9]{16})/);
36
+
37
+ if (hueMatch) {
38
+ return [`0x${hueMatch[2].toLowerCase()}`, hueMatch[1]];
39
+ }
40
+
41
+ if (installCode.length === 95 || installCode.length === 91) {
42
+ const keyStart = installCode.length - (installCode.length === 95 ? 36 : 32);
43
+
44
+ return [`0x${installCode.substring(keyStart - 19, keyStart - 3).toLowerCase()}`, installCode.substring(keyStart, installCode.length)];
45
+ }
46
+
47
+ throw new Error(`Unsupported install code, got ${installCode.length} chars, expected 95 or 91 chars, or known format`);
48
+ }
49
+
50
+ /**
51
+ * Check if install code (little-endian) is valid, and if not, and requested, fix it.
52
+ *
53
+ * WARNING: Due to conflicting sizes between 8-length code with invalid CRC, and 10-length code missing CRC, given 8-length codes are always assumed to be 8-length code with invalid CRC (most probable scenario).
54
+ *
55
+ * @param code The code to check. Reference is not modified by this procedure but is returned when code was valid, as `outCode`.
56
+ * @param adjust If false, throws if the install code is invalid, otherwise try to fix it (CRC)
57
+ * @returns
58
+ * - The adjusted code, or `code` if not adjusted.
59
+ * - If adjust is false, undefined, otherwise, the reason why the code needed adjusting or undefined if not.
60
+ * - Throws when adjust=false and invalid, or cannot fix.
61
+ */
62
+ export function checkInstallCode(code: Buffer, adjust = true): [outCode: Buffer, adjusted: "invalid CRC" | "missing CRC" | undefined] {
63
+ const crcLowByteIndex = code.length - INSTALL_CODE_CRC_SIZE;
64
+ const crcHighByteIndex = code.length - INSTALL_CODE_CRC_SIZE + 1;
65
+
66
+ for (const codeSize of INSTALL_CODE_SIZES) {
67
+ if (code.length === codeSize) {
68
+ // install code has CRC, check if valid, if not, replace it
69
+ const crc = crc16X25(code.subarray(0, -2));
70
+ const crcHighByte = (crc >> 8) & 0xff;
71
+ const crcLowByte = crc & 0xff;
72
+
73
+ if (code[crcLowByteIndex] !== crcLowByte || code[crcHighByteIndex] !== crcHighByte) {
74
+ // see WARNING above, 8 is smallest valid length, so always ends up here
75
+ if (adjust) {
76
+ const outCode = Buffer.from(code);
77
+ outCode[crcLowByteIndex] = crcLowByte;
78
+ outCode[crcHighByteIndex] = crcHighByte;
79
+
80
+ return [outCode, "invalid CRC"];
81
+ }
82
+
83
+ throw new Error(`Install code ${code.toString("hex")} failed CRC validation`);
84
+ }
85
+
86
+ return [code, undefined];
87
+ }
88
+
89
+ if (code.length === codeSize - INSTALL_CODE_CRC_SIZE) {
90
+ if (adjust) {
91
+ // install code is missing CRC
92
+ const crc = crc16X25(code);
93
+ const outCode = Buffer.alloc(code.length + INSTALL_CODE_CRC_SIZE);
94
+
95
+ code.copy(outCode, 0);
96
+ outCode.writeUInt16LE(crc, code.length);
97
+
98
+ return [outCode, "missing CRC"];
99
+ }
100
+
101
+ throw new Error(`Install code ${code.toString("hex")} failed CRC validation`);
102
+ }
103
+ }
104
+
105
+ // never returned from within the above loop
106
+ throw new Error(`Install code ${code.toString("hex")} has invalid size`);
107
+ }