-
Notifications
You must be signed in to change notification settings - Fork 279
/
emberAdapter.ts
3843 lines (3323 loc) · 165 KB
/
emberAdapter.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/* istanbul ignore file */
import Debug from "debug";
import equals from 'fast-deep-equal/es6';
import {fs} from "mz";
import SerialPortUtils from '../../serialPortUtils';
import SocketPortUtils from '../../socketPortUtils';
import {BackupUtils, RealpathSync, Wait} from "../../../utils";
import {Adapter, TsType} from "../..";
import {LoggerStub} from "../../../controller/logger-stub";
import {Backup, UnifiedBackupStorage} from "../../../models";
import {FrameType, Direction, ZclFrame, Foundation} from "../../../zcl";
import Cluster from "../../../zcl/definition/cluster";
import {
DeviceAnnouncePayload,
DeviceJoinedPayload,
DeviceLeavePayload,
Events,
RawDataPayload,
ZclDataPayload
} from "../../events";
import {halCommonCrc16, highByte, highLowToInt, lowByte, lowHighBytes} from "../utils/math";
import {Ezsp, EzspEvents} from "../ezsp/ezsp";
import {
EMBER_ENCRYPTION_KEY_SIZE,
EUI64_SIZE,
EZSP_MAX_FRAME_LENGTH,
EZSP_PROTOCOL_VERSION,
EZSP_STACK_TYPE_MESH
} from "../ezsp/consts";
import {
EzspConfigId,
EzspDecisionBitmask,
EzspDecisionId,
EzspPolicyId,
EzspValueId
} from "../ezsp/enums";
import {EzspBuffalo} from "../ezsp/buffalo";
import {
EmberApsOption,
EmberOutgoingMessageType,
EmberStatus,
EzspStatus,
EmberVersionType,
SLStatus,
SecManFlag,
EmberNodeType,
EmberNetworkStatus,
SecManKeyType,
EmberLeaveRequestFlags,
EmberInterpanMessageType,
EmberSourceRouteDiscoveryMode,
EmberTXPowerMode,
EmberKeepAliveMode,
EmberJoinDecision,
EmberExtendedSecurityBitmask,
EmberInitialSecurityBitmask,
EmberJoinMethod,
EmberNetworkInitBitmask,
EmberDeviceUpdate,
EzspNetworkScanType,
EmberIncomingMessageType,
EmberCounterType,
} from "../enums";
import {
EmberAesMmoHashContext,
EmberApsFrame,
EmberEUI64,
EmberExtendedPanId,
EmberInitialSecurityState,
EmberKeyData,
EmberMulticastId,
EmberMulticastTableEntry,
EmberNetworkInitStruct,
EmberNetworkParameters,
EmberNodeId,
EmberPanId,
EmberVersion,
SecManAPSKeyMetadata,
SecManContext,
SecManKey,
} from "../types";
import {
EmberZdoStatus,
EndDeviceAnnouncePayload,
LQITableResponsePayload,
SimpleDescriptorResponsePayload,
NodeDescriptorResponsePayload,
ActiveEndpointsResponsePayload,
RoutingTableResponsePayload,
ACTIVE_ENDPOINTS_REQUEST,
BINDING_TABLE_REQUEST,
BIND_REQUEST,
IEEE_ADDRESS_REQUEST,
LEAVE_REQUEST,
LQI_TABLE_REQUEST,
MATCH_DESCRIPTORS_REQUEST,
MULTICAST_BINDING,
NETWORK_ADDRESS_REQUEST,
NODE_DESCRIPTOR_REQUEST,
PERMIT_JOINING_REQUEST,
POWER_DESCRIPTOR_REQUEST,
ROUTING_TABLE_REQUEST,
SIMPLE_DESCRIPTOR_REQUEST,
UNBIND_REQUEST,
UNICAST_BINDING,
ZDO_ENDPOINT,
ZDO_MESSAGE_OVERHEAD,
ZDO_PROFILE_ID,
PERMIT_JOINING_RESPONSE,
NODE_DESCRIPTOR_RESPONSE,
LQI_TABLE_RESPONSE,
ROUTING_TABLE_RESPONSE,
ACTIVE_ENDPOINTS_RESPONSE,
SIMPLE_DESCRIPTOR_RESPONSE,
BIND_RESPONSE,
UNBIND_RESPONSE,
LEAVE_RESPONSE
} from "../zdo";
import {
EMBER_BROADCAST_ADDRESS,
EMBER_RX_ON_WHEN_IDLE_BROADCAST_ADDRESS,
EMBER_SLEEPY_BROADCAST_ADDRESS,
EMBER_INSTALL_CODE_CRC_SIZE,
EMBER_INSTALL_CODE_SIZES,
MANUFACTURER_CODE,
EMBER_NUM_802_15_4_CHANNELS,
EMBER_MIN_802_15_4_CHANNEL_NUMBER,
ZIGBEE_COORDINATOR_ADDRESS,
UNKNOWN_NETWORK_STATE,
EMBER_UNKNOWN_NODE_ID,
MAXIMUM_APS_PAYLOAD_LENGTH,
APS_ENCRYPTION_OVERHEAD,
APS_FRAGMENTATION_OVERHEAD,
INVALID_PAN_ID,
LONG_DEST_FRAME_CONTROL,
MAC_ACK_REQUIRED,
MAXIMUM_INTERPAN_LENGTH,
STUB_NWK_FRAME_CONTROL,
TOUCHLINK_PROFILE_ID,
INTERPAN_APS_FRAME_TYPE,
SHORT_DEST_FRAME_CONTROL,
EMBER_HIGH_RAM_CONCENTRATOR,
BLANK_EUI64,
STACK_PROFILE_ZIGBEE_PRO,
SECURITY_LEVEL_Z3,
INVALID_RADIO_CHANNEL,
BLANK_EXTENDED_PAN_ID,
GP_ENDPOINT,
EMBER_ALL_802_15_4_CHANNELS_MASK,
ZIGBEE_PROFILE_INTEROPERABILITY_LINK_KEY,
} from "../consts";
import {EmberRequestQueue} from "./requestQueue";
import {FIXED_ENDPOINTS} from "./endpoints";
import {aesMmoHashInit, initNetworkCache, initSecurityManagerContext} from "../utils/initters";
import {randomBytes} from "crypto";
import {EmberOneWaitress} from "./oneWaitress";
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import {EmberTokensManager} from "./tokensManager";
const debug = Debug('zigbee-herdsman:adapter:ember:adapter');
export type NetworkCache = {
//-- basic network info
eui64: EmberEUI64,
parameters: EmberNetworkParameters,
status: EmberNetworkStatus,
/** uint8_t */
};
/**
*
*/
type ConcentratorConfig = {
/**
* Minimum Time between broadcasts (in seconds) <1-60>
* Default: 10
* The minimum amount of time that must pass between MTORR broadcasts.
*/
minTime: number,
/**
* Maximum Time between broadcasts (in seconds) <30-300>
* Default: 60
* The maximum amount of time that can pass between MTORR broadcasts.
*/
maxTime: number,
/**
* Route Error Threshold <1-100>
* Default: 3
* The number of route errors that will trigger a re-broadcast of the MTORR.
*/
routeErrorThreshold: number,
/**
* Delivery Failure Threshold <1-100>
* Default: 1
* The number of APS delivery failures that will trigger a re-broadcast of the MTORR.
*/
deliveryFailureThreshold: number,
/**
* Maximum number of hops for Broadcast <0-30>
* Default: 0
* The maximum number of hops that the MTORR broadcast will be allowed to have.
* A value of 0 will be converted to the EMBER_MAX_HOPS value set by the stack.
*/
mapHops: number,
};
/**
* Use for a link key backup.
*
* Each entry notes the EUI64 of the device it is paired to and the key data.
* This key may be hashed and not the actual link key currently in use.
*/
type LinkKeyBackupData = {
deviceEui64: EmberEUI64,
key: EmberKeyData,
outgoingFrameCounter: number,
incomingFrameCounter: number,
};
/** Enum to pass strings from numbers up to Z2M. */
enum RoutingTableStatus {
ACTIVE = 0x0,
DISCOVERY_UNDERWAY = 0x1,
DISCOVERY_FAILED = 0x2,
INACTIVE = 0x3,
VALIDATION_UNDERWAY = 0x4,
RESERVED1 = 0x5,
RESERVED2 = 0x6,
RESERVED3 = 0x7,
};
/** Events specific to OneWaitress usage. */
enum OneWaitressEvents {
STACK_STATUS_NETWORK_UP = 'STACK_STATUS_NETWORK_UP',
STACK_STATUS_NETWORK_DOWN = 'STACK_STATUS_NETWORK_DOWN',
STACK_STATUS_NETWORK_OPENED = 'STACK_STATUS_NETWORK_OPENED',
STACK_STATUS_NETWORK_CLOSED = 'STACK_STATUS_NETWORK_CLOSED',
};
enum NetworkInitAction {
/** Ain't that nice! */
DONE,
/** Config mismatch, must leave network. */
LEAVE,
/** Config mismatched, left network. Will evaluate forming from backup or config next. */
LEFT,
/** Form the network using config. No backup, or backup mismatch. */
FORM_CONFIG,
/** Re-form the network using full backed-up data. */
FORM_BACKUP,
};
/** NOTE: Drivers can override `manufacturer`. Verify logic doesn't work in most cases anyway. */
const autoDetectDefinitions = [
/** NOTE: Manuf code "0x1321" for "Shenzhen Sonoff Technologies Co., Ltd." */
{manufacturer: 'ITEAD', vendorId: '1a86', productId: '55d4'},// Sonoff ZBDongle-E
/** NOTE: Manuf code "0x134B" for "Nabu Casa, Inc." */
{manufacturer: 'Nabu Casa', vendorId: '10c4', productId: 'ea60'},// Home Assistant SkyConnect
];
/**
* Config for EMBER_LOW_RAM_CONCENTRATOR type concentrator.
*
* Based on ZigbeeMinimalHost/zigpc
*/
const LOW_RAM_CONCENTRATOR_CONFIG: ConcentratorConfig = {
minTime: 5,// zigpc: 10
maxTime: 60,// zigpc: 60
routeErrorThreshold: 3,// zigpc: 3
deliveryFailureThreshold: 1,// zigpc: 1, ZigbeeMinimalHost: 3
mapHops: 0,// zigpc: 0
};
/**
* Config for EMBER_HIGH_RAM_CONCENTRATOR type concentrator.
*
* XXX: For now, same as low, until proper values can be determined.
*/
const HIGH_RAM_CONCENTRATOR_CONFIG: ConcentratorConfig = {
minTime: 5,
maxTime: 60,
routeErrorThreshold: 3,
deliveryFailureThreshold: 1,
mapHops: 0,
};
/**
* Application generated ZDO messages use sequence numbers 0-127, and the stack
* uses sequence numbers 128-255. This simplifies life by eliminating the need
* for coordination between the two entities, and allows both to send ZDO
* messages with non-conflicting sequence numbers.
*/
const APPLICATION_ZDO_SEQUENCE_MASK = 0x7F;
/** Current revision of the spec by zigbee alliance. XXX: what are `Zigbee Pro 2023` devices reporting?? */
const CURRENT_ZIGBEE_SPEC_REVISION = 23;
/** Each scan period is 15.36ms. Scan for at least 200ms (2^4 + 1 periods) to pick up WiFi beacon frames. */
const ENERGY_SCAN_DURATION = 4;
/** Oldest supported EZSP version for backups. Don't take the risk to restore a broken network until older backup versions can be investigated. */
const BACKUP_OLDEST_SUPPORTED_EZSP_VERSION = 12;
/**
* 9sec is minimum recommended for `ezspBroadcastNextNetworkKey` to have propagated throughout network.
* NOTE: This is blocking the request queue, so we shouldn't go crazy high.
*/
const BROADCAST_NETWORK_KEY_SWITCH_WAIT_TIME = 15000;
/**
* Stack configuration values for various supported stacks.
*
* https://github.com/darkxst/silabs-firmware-builder/tree/main/manifests
* https://github.com/NabuCasa/silabs-firmware/wiki/Zigbee-EmberZNet-NCP-firmware-configuration#skyconnect
* https://github.com/SiliconLabs/UnifySDK/blob/main/applications/zigbeed/project_files/zigbeed.slcp
*/
const STACK_CONFIGS = {
"default": {
/** <1-250> (Default: 2) @see EzspConfigId.ADDRESS_TABLE_SIZE */
ADDRESS_TABLE_SIZE: 16,// zigpc: 32, darkxst: 16, nabucasa: 16
/** <0-4> (Default: 2) @see EzspConfigId.TRUST_CENTER_ADDRESS_CACHE_SIZE */
TRUST_CENTER_ADDRESS_CACHE_SIZE: 2,
/** (Default: USE_TOKEN) @see EzspConfigId.TX_POWER_MODE */
TX_POWER_MODE: EmberTXPowerMode.USE_TOKEN,
/** <-> (Default: 1) @see EzspConfigId.SUPPORTED_NETWORKS */
SUPPORTED_NETWORKS: 1,
/** <-> (Default: ) @see EzspConfigId.STACK_PROFILE */
STACK_PROFILE: STACK_PROFILE_ZIGBEE_PRO,
/** <-> (Default: ) @see EzspConfigId.SECURITY_LEVEL */
SECURITY_LEVEL: SECURITY_LEVEL_Z3,
/** (Default: KEEP_ALIVE_SUPPORT_ALL) @see EzspValueId.END_DEVICE_KEEP_ALIVE_SUPPORT_MODE */
END_DEVICE_KEEP_ALIVE_SUPPORT_MODE: EmberKeepAliveMode.KEEP_ALIVE_SUPPORT_ALL,
/** <-> (Default: MAXIMUM_APS_PAYLOAD_LENGTH) @see EzspValueId.MAXIMUM_INCOMING_TRANSFER_SIZE */
MAXIMUM_INCOMING_TRANSFER_SIZE: MAXIMUM_APS_PAYLOAD_LENGTH,
/** <-> (Default: MAXIMUM_APS_PAYLOAD_LENGTH) @see EzspValueId.MAXIMUM_OUTGOING_TRANSFER_SIZE */
MAXIMUM_OUTGOING_TRANSFER_SIZE: MAXIMUM_APS_PAYLOAD_LENGTH,
/** <-> (Default: 10000) @see EzspValueId.TRANSIENT_DEVICE_TIMEOUT */
TRANSIENT_DEVICE_TIMEOUT: 10000,
/** <0-127> (Default: 2) @see EzspConfigId.BINDING_TABLE_SIZE */
BINDING_TABLE_SIZE: 16,// zigpc: 2, Z3GatewayGPCombo: 5, nabucasa: 32
/** <0-127> (Default: 0) @see EzspConfigId.KEY_TABLE_SIZE */
KEY_TABLE_SIZE: 0,// zigpc: 4
/** <6-64> (Default: 6) @see EzspConfigId.MAX_END_DEVICE_CHILDREN */
MAX_END_DEVICE_CHILDREN: 32,// zigpc: 6, nabucasa: 32, Dongle-E (Sonoff firmware): 32
/** <1-255> (Default: 10) @see EzspConfigId.APS_UNICAST_MESSAGE_COUNT */
APS_UNICAST_MESSAGE_COUNT: 20,// zigpc: 10, darkxst: 20, nabucasa: 20
/** <15-254> (Default: 15) @see EzspConfigId.BROADCAST_TABLE_SIZE */
BROADCAST_TABLE_SIZE: 15,// zigpc: 15, Z3GatewayGPCombo: 35 - NOTE: Sonoff Dongle-E fails at 35
/** [1, 16, 26] (Default: 16). @see EzspConfigId.NEIGHBOR_TABLE_SIZE */
NEIGHBOR_TABLE_SIZE: 26,// zigpc: 16, darkxst: 26, nabucasa: 26
/** (Default: 8) @see EzspConfigId.END_DEVICE_POLL_TIMEOUT */
END_DEVICE_POLL_TIMEOUT: 8,// zigpc: 8
/** <0-65535> (Default: 300) @see EzspConfigId.TRANSIENT_KEY_TIMEOUT_S */
TRANSIENT_KEY_TIMEOUT_S: 300,// zigpc: 65535
/** <-> (Default: 16) @see EzspConfigId.RETRY_QUEUE_SIZE */
RETRY_QUEUE_SIZE: 16,// nabucasa: 16
/** <0-255> (Default: 0) @see EzspConfigId.SOURCE_ROUTE_TABLE_SIZE */
SOURCE_ROUTE_TABLE_SIZE: 200,// Z3GatewayGPCombo: 100, darkxst: 200, nabucasa: 200
/** <1-250> (Default: 8) @see EzspConfigId.MULTICAST_TABLE_SIZE */
MULTICAST_TABLE_SIZE: 16,// darkxst: 16, nabucasa: 16 - NOTE: should always be at least enough to register FIXED_ENDPOINTS multicastIds
},
"zigbeed": {
ADDRESS_TABLE_SIZE: 128,
TRUST_CENTER_ADDRESS_CACHE_SIZE: 2,
TX_POWER_MODE: EmberTXPowerMode.USE_TOKEN,
SUPPORTED_NETWORKS: 1,
STACK_PROFILE: STACK_PROFILE_ZIGBEE_PRO,
SECURITY_LEVEL: SECURITY_LEVEL_Z3,
END_DEVICE_KEEP_ALIVE_SUPPORT_MODE: EmberKeepAliveMode.KEEP_ALIVE_SUPPORT_ALL,
MAXIMUM_INCOMING_TRANSFER_SIZE: MAXIMUM_APS_PAYLOAD_LENGTH,
MAXIMUM_OUTGOING_TRANSFER_SIZE: MAXIMUM_APS_PAYLOAD_LENGTH,
TRANSIENT_DEVICE_TIMEOUT: 10000,
BINDING_TABLE_SIZE: 128,
KEY_TABLE_SIZE: 0,// zigbeed 128
MAX_END_DEVICE_CHILDREN: 64,
APS_UNICAST_MESSAGE_COUNT: 32,
BROADCAST_TABLE_SIZE: 15,
NEIGHBOR_TABLE_SIZE: 26,
END_DEVICE_POLL_TIMEOUT: 8,
TRANSIENT_KEY_TIMEOUT_S: 300,
RETRY_QUEUE_SIZE: 16,
SOURCE_ROUTE_TABLE_SIZE: 254,
MULTICAST_TABLE_SIZE: 128,
},
};
/**
* NOTE: This from SDK is currently ignored here because of issue in below link:
* - BUGZID 12261: Concentrators use MTORRs for route discovery and should not enable route discovery in the APS options.
* - https://community.silabs.com/s/question/0D58Y00008DRfDCSA1/coordinator-cant-send-unicast-to-sleepy-node-after-reboot?language=en_US
*
* No issue have been linked to this at the moment, so keeping ENABLE_ROUTE_DISCOVERY just in case...
*/
const DEFAULT_APS_OPTIONS = (EmberApsOption.RETRY | EmberApsOption.ENABLE_ROUTE_DISCOVERY | EmberApsOption.ENABLE_ADDRESS_DISCOVERY);
/**
* Enabling this allows to immediately reject requests that won't be able to get to their destination.
* However, it causes more NCP calls, notably to get the source route overhead.
* XXX: Needs further testing before enabling
*/
const CHECK_APS_PAYLOAD_LENGTH = false;
/** Time for a ZDO request to get a callback response. ASH is 2400*6 for ACK timeout. */
const DEFAULT_ZDO_REQUEST_TIMEOUT = 15000;// msec
/** Time for a ZCL request to get a callback response. ASH is 2400*6 for ACK timeout. */
const DEFAULT_ZCL_REQUEST_TIMEOUT = 15000;//msec
/** Time for a network-related request to get a response (usually via event). */
const DEFAULT_NETWORK_REQUEST_TIMEOUT = 10000;// nothing on the network to bother requests, should be much faster than this
/** Time between watchdog counters reading/clearing */
const WATCHDOG_COUNTERS_FEED_INTERVAL = 3600000;// every hour...
/**
* Relay calls between Z2M and EZSP-layer and handle any error that might occur via queue & waitress.
*
* Anything post `start` that requests anything from the EZSP layer must run through the request queue for proper execution flow.
*/
export class EmberAdapter extends Adapter {
/** Key in STACK_CONFIGS */
public readonly stackConfig: 'default' | 'zigbeed';
/** EMBER_LOW_RAM_CONCENTRATOR or EMBER_HIGH_RAM_CONCENTRATOR. */
private concentratorType: number;
private readonly ezsp: Ezsp;
private version: {ezsp: number, revision: string} & EmberVersion;
private readonly requestQueue: EmberRequestQueue;
private readonly oneWaitress: EmberOneWaitress;
/** Periodically retrieve counters then clear them. */
private watchdogCountersHandle: NodeJS.Timeout;
/** Hold ZDO request in process. */
private readonly zdoRequestBuffalo: EzspBuffalo;
/** Sequence number used for ZDO requests. static uint8_t */
private zdoRequestSequence: number;
/** Default radius used for broadcast ZDO requests. uint8_t */
private zdoRequestRadius: number;
private interpanLock: boolean;
/**
* Cached network params to avoid NCP calls. Prevents frequent EZSP transactions.
* NOTE: Do not use directly, use getter functions for it that check if valid or need retrieval from NCP.
*/
private networkCache: NetworkCache;
constructor(networkOptions: TsType.NetworkOptions, serialPortOptions: TsType.SerialPortOptions, backupPath: string,
adapterOptions: TsType.AdapterOptions, logger?: LoggerStub) {
super(networkOptions, serialPortOptions, backupPath, adapterOptions, logger);
// TODO config, should be fine like this for now?
this.stackConfig = SocketPortUtils.isTcpPath(serialPortOptions.path) ? 'zigbeed' : 'default';
// TODO config
this.concentratorType = EMBER_HIGH_RAM_CONCENTRATOR;
const delay = (typeof this.adapterOptions.delay === 'number') ? Math.min(Math.max(this.adapterOptions.delay, 5), 60) : 5;
debug(`Using delay=${delay}.`);
this.requestQueue = new EmberRequestQueue(delay);
this.oneWaitress = new EmberOneWaitress();
this.zdoRequestBuffalo = new EzspBuffalo(Buffer.alloc(EZSP_MAX_FRAME_LENGTH));
this.ezsp = new Ezsp(delay, serialPortOptions);
this.ezsp.on(EzspEvents.STACK_STATUS, this.onStackStatus.bind(this));
this.ezsp.on(EzspEvents.MESSAGE_SENT_DELIVERY_FAILED, this.onMessageSentDeliveryFailed.bind(this));
this.ezsp.on(EzspEvents.ZDO_RESPONSE, this.onZDOResponse.bind(this));
this.ezsp.on(EzspEvents.END_DEVICE_ANNOUNCE, this.onEndDeviceAnnounce.bind(this));
this.ezsp.on(EzspEvents.INCOMING_MESSAGE, this.onIncomingMessage.bind(this));
this.ezsp.on(EzspEvents.TOUCHLINK_MESSAGE, this.onTouchlinkMessage.bind(this));
this.ezsp.on(EzspEvents.GREENPOWER_MESSAGE, this.onGreenpowerMessage.bind(this));
this.ezsp.on(EzspEvents.TRUST_CENTER_JOIN, this.onTrustCenterJoin.bind(this));
}
/**
* Emitted from @see Ezsp.ezspStackStatusHandler
* @param status
*/
private async onStackStatus(status: EmberStatus): Promise<void> {
// to be extra careful, should clear network cache upon receiving this.
this.clearNetworkCache();
switch (status) {
case EmberStatus.NETWORK_UP: {
this.oneWaitress.resolveEvent(OneWaitressEvents.STACK_STATUS_NETWORK_UP);
console.log(`[STACK STATUS] Network up.`);
break;
}
case EmberStatus.NETWORK_DOWN: {
this.oneWaitress.resolveEvent(OneWaitressEvents.STACK_STATUS_NETWORK_DOWN);
console.log(`[STACK STATUS] Network down.`);
break;
}
case EmberStatus.NETWORK_OPENED: {
this.oneWaitress.resolveEvent(OneWaitressEvents.STACK_STATUS_NETWORK_OPENED);
this.requestQueue.enqueue(
async (): Promise<EmberStatus> => {
const setJPstatus = (await this.emberSetJoinPolicy(EmberJoinDecision.USE_PRECONFIGURED_KEY));
if (setJPstatus !== EzspStatus.SUCCESS) {
console.error(`[ZDO] Failed set join policy for with status=${EzspStatus[setJPstatus]}.`);
return EmberStatus.ERR_FATAL;
}
return EmberStatus.SUCCESS;
},
console.error,// no reject, just log error if any
true,// prioritize just to avoid delays if queue is busy
);
console.log(`[STACK STATUS] Network opened.`);
break;
}
case EmberStatus.NETWORK_CLOSED: {
this.oneWaitress.resolveEvent(OneWaitressEvents.STACK_STATUS_NETWORK_CLOSED);
console.log(`[STACK STATUS] Network closed.`);
break;
}
default: {
debug(`[STACK STATUS] ${EmberStatus[status]}.`);
break;
}
}
}
/**
* Emitted from @see Ezsp.ezspMessageSentHandler
* WARNING: Cannot rely on `ezspMessageSentHandler` > `ezspIncomingMessageHandler` order, some devices mix it up!
*
* @param type
* @param indexOrDestination
* @param apsFrame
* @param messageTag
*/
private async onMessageSentDeliveryFailed(type: EmberOutgoingMessageType, indexOrDestination: number, apsFrame: EmberApsFrame, messageTag: number)
: Promise<void> {
switch (type) {
case EmberOutgoingMessageType.BROADCAST:
case EmberOutgoingMessageType.BROADCAST_WITH_ALIAS:
case EmberOutgoingMessageType.MULTICAST:
case EmberOutgoingMessageType.MULTICAST_WITH_ALIAS: {
// BC/MC not checking for message sent, avoid unnecessary waitress lookups
console.error(`Delivery of ${EmberOutgoingMessageType[type]} failed for "${indexOrDestination}" `
+ `[apsFrame=${JSON.stringify(apsFrame)} messageTag=${messageTag}]`);
break;
}
default: {
// reject any waitress early (don't wait for timeout if we know we're gonna get there eventually)
this.oneWaitress.deliveryFailedFor(indexOrDestination, apsFrame);
break;
}
}
}
/**
* Emitted from @see Ezsp.ezspIncomingMessageHandler
*
* @param clusterId The ZDO response cluster ID.
* @param sender The sender of the response. Should match `payload.nodeId` in many responses.
* @param payload If null, the response indicated a failure.
*/
private async onZDOResponse(status: EmberZdoStatus, sender: EmberNodeId, apsFrame: EmberApsFrame, payload: unknown)
: Promise<void> {
this.oneWaitress.resolveZDO(status, sender, apsFrame, payload);
}
/**
* Emitted from @see Ezsp.ezspIncomingMessageHandler
*
* @param sender
* @param nodeId
* @param eui64
* @param macCapFlags
*/
private async onEndDeviceAnnounce(sender: EmberNodeId, apsFrame: EmberApsFrame, payload: EndDeviceAnnouncePayload): Promise<void> {
// reduced function device
// if ((payload.capabilities.deviceType === 0)) {
// }
this.emit(Events.deviceAnnounce, {networkAddress: payload.nodeId, ieeeAddr: payload.eui64} as DeviceAnnouncePayload);
}
/**
* Emitted from @see Ezsp.ezspIncomingMessageHandler
*
* @param type
* @param apsFrame
* @param lastHopLqi
* @param sender
* @param messageContents
*/
private async onIncomingMessage(type: EmberIncomingMessageType, apsFrame: EmberApsFrame, lastHopLqi: number, sender: EmberNodeId,
messageContents: Buffer): Promise<void> {
try {
const payload: ZclDataPayload = {
address: sender,
frame: ZclFrame.fromBuffer(apsFrame.clusterId, messageContents),
endpoint: apsFrame.sourceEndpoint,
linkquality: lastHopLqi,
groupID: apsFrame.groupId,
wasBroadcast: ((type === EmberIncomingMessageType.BROADCAST) || (type === EmberIncomingMessageType.BROADCAST_LOOPBACK)),
destinationEndpoint: apsFrame.destinationEndpoint,
};
this.oneWaitress.resolveZCL(payload);
this.emit(Events.zclData, payload);
} catch (error) {
const payload: RawDataPayload = {
clusterID: apsFrame.clusterId,
address: sender,
data: messageContents,
endpoint: apsFrame.sourceEndpoint,
linkquality: lastHopLqi,
groupID: apsFrame.groupId,
wasBroadcast: ((type === EmberIncomingMessageType.BROADCAST) || (type === EmberIncomingMessageType.BROADCAST_LOOPBACK)),
destinationEndpoint: apsFrame.destinationEndpoint,
};
this.emit(Events.rawData, payload);
}
}
/**
* Emitted from @see Ezsp.ezspMacFilterMatchMessageHandler when the message is a valid InterPAN touchlink message.
*
* @param sourcePanId
* @param sourceAddress
* @param groupId
* @param lastHopLqi
* @param messageContents
*/
private async onTouchlinkMessage(sourcePanId: EmberPanId, sourceAddress: EmberEUI64, groupId: number | null, lastHopLqi: number,
messageContents: Buffer): Promise<void> {
const payload: ZclDataPayload = {
frame: ZclFrame.fromBuffer(Cluster.touchlink.ID, messageContents),
address: sourceAddress,
endpoint: 1,// arbitrary since not sent over-the-air
linkquality: lastHopLqi,
groupID: groupId,
wasBroadcast: true,// XXX: since always sent broadcast atm...
destinationEndpoint: FIXED_ENDPOINTS[0].endpoint,
};
this.oneWaitress.resolveZCL(payload);
this.emit(Events.zclData, payload);
}
/**
* Emitted from @see Ezsp.ezspGpepIncomingMessageHandler
*
* @param sequenceNumber
* @param commandIdentifier
* @param sourceId
* @param frameCounter
* @param gpdCommandId
* @param gpdCommandPayload
* @param gpdLink
*/
private async onGreenpowerMessage(sequenceNumber: number, commandIdentifier: number, sourceId: number, frameCounter: number,
gpdCommandId: number, gpdCommandPayload: Buffer, gpdLink: number) : Promise<void> {
try {
const gpdHeader = Buffer.alloc(15);
gpdHeader.writeUInt8(0b00000001, 0);// frameControl: FrameType.SPECIFIC + Direction.CLIENT_TO_SERVER + disableDefaultResponse=false
gpdHeader.writeUInt8(sequenceNumber, 1);// transactionSequenceNumber
gpdHeader.writeUInt8(commandIdentifier, 2);// commandIdentifier
gpdHeader.writeUInt16LE(0, 3);// options XXX: bypassed, same as deconz https://github.com/Koenkk/zigbee-herdsman/pull/536
gpdHeader.writeUInt32LE(sourceId, 5);// srcID
// omitted: gpdIEEEAddr ieeeAddr
// omitted: gpdEndpoint uint8
gpdHeader.writeUInt32LE(frameCounter, 9);// frameCounter
gpdHeader.writeUInt8(gpdCommandId, 13);// commandID
gpdHeader.writeUInt8(gpdCommandPayload.length, 14);// payloadSize
const gpFrame = ZclFrame.fromBuffer(Cluster.greenPower.ID, Buffer.concat([gpdHeader, gpdCommandPayload]));
const payload: ZclDataPayload = {
frame: gpFrame,
address: sourceId,
endpoint: GP_ENDPOINT,
linkquality: gpdLink,
groupID: this.greenPowerGroup,
// XXX: upstream sends to `gppNwkAddr` if `wasBroadcast` is false, even if `gppNwkAddr` is null
wasBroadcast: (gpFrame.Payload.gppNwkAddr != null) ? false : true,
destinationEndpoint: GP_ENDPOINT,
};
this.oneWaitress.resolveZCL(payload);
this.emit(Events.zclData, payload);
} catch (err) {
console.error(`<~x~ [GP] Failed creating ZCL payload. Skipping. ${err}`);
return;
}
}
/**
* Emitted from @see Ezsp.ezspTrustCenterJoinHandler
* Also from @see Ezsp.ezspIdConflictHandler as a DEVICE_LEFT
*
* @param newNodeId
* @param newNodeEui64
* @param status
* @param policyDecision
* @param parentOfNewNodeId
*/
private async onTrustCenterJoin(newNodeId: EmberNodeId, newNodeEui64: EmberEUI64, status: EmberDeviceUpdate,
policyDecision: EmberJoinDecision, parentOfNewNodeId: EmberNodeId): Promise<void> {
if (status === EmberDeviceUpdate.DEVICE_LEFT) {
const payload: DeviceLeavePayload = {
networkAddress: newNodeId,
ieeeAddr: newNodeEui64,
};
this.emit(Events.deviceLeave, payload);
} else {
if (policyDecision !== EmberJoinDecision.DENY_JOIN) {
const payload: DeviceJoinedPayload = {
networkAddress: newNodeId,
ieeeAddr: newNodeEui64,
};
this.emit(Events.deviceJoined, payload);
} else {
console.log(`[TRUST CENTER] Device ${newNodeId}:${newNodeEui64} was denied joining via ${parentOfNewNodeId}.`);
}
}
}
private async watchdogCounters(): Promise<void> {
this.requestQueue.enqueue(
async (): Promise<EmberStatus> => {
// listed as per EmberCounterType
const counters = (await this.ezsp.ezspReadAndClearCounters());
let countersLogString = "[NCP COUNTERS] ";
for (let i = 0; i < EmberCounterType.COUNT; i++) {
countersLogString += `${EmberCounterType[i]}: ${counters[i]} | `;
}
console.log(countersLogString);
return EmberStatus.SUCCESS;
},
console.error,// no reject, just log error if any
);
}
private initVariables(): void {
this.ezsp.removeAllListeners(EzspEvents.ncpNeedsResetAndInit);
clearInterval(this.watchdogCountersHandle);
this.zdoRequestBuffalo.setPosition(0);
this.zdoRequestSequence = 0;// start at 1
this.zdoRequestRadius = 255;
this.interpanLock = false;
this.networkCache = initNetworkCache();
this.ezsp.once(EzspEvents.ncpNeedsResetAndInit, this.onNcpNeedsResetAndInit.bind(this));
}
/**
* Proceed to execute the long list of commands required to setup comms between Host<>NCP.
* This is called by start and on internal reset.
*/
private async initEzsp(): Promise<TsType.StartResult> {
let result: TsType.StartResult = "resumed";
await this.onNCPPreReset();
try {
// NOTE: something deep in this call can throw too
const result = (await this.ezsp.start());
if (result !== EzspStatus.SUCCESS) {
throw new Error(`Failed to start EZSP layer with status=${EzspStatus[result]}.`);
}
} catch (err) {
throw err;
}
// call before any other command, else fails
await this.emberVersion();
await this.initNCPPreConfiguration();
await this.initNCPAddressTable();
await this.initNCPConfiguration();
// WARNING: From here on EZSP commands that affect memory allocation on the NCP should no longer be called (like resizing tables)
await this.onNCPPostReset();
await this.registerFixedEndpoints();
this.clearNetworkCache();
result = (await this.initTrustCenter());
// after network UP, as per SDK, ensures clean slate
await this.initNCPConcentrator();
// await (this.emberStartEnergyScan());// TODO: via config of some kind, better off waiting for UI supports though
// populate network cache info
const [status, , parameters] = (await this.ezsp.ezspGetNetworkParameters());
if (status !== EmberStatus.SUCCESS) {
throw new Error(`Failed to get network parameters with status=${EmberStatus[status]}.`);
}
this.networkCache.parameters = parameters;
this.networkCache.status = (await this.ezsp.ezspNetworkState());
this.networkCache.eui64 = (await this.ezsp.ezspGetEui64());
debug(`[INIT] Network Ready! ${JSON.stringify(this.networkCache)}`);
return result;
}
/**
* NCP Config init. Should always be called first in the init stack (after version cmd).
* @returns
*/
private async initNCPPreConfiguration(): Promise<void> {
// this can only decrease, not increase, NCP-side value
await this.emberSetEzspConfigValue(EzspConfigId.ADDRESS_TABLE_SIZE, STACK_CONFIGS[this.stackConfig].ADDRESS_TABLE_SIZE);
await this.emberSetEzspConfigValue(
EzspConfigId.TRUST_CENTER_ADDRESS_CACHE_SIZE,
STACK_CONFIGS[this.stackConfig].TRUST_CENTER_ADDRESS_CACHE_SIZE
);
if (STACK_CONFIGS[this.stackConfig].STACK_PROFILE === STACK_PROFILE_ZIGBEE_PRO) {
// BUG 14222: If stack profile is 2 (ZigBee Pro), we need to enforce
// the standard stack configuration values for that feature set.
/** MAC indirect timeout should be 7.68 secs */
await this.emberSetEzspConfigValue(EzspConfigId.INDIRECT_TRANSMISSION_TIMEOUT, 7680);
/** Max hops should be 2 * nwkMaxDepth, where nwkMaxDepth is 15 */
await this.emberSetEzspConfigValue(EzspConfigId.MAX_HOPS, 30);
}
await this.emberSetEzspConfigValue(EzspConfigId.TX_POWER_MODE, STACK_CONFIGS[this.stackConfig].TX_POWER_MODE);
await this.emberSetEzspConfigValue(EzspConfigId.SUPPORTED_NETWORKS, STACK_CONFIGS[this.stackConfig].SUPPORTED_NETWORKS);
await this.emberSetEzspValue(
EzspValueId.END_DEVICE_KEEP_ALIVE_SUPPORT_MODE,
1,
[STACK_CONFIGS[this.stackConfig].END_DEVICE_KEEP_ALIVE_SUPPORT_MODE]
);
// allow other devices to modify the binding table
await this.emberSetEzspPolicy(
EzspPolicyId.BINDING_MODIFICATION_POLICY,
EzspDecisionId.CHECK_BINDING_MODIFICATIONS_ARE_VALID_ENDPOINT_CLUSTERS
);
// return message tag and message contents in ezspMessageSentHandler()
await this.emberSetEzspPolicy(
EzspPolicyId.MESSAGE_CONTENTS_IN_CALLBACK_POLICY,
EzspDecisionId.MESSAGE_TAG_AND_CONTENTS_IN_CALLBACK
);
await this.emberSetEzspValue(
EzspValueId.MAXIMUM_INCOMING_TRANSFER_SIZE,
2,
lowHighBytes(STACK_CONFIGS[this.stackConfig].MAXIMUM_INCOMING_TRANSFER_SIZE)
);
await this.emberSetEzspValue(
EzspValueId.MAXIMUM_OUTGOING_TRANSFER_SIZE,
2,
lowHighBytes(STACK_CONFIGS[this.stackConfig].MAXIMUM_OUTGOING_TRANSFER_SIZE)
);
await this.emberSetEzspValue(
EzspValueId.TRANSIENT_DEVICE_TIMEOUT,
2,
lowHighBytes(STACK_CONFIGS[this.stackConfig].TRANSIENT_DEVICE_TIMEOUT)
);
// Set the manufacturing code. This is defined by ZigBee document 053874r10
// Ember's ID is 0x1002 and is the default, but this can be overridden in App Builder.
await this.ezsp.ezspSetManufacturerCode(MANUFACTURER_CODE);
// network security init
await this.emberSetEzspConfigValue(EzspConfigId.STACK_PROFILE, STACK_CONFIGS[this.stackConfig].STACK_PROFILE);
await this.emberSetEzspConfigValue(EzspConfigId.SECURITY_LEVEL, STACK_CONFIGS[this.stackConfig].SECURITY_LEVEL);
}
/**
* NCP Address table init.
* @returns
*/
private async initNCPAddressTable(): Promise<void> {
const desiredTableSize = STACK_CONFIGS[this.stackConfig].ADDRESS_TABLE_SIZE;
// If the host and the ncp disagree on the address table size, explode.
const [status, addressTableSize] = (await this.ezsp.ezspGetConfigurationValue(EzspConfigId.ADDRESS_TABLE_SIZE));
// After the change of ncp memory model in UC, we can not increase the default NCP table sizes anymore.
// Therefore, checking for desiredTableSize == (ncp)addressTableSize might not be always true anymore
// assert(desiredTableSize <= addressTableSize);
if ((status !== EzspStatus.SUCCESS) || (addressTableSize > desiredTableSize)) {
throw new Error(
`[INIT] NCP (${addressTableSize}) disagrees with Host (min ${desiredTableSize}) on table size. status=${EzspStatus[status]}`
);
}
}
/**
* NCP configuration init
*/
private async initNCPConfiguration(): Promise<void> {
await this.emberSetEzspConfigValue(EzspConfigId.BINDING_TABLE_SIZE, STACK_CONFIGS[this.stackConfig].BINDING_TABLE_SIZE);
await this.emberSetEzspConfigValue(EzspConfigId.KEY_TABLE_SIZE, STACK_CONFIGS[this.stackConfig].KEY_TABLE_SIZE);
await this.emberSetEzspConfigValue(EzspConfigId.MAX_END_DEVICE_CHILDREN, STACK_CONFIGS[this.stackConfig].MAX_END_DEVICE_CHILDREN);
await this.emberSetEzspConfigValue(EzspConfigId.APS_UNICAST_MESSAGE_COUNT, STACK_CONFIGS[this.stackConfig].APS_UNICAST_MESSAGE_COUNT);
await this.emberSetEzspConfigValue(EzspConfigId.BROADCAST_TABLE_SIZE, STACK_CONFIGS[this.stackConfig].BROADCAST_TABLE_SIZE);
await this.emberSetEzspConfigValue(EzspConfigId.NEIGHBOR_TABLE_SIZE, STACK_CONFIGS[this.stackConfig].NEIGHBOR_TABLE_SIZE);
await this.emberSetEzspConfigValue(EzspConfigId.END_DEVICE_POLL_TIMEOUT, STACK_CONFIGS[this.stackConfig].END_DEVICE_POLL_TIMEOUT);
await this.emberSetEzspConfigValue(EzspConfigId.TRANSIENT_KEY_TIMEOUT_S, STACK_CONFIGS[this.stackConfig].TRANSIENT_KEY_TIMEOUT_S);
await this.emberSetEzspConfigValue(EzspConfigId.RETRY_QUEUE_SIZE, STACK_CONFIGS[this.stackConfig].RETRY_QUEUE_SIZE);
await this.emberSetEzspConfigValue(EzspConfigId.SOURCE_ROUTE_TABLE_SIZE, STACK_CONFIGS[this.stackConfig].SOURCE_ROUTE_TABLE_SIZE);
await this.emberSetEzspConfigValue(EzspConfigId.MULTICAST_TABLE_SIZE, STACK_CONFIGS[this.stackConfig].MULTICAST_TABLE_SIZE);
}
/**
* NCP concentrator init. Also enables source route discovery mode with RESCHEDULE.
*
* From AN1233:
* To function correctly in a Zigbee PRO network, a trust center also requires that:
*
* 1. The trust center application must act as a concentrator (either high or low RAM).
* 2. The trust center application must have support for source routing.
* It must record the source routes and properly handle requests by the stack for a particular source route.
* 3. The trust center application must use an address cache for security, in order to maintain a mapping of IEEE address to short ID.
*
* Failure to satisfy all of the above requirements may result in failures when joining/rejoining devices to the network across multiple hops
* (through a target node that is neither the trust center nor one of its neighboring routers.)
*/
private async initNCPConcentrator(): Promise<void> {
const config = (this.concentratorType === EMBER_HIGH_RAM_CONCENTRATOR) ? HIGH_RAM_CONCENTRATOR_CONFIG : LOW_RAM_CONCENTRATOR_CONFIG;
const status = (await this.ezsp.ezspSetConcentrator(
true,
this.concentratorType,
config.minTime,
config.maxTime,
config.routeErrorThreshold,
config.deliveryFailureThreshold,
config.mapHops,
));
if (status !== EmberStatus.SUCCESS) {
throw new Error(`[CONCENTRATOR] Failed to set concentrator with status=${status}.`);
}
const remainTilMTORR = (await this.ezsp.ezspSetSourceRouteDiscoveryMode(EmberSourceRouteDiscoveryMode.RESCHEDULE));
console.log(`[CONCENTRATOR] Started source route discovery. ${remainTilMTORR}ms until next broadcast.`);
}
/**
* Register fixed endpoints and set any related multicast entries that need to be.
*/
private async registerFixedEndpoints(): Promise<void> {
let mcTableIdx = 0;
for (const ep of FIXED_ENDPOINTS) {
if (ep.networkIndex !== 0x00) {
debug(`Multi-network not currently supported. Skipping endpoint ${JSON.stringify(ep)}.`);
continue;
}
const [epStatus,] = (await this.ezsp.ezspGetEndpointFlags(ep.endpoint));
// endpoint not already registered
if (epStatus !== EzspStatus.SUCCESS) {
// check to see if ezspAddEndpoint needs to be called
// if ezspInit is called without NCP reset, ezspAddEndpoint is not necessary and will return an error
const status = (await this.ezsp.ezspAddEndpoint(
ep.endpoint,
ep.profileId,
ep.deviceId,
ep.deviceVersion,
ep.inClusterList.slice(),// copy
ep.outClusterList.slice(),// copy
));
if (status === EzspStatus.SUCCESS) {
debug(`Registered endpoint "${ep.endpoint}" with status=${EzspStatus[status]}.`);
} else {
throw new Error(`Failed to register endpoint "${ep.endpoint}" with status=${EzspStatus[status]}.`);
}
} else {
debug(`Endpoint "${ep.endpoint}" already registered.`);
}
for (const multicastId of ep.multicastIds) {
const multicastEntry: EmberMulticastTableEntry = {
multicastId,
endpoint: ep.endpoint,
networkIndex: ep.networkIndex,
};
const status = (await this.ezsp.ezspSetMulticastTableEntry(mcTableIdx++, multicastEntry));
if (status !== EmberStatus.SUCCESS) {
throw new Error(`Failed to register group "${multicastId}" in multicast table with status=${EmberStatus[status]}.`);
}
debug(`Registered multicast table entry: ${JSON.stringify(multicastEntry)}.`);