zigbee-herdsman 4.3.1 → 4.4.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.
Files changed (36) hide show
  1. package/.release-please-manifest.json +1 -1
  2. package/CHANGELOG.md +20 -0
  3. package/dist/adapter/deconz/adapter/deconzAdapter.d.ts.map +1 -1
  4. package/dist/adapter/deconz/adapter/deconzAdapter.js +8 -5
  5. package/dist/adapter/deconz/adapter/deconzAdapter.js.map +1 -1
  6. package/dist/adapter/deconz/driver/constants.d.ts +0 -1
  7. package/dist/adapter/deconz/driver/constants.d.ts.map +1 -1
  8. package/dist/adapter/deconz/driver/constants.js.map +1 -1
  9. package/dist/adapter/deconz/driver/frameParser.d.ts.map +1 -1
  10. package/dist/adapter/deconz/driver/frameParser.js +108 -30
  11. package/dist/adapter/deconz/driver/frameParser.js.map +1 -1
  12. package/dist/controller/controller.d.ts.map +1 -1
  13. package/dist/controller/controller.js +5 -22
  14. package/dist/controller/controller.js.map +1 -1
  15. package/dist/controller/helpers/installCodes.d.ts +27 -0
  16. package/dist/controller/helpers/installCodes.d.ts.map +1 -0
  17. package/dist/controller/helpers/installCodes.js +90 -0
  18. package/dist/controller/helpers/installCodes.js.map +1 -0
  19. package/dist/zspec/utils.d.ts +0 -13
  20. package/dist/zspec/utils.d.ts.map +1 -1
  21. package/dist/zspec/utils.js +0 -49
  22. package/dist/zspec/utils.js.map +1 -1
  23. package/dist/zspec/zcl/definition/cluster.d.ts.map +1 -1
  24. package/dist/zspec/zcl/definition/cluster.js +6 -0
  25. package/dist/zspec/zcl/definition/cluster.js.map +1 -1
  26. package/package.json +1 -1
  27. package/src/adapter/deconz/adapter/deconzAdapter.ts +8 -5
  28. package/src/adapter/deconz/driver/constants.ts +0 -1
  29. package/src/adapter/deconz/driver/frameParser.ts +132 -32
  30. package/src/controller/controller.ts +5 -26
  31. package/src/controller/helpers/installCodes.ts +107 -0
  32. package/src/zspec/utils.ts +1 -60
  33. package/src/zspec/zcl/definition/cluster.ts +6 -0
  34. package/test/controller.test.ts +82 -5
  35. package/test/utils.test.ts +58 -7
  36. 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.1",
