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.
- package/.release-please-manifest.json +1 -1
- package/CHANGELOG.md +20 -0
- package/dist/adapter/deconz/adapter/deconzAdapter.d.ts.map +1 -1
- package/dist/adapter/deconz/adapter/deconzAdapter.js +8 -5
- package/dist/adapter/deconz/adapter/deconzAdapter.js.map +1 -1
- package/dist/adapter/deconz/driver/constants.d.ts +0 -1
- package/dist/adapter/deconz/driver/constants.d.ts.map +1 -1
- package/dist/adapter/deconz/driver/constants.js.map +1 -1
- package/dist/adapter/deconz/driver/frameParser.d.ts.map +1 -1
- package/dist/adapter/deconz/driver/frameParser.js +108 -30
- package/dist/adapter/deconz/driver/frameParser.js.map +1 -1
- package/dist/controller/controller.d.ts.map +1 -1
- package/dist/controller/controller.js +5 -22
- package/dist/controller/controller.js.map +1 -1
- package/dist/controller/helpers/installCodes.d.ts +27 -0
- package/dist/controller/helpers/installCodes.d.ts.map +1 -0
- package/dist/controller/helpers/installCodes.js +90 -0
- package/dist/controller/helpers/installCodes.js.map +1 -0
- package/dist/zspec/utils.d.ts +0 -13
- package/dist/zspec/utils.d.ts.map +1 -1
- package/dist/zspec/utils.js +0 -49
- package/dist/zspec/utils.js.map +1 -1
- package/dist/zspec/zcl/definition/cluster.d.ts.map +1 -1
- package/dist/zspec/zcl/definition/cluster.js +6 -0
- package/dist/zspec/zcl/definition/cluster.js.map +1 -1
- package/package.json +1 -1
- package/src/adapter/deconz/adapter/deconzAdapter.ts +8 -5
- package/src/adapter/deconz/driver/constants.ts +0 -1
- package/src/adapter/deconz/driver/frameParser.ts +132 -32
- package/src/controller/controller.ts +5 -26
- package/src/controller/helpers/installCodes.ts +107 -0
- package/src/zspec/utils.ts +1 -60
- package/src/zspec/zcl/definition/cluster.ts +6 -0
- package/test/controller.test.ts +82 -5
- package/test/utils.test.ts +58 -7
- package/test/zspec/utils.test.ts +0 -64
package/package.json
CHANGED
|
@@ -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
|
-
|
|
255
|
+
const txOptions = 0;
|
|
256
256
|
|
|
257
|
-
|
|
258
|
-
|
|
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,
|
|
@@ -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
|
|
444
|
-
let
|
|
445
|
-
let
|
|
446
|
-
let
|
|
447
|
-
let
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
const
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
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
|
-
|
|
250
|
-
const
|
|
251
|
-
|
|
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] =
|
|
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
|
+
}
|
package/src/zspec/utils.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import {createCipheriv} from "node:crypto";
|
|
2
|
-
import {AES_MMO_128_BLOCK_SIZE, ALL_802_15_4_CHANNELS
|
|
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},
|
package/test/controller.test.ts
CHANGED
|
@@ -1839,7 +1839,7 @@ describe("Controller", () => {
|
|
|
1839
1839
|
await controller.addInstallCode(code);
|
|
1840
1840
|
expect(mockAddInstallCode).toHaveBeenCalledTimes(1);
|
|
1841
1841
|
expect(mockAddInstallCode).toHaveBeenCalledWith(
|
|
1842
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
|