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.
- package/.release-please-manifest.json +1 -1
- package/CHANGELOG.md +20 -0
- package/dist/adapter/deconz/driver/constants.d.ts +1 -2
- package/dist/adapter/deconz/driver/constants.d.ts.map +1 -1
- package/dist/adapter/deconz/driver/constants.js +1 -0
- package/dist/adapter/deconz/driver/constants.js.map +1 -1
- package/dist/adapter/deconz/driver/driver.d.ts +5 -1
- package/dist/adapter/deconz/driver/driver.d.ts.map +1 -1
- package/dist/adapter/deconz/driver/driver.js +92 -52
- package/dist/adapter/deconz/driver/driver.js.map +1 -1
- package/dist/adapter/deconz/driver/frameParser.d.ts.map +1 -1
- package/dist/adapter/deconz/driver/frameParser.js +115 -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/driver/constants.ts +2 -2
- package/src/adapter/deconz/driver/driver.ts +104 -55
- package/src/adapter/deconz/driver/frameParser.ts +139 -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
|
@@ -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
|
-
//
|
|
525
|
-
|
|
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
|
-
|
|
528
|
-
|
|
529
|
-
|
|
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 |
|
|
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
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|