66
+ "version": "4.4.0",
67
67
  "scripts": {
68
68
  "build": "tsc",
69
69
  "build:watch": "tsc -w",
@@ -252,11 +252,12 @@ export class DeconzAdapter extends Adapter {
252
252
  ): Promise<ZdoTypes.RequestToResponseMap[K] | undefined> {
253
253
  const transactionID = this.nextTransactionID();
254
254
  payload[0] = transactionID;
255
- let txOptions = 0;
255
+ const txOptions = 0;
256
256
 
257
- if (networkAddress < NwkBroadcastAddress.BroadcastLowPowerRouters) {
258
- txOptions = 0x4; // enable APS ACKs for unicast addresses
259
- }
257
+ // TODO(mpi): Disable APS ACKs for now until we find a better solution to not block queues.
258
+ //if (networkAddress < NwkBroadcastAddress.BroadcastLowPowerRouters) {
259
+ // txOptions = 0x4; // enable APS ACKs for unicast addresses
260
+ //}
260
261
 
261
262
  const isNwkAddrRequest = clusterId === Zdo.ClusterId.NETWORK_ADDRESS_REQUEST;
262
263
  const req: ApsDataRequest = {
@@ -361,7 +362,9 @@ export class DeconzAdapter extends Adapter {
361
362
  const payload = zclFrame.toBuffer();
362
363
 
363
364
  // TODO(mpi): Enable APS ACKs for tricky devices, maintain a list of those, or keep at least a few slots free for non APS ACK requests.
364
- const txOptions = 0x4; // 0x00 normal, 0x04 APS ACK
365
+ //const txOptions = 0x4; // 0x00 normal, 0x04 APS ACK
366
+ // TODO(mpi): Disable APS ACKs for now until we find a better solution to not block queues.
367
+ const txOptions = 0;
365
368
 
366
369
  const request: ApsDataRequest = {
367
370
  requestId: transactionID,
@@ -196,7 +196,6 @@ interface ReceivedDataResponse {
196
196
  }
197
197
 
198
198
  interface GpDataInd {
199
- rspId: number;
200
199
  seqNr: number;
201
200
  id: number;
202
201
  options: number;
@@ -438,43 +438,141 @@ function parseDeviceStateChangedNotification(view: DataView): number | null {
438
438
  }
439
439
  }
440
440
 
441
+ // The ApplicationID sub-field contains the information about the application used by the GPD.
442
+ // ApplicationID = 0b000 indicates the GPD_ID field has the length of 4B and contains the GPD SrcID.
443
+ // ApplicationID = 0b010 indicates the GPD_ID field has the length of 8B and contains the GPD IEEE address.
444
+ enum GpApplicationId {
445
+ SrcId4B = 0,
446
+ Lped = 1,
447
+ Ieee8B = 2,
448
+ }
449
+
450
+ enum GpSecurityLevel {
451
+ NoSecurity = 0,
452
+ // TODO(mpi): "Reserved" is noted in the available spec but the code defined it as follows:
453
+ Reserved = 1, // 0b01 1LSB of frame counter and short (2B) MIC only
454
+ FrameCounter4BMic4B = 2,
455
+ EncryptionFrameCounter4BMic4B = 3,
456
+ }
457
+
458
+ enum ZgpConstants {
459
+ GpNwkProtocolVersion = 3,
460
+ GpNwkDataFrame = 0,
461
+ GpNwkMaintenanceFrame = 1,
462
+ GpMinMsduSize = 1,
463
+ GpAutoCommissioningFlag = 1 << 6,
464
+ GpNwkFrameControlExtensionFlag = 1 << 7,
465
+ }
466
+
441
467
  function parseGreenPowerDataIndication(view: DataView): GpDataInd | null {
442
468
  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));
469
+ let srcId = 0;
470
+ let frameCounter = 0;
471
+ let commandId = 0;
472
+ let commandFrameSize = 0;
473
+ let commandFrame: Buffer | undefined;
474
+
475
+ const buf = new Buffalo(Buffer.from(view.buffer));
476
+
477
+ const _fwCommandId = buf.readUInt8();
478
+ const seqNr = buf.readUInt8();
479
+ const fwStatus = buf.readUInt8();
480
+
481
+ if (fwStatus !== CommandStatus.Success) {
482
+ return null;
483
+ }
484
+
485
+ const _frameLength = buf.readUInt16();
486
+ const _payloadLength = buf.readUInt16();
487
+
488
+ // payload
489
+
490
+ // implementation ported from deCONZ GP code
491
+ const nwkFrameControl = buf.readUInt8();
492
+
493
+ // check frame type
494
+ const frameType = nwkFrameControl & 0x03;
495
+
496
+ if (frameType !== ZgpConstants.GpNwkDataFrame && frameType !== ZgpConstants.GpNwkMaintenanceFrame) {
497
+ return null;
498
+ }
499
+
500
+ // check green power protocol version
501
+ if (((nwkFrameControl >> 2) & 0x03) !== ZgpConstants.GpNwkProtocolVersion) {
502
+ return null;
503
+ }
504
+
505
+ // extended frame control
506
+ let nwkExtFrameControl = 0;
507
+ const hasExtensionFlag = nwkFrameControl & ZgpConstants.GpNwkFrameControlExtensionFlag;
508
+ if (hasExtensionFlag) {
509
+ nwkExtFrameControl = buf.readUInt8();
510
+ }
511
+
512
+ const options = nwkExtFrameControl | (nwkFrameControl << 8);
513
+
514
+ const applicationId = nwkExtFrameControl & 7;
515
+ const securityLevel = (nwkExtFrameControl >> 3) & 3;
516
+
517
+ if (applicationId !== GpApplicationId.SrcId4B && applicationId !== GpApplicationId.Ieee8B) {
518
+ return null; // NOTE: GpApplicationId.Lped (1) should be dropped as per spec
519
+ }
520
+
521
+ // The GPDSrcID field is present if the Frame Type sub-field is set to 0b00 and the ApplicationID sub-
522
+ // field of the Extended NWK Frame Control field is set to 0b000 (or not present).
523
+ if (
524
+ applicationId === GpApplicationId.SrcId4B &&
525
+ frameType === ZgpConstants.GpNwkDataFrame /*|| (frameType === ZgpConstants.GpNwkMaintenanceFrame && hasExtensionFlag) */
526
+ ) {
527
+ srcId = buf.readUInt32();
528
+ }
529
+ // TODO(mpi): for applicationId == GpApplicationId.Ieee8B:
530
+ // currently Ieee addresses aren't supported, do they actually appear?!
531
+ // these need be extracted from MAC header which we don't have here (this is only the NWK payload).
532
+
533
+ // frame counter filed
534
+ frameCounter = 0;
535
+ let micSize = 0;
536
+
537
+ if (hasExtensionFlag && frameType === ZgpConstants.GpNwkDataFrame) {
538
+ if (applicationId === GpApplicationId.Ieee8B) {
539
+ const _endpoint = buf.readUInt8();
540
+ }
541
+ // If the SecurityLevel is set to 0b00, the SecurityKey sub-field is ignored on reception, and the
542
+ // fields Security frame counter and MIC are not present.
543
+ if (securityLevel === GpSecurityLevel.Reserved) {
544
+ micSize = 2; // TODO(mpi) does this actually exists? Check recent specs!
545
+ } else if (securityLevel === GpSecurityLevel.FrameCounter4BMic4B || securityLevel === GpSecurityLevel.EncryptionFrameCounter4BMic4B) {
546
+ frameCounter = buf.readUInt32();
547
+ micSize = 4;
548
+ }
549
+ }
550
+
551
+ if (!buf.isMore()) {
552
+ return null;
553
+ }
554
+
555
+ if (applicationId === GpApplicationId.SrcId4B || applicationId === GpApplicationId.Ieee8B) {
556
+ commandId = buf.readUInt8();
557
+ commandFrameSize = buf.getBuffer().length - buf.getPosition() - micSize;
558
+ //logger.debug(`GPD payload length: ${commandFrameSize}, mic size: ${micSize}`, NS);
559
+ if (commandFrameSize < 0) {
560
+ logger.error(`GPD payload length < 0: ${commandFrameSize}`, NS);
561
+ return null;
562
+ }
563
+ commandFrame = Buffer.from(buf.readBuffer(commandFrameSize)); // copy
564
+ }
565
+
566
+ // NOTE(mpi): The old adapter treated (view.byteLength < 30) as notification, larger as commissioning?!
567
+ // The controller thus rejected commissioning frames.
568
+ const id = 0; // 0 = notification, 4 = commissioning
569
+
570
+ if (commandFrame === undefined) {
571
+ logger.debug("GPD discard frame since commandFrame is null?!", NS);
572
+ return null;
474
573
  }
475
574
 
476
575
  const ind: GpDataInd = {
477
- rspId,
478
576
  seqNr,
479
577
  id,
480
578
  options,
@@ -485,6 +583,7 @@ function parseGreenPowerDataIndication(view: DataView): GpDataInd | null {
485
583
  commandFrame,
486
584
  };
487
585
 
586
+ // TODO(mpi): This only tracks one frame, might be a bit optimistic
488
587
  if (!(lastReceivedGpInd.srcId === srcId && lastReceivedGpInd.commandId === commandId && lastReceivedGpInd.frameCounter === frameCounter)) {
489
588
  lastReceivedGpInd.srcId = srcId;
490
589
  lastReceivedGpInd.commandId = commandId;
@@ -503,6 +602,7 @@ function parseGreenPowerDataIndication(view: DataView): GpDataInd | null {
503
602
  return null;
504
603
  }
505
604
  }
605
+
506
606
  function parseMacPollCommand(_view: DataView): number {
507
607
  //logger.debug("Received command MAC_POLL", NS);
508
608
  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
+ }
@@ -1,5 +1,5 @@
1
1
  import {createCipheriv} from "node:crypto";
2
- import {AES_MMO_128_BLOCK_SIZE, ALL_802_15_4_CHANNELS, INSTALL_CODE_CRC_SIZE, INSTALL_CODE_SIZES} from "./consts";
2
+ import {AES_MMO_128_BLOCK_SIZE, ALL_802_15_4_CHANNELS} from "./consts";
3
3
  import {BroadcastAddress} from "./enums";
4
4
  import type {Eui64} from "./tstypes";
5
5
 
@@ -245,62 +245,3 @@ export function aes128MmoHash(data: Buffer): Buffer {
245
245
 
246
246
  return result;
247
247
  }
248
-
249
- /**
250
- * Check if install code (little-endian) is valid, and if not, and requested, fix it.
251
- *
252
- * 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).
253
- *
254
- * @param code The code to check. Reference is not modified by this procedure but is returned when code was valid, as `outCode`.
255
- * @param adjust If false, throws if the install code is invalid, otherwise try to fix it (CRC)
256
- * @returns
257
- * - The adjusted code, or `code` if not adjusted.
258
- * - If adjust is false, undefined, otherwise, the reason why the code needed adjusting or undefined if not.
259
- * - Throws when adjust=false and invalid, or cannot fix.
260
- */
261
- export function checkInstallCode(code: Buffer, adjust = true): [outCode: Buffer, adjusted: "invalid CRC" | "missing CRC" | undefined] {
262
- const crcLowByteIndex = code.length - INSTALL_CODE_CRC_SIZE;
263
- const crcHighByteIndex = code.length - INSTALL_CODE_CRC_SIZE + 1;
264
-
265
- for (const codeSize of INSTALL_CODE_SIZES) {
266
- if (code.length === codeSize) {
267
- // install code has CRC, check if valid, if not, replace it
268
- const crc = crc16X25(code.subarray(0, -2));
269
- const crcHighByte = (crc >> 8) & 0xff;
270
- const crcLowByte = crc & 0xff;
271
-
272
- if (code[crcLowByteIndex] !== crcLowByte || code[crcHighByteIndex] !== crcHighByte) {
273
- // see WARNING above, 8 is smallest valid length, so always ends up here
274
- if (adjust) {
275
- const outCode = Buffer.from(code);
276
- outCode[crcLowByteIndex] = crcLowByte;
277
- outCode[crcHighByteIndex] = crcHighByte;
278
-
279
- return [outCode, "invalid CRC"];
280
- }
281
-
282
- throw new Error(`Install code ${code.toString("hex")} failed CRC validation`);
283
- }
284
-
285
- return [code, undefined];
286
- }
287
-
288
- if (code.length === codeSize - INSTALL_CODE_CRC_SIZE) {
289
- if (adjust) {
290
- // install code is missing CRC
291
- const crc = crc16X25(code);
292
- const outCode = Buffer.alloc(code.length + INSTALL_CODE_CRC_SIZE);
293
-
294
- code.copy(outCode, 0);
295
- outCode.writeUInt16LE(crc, code.length);
296
-
297
- return [outCode, "missing CRC"];
298
- }
299
-
300
- throw new Error(`Install code ${code.toString("hex")} failed CRC validation`);
301
- }
302
- }
303
-
304
- // never returned from within the above loop
305
- throw new Error(`Install code ${code.toString("hex")} has invalid size`);
306
- }
@@ -1992,6 +1992,9 @@ export const Clusters: Readonly<Record<ClusterName, Readonly<ClusterDefinition>>
1992
1992
  danfossRoomFloorSensorMode: {ID: 0x4120, type: DataType.ENUM8, manufacturerCode: ManufacturerCode.DANFOSS_A_S},
1993
1993
  danfossFloorMinSetpoint: {ID: 0x4121, type: DataType.INT16, manufacturerCode: ManufacturerCode.DANFOSS_A_S},
1994
1994
  danfossFloorMaxSetpoint: {ID: 0x4122, type: DataType.INT16, manufacturerCode: ManufacturerCode.DANFOSS_A_S},
1995
+ danfossScheduleTypeUsed: {ID: 0x4130, type: DataType.ENUM8, manufacturerCode: ManufacturerCode.DANFOSS_A_S},
1996
+ danfossIcon2PreHeat: {ID: 0x4131, type: DataType.ENUM8, manufacturerCode: ManufacturerCode.DANFOSS_A_S},
1997
+ danfossIcon2PreHeatStatus: {ID: 0x414f, type: DataType.ENUM8, manufacturerCode: ManufacturerCode.DANFOSS_A_S},
1995
1998
  elkoLoad: {ID: 0x0401, type: DataType.UINT16},
1996
1999
  elkoDisplayText: {ID: 0x0402, type: DataType.CHAR_STR},
1997
2000
  elkoSensor: {ID: 0x0403, type: DataType.ENUM8},
@@ -4280,8 +4283,11 @@ export const Clusters: Readonly<Record<ClusterName, Readonly<ClusterDefinition>>
4280
4283
  lastMessageLqi: {ID: 284, type: DataType.UINT8},
4281
4284
  lastMessageRssi: {ID: 285, type: DataType.INT8},
4282
4285
  danfossSystemStatusCode: {ID: 0x4000, type: DataType.BITMAP16, manufacturerCode: ManufacturerCode.DANFOSS_A_S},
4286
+ danfossHeatSupplyRequest: {ID: 0x4031, type: DataType.ENUM8, manufacturerCode: ManufacturerCode.DANFOSS_A_S},
4283
4287
  danfossSystemStatusWater: {ID: 0x4200, type: DataType.ENUM8, manufacturerCode: ManufacturerCode.DANFOSS_A_S},
4284
4288
  danfossMultimasterRole: {ID: 0x4201, type: DataType.ENUM8, manufacturerCode: ManufacturerCode.DANFOSS_A_S},
4289
+ danfossIconApplication: {ID: 0x4210, type: DataType.ENUM8, manufacturerCode: ManufacturerCode.DANFOSS_A_S},
4290
+ danfossIconForcedHeatingCooling: {ID: 0x4220, type: DataType.ENUM8, manufacturerCode: ManufacturerCode.DANFOSS_A_S},
4285
4291
  schneiderMeterStatus: {ID: 0xff01, type: DataType.UINT32, manufacturerCode: ManufacturerCode.SCHNEIDER_ELECTRIC},
4286
4292
  schneiderDiagnosticRegister1: {ID: 0xff02, type: DataType.UINT32, manufacturerCode: ManufacturerCode.SCHNEIDER_ELECTRIC},
4287
4293
  schneiderCommunicationQuality: {ID: 0x4000, type: DataType.UINT8, manufacturerCode: ManufacturerCode.SCHNEIDER_ELECTRIC},
@@ -1839,7 +1839,7 @@ describe("Controller", () => {
1839
1839
  await controller.addInstallCode(code);
1840
1840
  expect(mockAddInstallCode).toHaveBeenCalledTimes(1);
1841
1841
  expect(mockAddInstallCode).toHaveBeenCalledWith(
1842
- "0x9035EAFFFE424783",
1842
+ "0x9035eafffe424783",
1843
1843
  Buffer.from([0xae, 0x3b, 0x28, 0x72, 0x81, 0xcf, 0x16, 0xf5, 0x50, 0x73, 0x3a, 0x0c, 0xec, 0x38, 0xaa, 0x31, 0xe8, 0x02]),
1844
1844
  false,
1845
1845
  );
@@ -1851,37 +1851,102 @@ describe("Controller", () => {
1851
1851
  await controller.addInstallCode(code);
1852
1852
  expect(mockAddInstallCode).toHaveBeenCalledTimes(2);
1853
1853
  expect(mockAddInstallCode).toHaveBeenCalledWith(
1854
- "0x000D6F00179F2BC9",
1854
+ "0x000d6f00179f2bc9",
1855
1855
  Buffer.from([0xd0, 0xf4, 0x71, 0xc9, 0xbb, 0xa2, 0xc0, 0x20, 0x86, 0x08, 0xe9, 0x1e, 0xed, 0x17, 0xe2, 0xb1, 0x9a, 0xec]),
1856
1856
  false,
1857
1857
  );
1858
1858
  expect(mockAddInstallCode).toHaveBeenCalledWith(
1859
- "0x000D6F00179F2BC9",
1859
+ "0x000d6f00179f2bc9",
1860
1860
  Buffer.from([0xd0, 0xf4, 0x71, 0xc9, 0xbb, 0xa2, 0xc0, 0x20, 0x86, 0x08, 0xe9, 0x1e, 0xed, 0x17, 0xe2, 0xb1]),
1861
1861
  true,
1862
1862
  );
1863
1863
  expect(mockLogger.info).toHaveBeenCalledWith(`Install code was adjusted for reason 'missing CRC'.`, "zh:controller");
1864
1864
  });
1865
1865
 
1866
+ it("Add install code widely adopted format", async () => {
1867
+ await controller.start();
1868
+ // inovelli
1869
+ const code = "Z:6C5CB1FFFE44FDFD$I:5492072F8DE72829FEE139CF8ACA4F43EF21";
1870
+ await controller.addInstallCode(code);
1871
+ expect(mockAddInstallCode).toHaveBeenNthCalledWith(
1872
+ 1,
1873
+ "0x6c5cb1fffe44fdfd",
1874
+ Buffer.from([
1875
+ 0x54, 0x92, 0x07, 0x2f, 0x8d, 0xe7, 0x28, 0x29, 0xfe, 0xe1, 0x39, 0xcf, 0x8a, 0xca, 0x4f, 0x43, /*0xef, 0x21 bad CRC?*/ 0x85, 0x44,
1876
+ ]),
1877
+ false,
1878
+ );
1879
+ // danfoss
1880
+ const code2 = "G$M:IC2%Z:540F57FFFE599FAA$I:D79CB21C6D197CE7A3339A683A90DFF2442A%M:1246";
1881
+ await controller.addInstallCode(code2);
1882
+ expect(mockAddInstallCode).toHaveBeenNthCalledWith(
1883
+ 2,
1884
+ "0x540f57fffe599faa",
1885
+ Buffer.from([0xd7, 0x9c, 0xb2, 0x1c, 0x6d, 0x19, 0x7c, 0xe7, 0xa3, 0x33, 0x9a, 0x68, 0x3a, 0x90, 0xdf, 0xf2, 0x44, 0x2a]),
1886
+ false,
1887
+ );
1888
+ // ??
1889
+ const code3 = "Z:4CC206FFFE306C43$I:150A1A57D11A1A01362622DBA97ACF0F9185$D:202%B:4CC206306C43$P:983639%M:1220$F:002D";
1890
+ await controller.addInstallCode(code3);
1891
+ expect(mockAddInstallCode).toHaveBeenNthCalledWith(
1892
+ 3,
1893
+ "0x4cc206fffe306c43",
1894
+ Buffer.from([
1895
+ 0x15, 0x0a, 0x1a, 0x57, 0xd1, 0x1a, 0x1a, 0x01, 0x36, 0x26, 0x22, 0xdb, 0xa9, 0x7a, 0xcf, 0x0f, /*0x91, 0x85 bad CRC?*/ 0x0c, 0xca,
1896
+ ]),
1897
+ false,
1898
+ );
1899
+ // ubisys
1900
+ const code4 = "Z:0102030405060708$I:0102030405060708090A0B0C0D0E0F1090FD%G$M:S1-R%M:10F2";
1901
+ await controller.addInstallCode(code4);
1902
+ expect(mockAddInstallCode).toHaveBeenNthCalledWith(
1903
+ 4,
1904
+ "0x0102030405060708",
1905
+ Buffer.from([0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x90, 0xfd]),
1906
+ false,
1907
+ );
1908
+ // ledvance
1909
+ const code5 = "Z:F0D1B8000017389A$I:6AB6973274EE6F720200530162754044C930%M:1189$D:008B452720";
1910
+ await controller.addInstallCode(code5);
1911
+ expect(mockAddInstallCode).toHaveBeenNthCalledWith(
1912
+ 5,
1913
+ "0xf0d1b8000017389a",
1914
+ Buffer.from([0x6a, 0xb6, 0x97, 0x32, 0x74, 0xee, 0x6f, 0x72, 0x02, 0x00, 0x53, 0x01, 0x62, 0x75, 0x40, 0x44, 0xc9, 0x30]),
1915
+ false,
1916
+ );
1917
+ });
1918
+
1866
1919
  it("Add install code Aqara", async () => {
1867
1920
  await controller.start();
1868
1921
  const code = "G$M:69775$S:680S00003915$D:0000000017B2335C%Z$A:54EF44100006E7DF$I:3313A005E177A647FC7925620AB207C4BEF5";
1869
1922
  await controller.addInstallCode(code);
1870
1923
  expect(mockAddInstallCode).toHaveBeenCalledTimes(1);
1871
1924
  expect(mockAddInstallCode).toHaveBeenCalledWith(
1872
- "0x54EF44100006E7DF",
1925
+ "0x54ef44100006e7df",
1873
1926
  Buffer.from([0x33, 0x13, 0xa0, 0x05, 0xe1, 0x77, 0xa6, 0x47, 0xfc, 0x79, 0x25, 0x62, 0x0a, 0xb2, 0x07, 0xc4, 0xbe, 0xf5]),
1874
1927
  false,
1875
1928
  );
1876
1929
  });
1877
1930
 
1931
+ it("Add install code Hue", async () => {
1932
+ await controller.start();
1933
+ const code = "HUE:Z:0123456789ABCDEF0123456789ABCDEF0123 M:0123456789ABCDEF D:L3B A:1184";
1934
+ await controller.addInstallCode(code);
1935
+ expect(mockAddInstallCode).toHaveBeenCalledTimes(1);
1936
+ expect(mockAddInstallCode).toHaveBeenCalledWith(
1937
+ "0x0123456789abcdef",
1938
+ Buffer.from([0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, 0xe7, 0xb8]),
1939
+ false,
1940
+ );
1941
+ });
1942
+
1878
1943
  it("Add install code pipe", async () => {
1879
1944
  await controller.start();
1880
1945
  const code = "54EF44100006E7DF|3313A005E177A647FC7925620AB207C4BEF5";
1881
1946
  await controller.addInstallCode(code);
1882
1947
  expect(mockAddInstallCode).toHaveBeenCalledTimes(1);
1883
1948
  expect(mockAddInstallCode).toHaveBeenCalledWith(
1884
- "0x54EF44100006E7DF",
1949
+ "0x54ef44100006e7df",
1885
1950
  Buffer.from([0x33, 0x13, 0xa0, 0x05, 0xe1, 0x77, 0xa6, 0x47, 0xfc, 0x79, 0x25, 0x62, 0x0a, 0xb2, 0x07, 0xc4, 0xbe, 0xf5]),
1886
1951
  false,
1887
1952
  );
@@ -1897,6 +1962,18 @@ describe("Controller", () => {
1897
1962
  expect(mockAddInstallCode).toHaveBeenCalledTimes(0);
1898
1963
  });
1899
1964
 
1965
+ it("Add install code unknown format", async () => {
1966
+ await controller.start();
1967
+
1968
+ const code = "54EF44100006E7DF`3313A005E177A647FC7925620AB207";
1969
+
1970
+ await expect(controller.addInstallCode(code)).rejects.toThrow(
1971
+ "Unsupported install code, got 47 chars, expected 95 or 91 chars, or known format",
1972
+ );
1973
+
1974
+ expect(mockAddInstallCode).toHaveBeenCalledTimes(0);
1975
+ });
1976
+
1900
1977
  it("Controller permit joining all, disabled automatically", async () => {
1901
1978
  await controller.start();
1902
1979