iobroker.zigbee 1.5.6 → 1.6.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/.devcontainer/devcontainer.json +35 -35
  2. package/.devcontainer/docker-compose.yml +51 -51
  3. package/.devcontainer/iobroker/Dockerfile +1 -1
  4. package/.devcontainer/nginx/nginx.conf +32 -32
  5. package/.devcontainer/parcel/Dockerfile +8 -8
  6. package/.devcontainer/parcel/run.sh +6 -6
  7. package/.eslintignore +1 -1
  8. package/.eslintrc.json +36 -36
  9. package/.github/FUNDING.yml +3 -3
  10. package/.github/stale.yml +13 -13
  11. package/.github/workflows/test-and-release.yml +151 -151
  12. package/.travis/wiki.sh +27 -27
  13. package/LICENSE +21 -21
  14. package/README.md +385 -353
  15. package/admin/adapter-settings.js +244 -208
  16. package/admin/admin.js +2704 -2714
  17. package/admin/img/R7060.png +0 -0
  18. package/admin/img/WHD02.png +0 -0
  19. package/admin/img/ikea_E1812.png +0 -0
  20. package/admin/img/philips_hue_lom001.png +0 -0
  21. package/admin/img/tuya_rb280.png +0 -0
  22. package/admin/index.html +159 -159
  23. package/admin/index_m.html +1055 -965
  24. package/admin/moment.min.js +1 -1
  25. package/admin/shuffle.min.js +2 -2
  26. package/admin/tab_m.html +1025 -934
  27. package/admin/vis-network.min.js +26 -26
  28. package/admin/words.js +106 -106
  29. package/docs/de/readme.md +27 -27
  30. package/docs/en/readme.md +30 -30
  31. package/docs/flashing_via_arduino_(en).md +110 -110
  32. package/docs/ru/readme.md +28 -28
  33. package/docs/tutorial/groups-1.png +0 -0
  34. package/docs/tutorial/groups-2.png +0 -0
  35. package/docs/tutorial/tab-dev-1.png +0 -0
  36. package/docs/tutorial/zigbee.png +0 -0
  37. package/io-package.json +317 -290
  38. package/lib/backup.js +132 -132
  39. package/lib/binding.js +325 -325
  40. package/lib/colors.js +460 -460
  41. package/lib/commands.js +435 -434
  42. package/lib/developer.js +148 -144
  43. package/lib/devices.js +3119 -3109
  44. package/lib/exclude.js +168 -168
  45. package/lib/exposes.js +204 -51
  46. package/lib/groups.js +316 -316
  47. package/lib/json.js +60 -60
  48. package/lib/networkmap.js +56 -56
  49. package/lib/ota.js +153 -153
  50. package/lib/rgb.js +225 -225
  51. package/lib/seriallist.js +37 -37
  52. package/lib/states.js +6381 -6322
  53. package/lib/statescontroller.js +502 -495
  54. package/lib/tools.js +54 -54
  55. package/lib/utils.js +151 -132
  56. package/lib/zbBaseExtension.js +31 -27
  57. package/lib/zbDelayedAction.js +151 -146
  58. package/lib/zbDeviceAvailability.js +306 -304
  59. package/lib/zbDeviceConfigure.js +148 -143
  60. package/lib/zbDeviceEvent.js +43 -43
  61. package/lib/zigbeecontroller.js +856 -822
  62. package/main.js +113 -39
  63. package/package.json +74 -73
  64. package/support/docgen.js +93 -93
@@ -1,822 +1,856 @@
1
- 'use strict';
2
-
3
- const pathLib = require('path');
4
- const ZigbeeHerdsman = require('zigbee-herdsman');
5
- const zigbeeHerdsmanConverters = require('zigbee-herdsman-converters');
6
- const EventEmitter = require('events').EventEmitter;
7
- const safeJsonStringify = require('./json');
8
- const DeviceAvailabilityExt = require('./zbDeviceAvailability');
9
- const DeviceConfigureExt = require('./zbDeviceConfigure');
10
- const DeviceEventExt = require('./zbDeviceEvent');
11
- const DelayedActionExt = require('./zbDelayedAction');
12
- const groupConverters = [
13
- zigbeeHerdsmanConverters.toZigbeeConverters.light_onoff_brightness,
14
- zigbeeHerdsmanConverters.toZigbeeConverters.light_colortemp,
15
- zigbeeHerdsmanConverters.toZigbeeConverters.light_color,
16
- zigbeeHerdsmanConverters.toZigbeeConverters.light_alert,
17
- zigbeeHerdsmanConverters.toZigbeeConverters.ignore_transition,
18
- zigbeeHerdsmanConverters.toZigbeeConverters.light_brightness_move,
19
- zigbeeHerdsmanConverters.toZigbeeConverters.light_brightness_step,
20
- zigbeeHerdsmanConverters.toZigbeeConverters.light_colortemp_move,
21
- zigbeeHerdsmanConverters.toZigbeeConverters.light_colortemp_step,
22
- zigbeeHerdsmanConverters.toZigbeeConverters.ight_colortemp_startup
23
- ];
24
-
25
- function isFunction(functionToCheck) {
26
- return functionToCheck && {}.toString.call(functionToCheck) === '[object Function]';
27
- }
28
-
29
- class ZigbeeController extends EventEmitter {
30
- /*
31
- events:
32
-
33
- log - log (level, msg, data)
34
- event - preparsed device events (type, dev, msg, data)
35
- new - new device connected to network (id, msg)
36
- leave - device leave the network (id, msg)
37
- join - join countdown (counter)
38
- ready - connection successfull ()
39
- */
40
- constructor(adapter) {
41
- super();
42
- this.adapter = adapter;
43
- this._permitJoinTime = 0;
44
- this.herdsman_started = false;
45
- this.extensions = [
46
- new DeviceAvailabilityExt(this, {}),
47
- new DeviceConfigureExt(this, {}),
48
- new DeviceEventExt(this, {}),
49
- new DelayedActionExt(this, {}),
50
- ];
51
- }
52
-
53
- configure(options) {
54
- const herdsmanSettings = {
55
- network: {
56
- panID: options.net.panId,
57
- extendedPanID: options.net.extPanId,
58
- channelList: options.net.channelList,
59
- networkKey: options.net.precfgkey,
60
- },
61
- databasePath: pathLib.join(options.dbDir, options.dbPath),
62
- backupPath: pathLib.join(options.dbDir, options.backupPath),
63
- serialPort: {
64
- baudRate: options.sp.baudRate,
65
- rtscts: options.sp.rtscts,
66
- path: options.sp.port,
67
- adapter: options.sp.adapter,
68
- },
69
- };
70
- // https://github.com/ioBroker/ioBroker.zigbee/issues/668
71
- if (!options.extPanIdFix) {
72
- delete herdsmanSettings.network.extendedPanID;
73
- herdsmanSettings.network.extenedPanID = options.net.extPanId;
74
- }
75
-
76
- if (options.transmitPower == undefined) {
77
- this.transmitPower = 0;
78
- } else {
79
- this.transmitPower = options.transmitPower;
80
- }
81
- this.disableLed = options.disableLed;
82
-
83
- this.debug(`Using zigbee-herdsman with settings: ${JSON.stringify(herdsmanSettings)}`);
84
- this.herdsman = new ZigbeeHerdsman.Controller(herdsmanSettings, this.adapter.log);
85
- this.callExtensionMethod('setOptions', [{ disableActivePing: options.disablePing, disableForcedPing: false, pingTimeout:300, pingCount:3 }] );
86
- }
87
-
88
- // Start controller
89
- async start() {
90
- try {
91
- //this.debug(`Using zigbee-herdsman with settings2: ${JSON.stringify(this)}`);
92
- this.debug(`Starting zigbee-herdsman...`);
93
- await this.herdsman.start();
94
-
95
- this.herdsman.on('adapterDisconnected', this.handleDisconnected.bind(this));
96
- this.herdsman.on('deviceAnnounce', this.handleDeviceAnnounce.bind(this));
97
- this.herdsman.on('deviceInterview', this.handleDeviceInterview.bind(this));
98
- this.herdsman.on('deviceJoined', this.handleDeviceJoined.bind(this));
99
- this.herdsman.on('deviceLeave', this.handleDeviceLeave.bind(this));
100
- this.herdsman.on('message', this.handleMessage.bind(this));
101
-
102
- this.debug('zigbee-herdsman started');
103
- this.herdsman_started = true;
104
- this.info(`Coordinator firmware version: ${JSON.stringify(await this.herdsman.getCoordinatorVersion())}`);
105
-
106
- // debug info from herdsman getNetworkParameters
107
- const debNetworkParam = JSON.parse(JSON.stringify(await this.herdsman.getNetworkParameters()));
108
- const extendedPanIDDebug = debNetworkParam.extendedPanID.replace('0x','');
109
-
110
- let extPanIDDebug = '';
111
- for (let i = extendedPanIDDebug.length - 1; i >= 0; i--) {
112
- extPanIDDebug += extendedPanIDDebug[i-1];
113
- extPanIDDebug += extendedPanIDDebug[i];
114
- i--;
115
- }
116
-
117
- this.debug(`Zigbee network parameters: panID=${debNetworkParam.panID} channel=${debNetworkParam.channel} extendedPanID=${extPanIDDebug}`);
118
-
119
- } catch (e) {
120
- this.error('Starting zigbee-herdsman problem : ' + JSON.stringify(e.message));
121
- throw 'Error herdsman start';
122
- }
123
- // Check if we have to turn off the led
124
- try {
125
- if (this.disableLed) {
126
- this.info('Disable LED');
127
- await this.herdsman.setLED(false);
128
- } else {
129
- await this.herdsman.setLED(true);
130
- }
131
- } catch (e) {
132
- this.info('Unable to disable LED, unsupported function.');
133
- }
134
-
135
- // only for CC1352P and CC26X2R1 transmit power
136
- let powerText = 'normal';
137
-
138
- if (this.transmitPower != '0') {
139
- switch(this.transmitPower) {
140
- case '-22':
141
- powerText = 'low';
142
- break;
143
- case '19':
144
- powerText = 'high';
145
- break;
146
- default:
147
- powerText = 'normal';
148
- }
149
- }
150
-
151
-
152
- this.info(' --> transmitPower : ' + powerText);
153
- try {
154
- await this.herdsman.setTransmitPower(this.transmitPower);
155
- } catch (e) {
156
- this.info('Unable to set transmit power, unsupported function.');
157
- }
158
-
159
-
160
- // Call extensions
161
- this.callExtensionMethod('onZigbeeStarted', []);
162
-
163
- // Log zigbee clients on startup
164
- const devices = await this.getClients();
165
- if (devices.length > 0) {
166
- this.info(`Currently ${devices.length} devices are joined:`);
167
- } else {
168
- this.info(`Currently no devices.`);
169
- }
170
- for (const device of devices) {
171
- const entity = await this.resolveEntity(device);
172
- // ensure that objects for all found clients are present
173
- this.callExtensionMethod('registerDevicePing', [device, entity]);
174
-
175
- if (entity.mapped) this.emit('new', entity);
176
- this.info(
177
- (entity.device.ieeeAddr) +
178
- ` (addr ${entity.device.networkAddress}): ` +
179
- (entity.mapped ?
180
- `${entity.mapped.model} - ${entity.mapped.vendor} ${entity.mapped.description} ` :
181
- `Not supported (model ${entity.device.modelID})`) +
182
- `(${entity.device.type})`
183
- );
184
- }
185
-
186
- this.emit('ready');
187
- }
188
-
189
- info(message, data) {
190
- this.emit('log', 'info', message, data);
191
- }
192
-
193
- error(message, data) {
194
- this.emit('log', 'error', message, data);
195
- }
196
-
197
- debug(message, data) {
198
- this.emit('log', 'debug', message, data);
199
- }
200
-
201
- warn(message, data) {
202
- this.emit('log', 'warn', message, data);
203
- }
204
-
205
- event(type, dev, message, data) {
206
- this.emit('event', type, dev, message, data);
207
- }
208
-
209
- callExtensionMethod(method, parameters) {
210
- for (const extension of this.extensions) {
211
- if (extension[method]) {
212
- try {
213
- if (parameters !== undefined) {
214
- extension[method](...parameters);
215
- } else {
216
- extension[method]();
217
- }
218
- } catch (error) {
219
- this.error(`Failed to call '${extension.constructor.name}' '${method}' (${error.stack})`);
220
- // throw error;
221
- }
222
- }
223
- }
224
- }
225
-
226
- async getClients(all) {
227
- if (this.herdsman.database) {
228
- const devices = await this.herdsman.getDevices();
229
- if (all) {
230
- return devices;
231
- } else {
232
- return devices.filter((device) => device.type !== 'Coordinator');
233
- }
234
- } else {
235
- return [];
236
- }
237
- }
238
-
239
- async getGroups() {
240
- try {
241
- return this.herdsman.getGroups();
242
- }
243
- catch {
244
- return undefined;
245
- }
246
- }
247
-
248
- async removeGroupById(id) {
249
- const group = await this.getGroupByID(id);
250
- try {
251
- if (group) group.removeFromDatabase();
252
- } catch (error) {
253
- this.error('error in removeGroupById: ' + error);
254
- }
255
- }
256
-
257
- async getGroupByID(id) {
258
- try {
259
- return this.herdsman.getGroupByID(id);
260
- }
261
- catch {
262
- return undefined;
263
- }
264
- }
265
-
266
- async getGroupMembersFromController(id) {
267
- const members = [];
268
- try {
269
- const group = await this.getGroupByID(id);
270
- if (group) {
271
- const groupmembers = group.members;
272
-
273
- for (const member of groupmembers) {
274
- const nwk = member.deviceNetworkAddress;
275
- const device = this.getDeviceByNetworkAddress(nwk);
276
- if (device && device.ieeeAddr) members.push( { device:device.ieeeAddr, model:device.modelID } );
277
- }
278
- }
279
- else {
280
- return undefined;
281
- }
282
-
283
- } catch (error) {
284
- if (error) this.error('getGroupMembersFromController: error is ' + JSON.stringify(error) + ' ' + JSON.stringify(new Error().stack));
285
- else this.error('unidentifed error in getGroupMembersFromController');
286
- }
287
- return members;
288
- }
289
-
290
- getDevice(key) {
291
- return this.herdsman.getDeviceByIeeeAddr(key);
292
- }
293
-
294
- getDevicesByType(type) {
295
- return this.herdsman.getDevicesByType(type);
296
- }
297
-
298
- getDeviceByNetworkAddress(networkAddress) {
299
- return this.herdsman.getDeviceByNetworkAddress(networkAddress);
300
- }
301
-
302
- async resolveEntity(key, ep) {
303
- //assert(typeof key === 'string' || key.constructor.name === 'Device', `Wrong type '${typeof key}'`);
304
-
305
- if (typeof key === 'string') {
306
- if (key === 'coordinator') {
307
- const coordinator = this.herdsman.getDevicesByType('Coordinator')[0];
308
- return {
309
- type: 'device',
310
- device: coordinator,
311
- endpoint: coordinator.getEndpoint(1),
312
- name: 'Coordinator',
313
- };
314
- } else {
315
- const device = await this.herdsman.getDeviceByIeeeAddr(key);
316
- if (device) {
317
- const mapped = zigbeeHerdsmanConverters.findByDevice(device);
318
- const endpoints = mapped && mapped.endpoint ? mapped.endpoint(device) : null;
319
- let endpoint;
320
- if (endpoints && ep != undefined && endpoints[ep]) {
321
- endpoint = device.getEndpoint(endpoints[ep]);
322
- } else if (endpoints && endpoints['default']) {
323
- endpoint = device.getEndpoint(endpoints['default']);
324
- } else {
325
- const epNum = parseInt(ep);
326
- if (!isNaN(epNum)) {
327
- endpoint = device.getEndpoint(epNum);
328
- } else {
329
- endpoint = device.endpoints[0];
330
- }
331
- }
332
- return {
333
- type: 'device',
334
- device,
335
- mapped,
336
- endpoint,
337
- endpoints: device.endpoints,
338
- name: key,
339
- };
340
- } else {
341
- return;
342
- }
343
- }
344
- } else if (typeof key === 'number') {
345
- let group = await this.herdsman.getGroupByID(key);
346
- if (!group) group = await this.herdsman.createGroup(key);
347
- group.toZigbee = groupConverters;
348
- group.model = 'group';
349
- return {
350
- type: 'group',
351
- mapped: group,
352
- group,
353
- name: `Group ${key}`,
354
- };
355
- } else {
356
- return {
357
- type: 'device',
358
- device: key,
359
- mapped: zigbeeHerdsmanConverters.findByDevice(key),
360
- name: key.type === 'Coordinator' ? 'Coordinator' : key.ieeeAddr,
361
- };
362
- }
363
- }
364
-
365
- async incMsgHandler(message){
366
- this.debug('incoming msg', message);
367
- const device = await this.herdsman.getDeviceByIeeeAddr(message.srcaddr);
368
- if (!device) {
369
- this.debug('Message without device!');
370
- return;
371
- }
372
- // We can't handle devices without modelId.
373
- if (!device.modelId) {
374
- this.debug('Message without modelId!');
375
- return;
376
- }
377
- this.event('msg', device.ieeeAddr, message, {
378
- modelId: device.modelId
379
- });
380
- }
381
-
382
- // Stop controller
383
- async stop() {
384
- // Call extensions
385
- await this.callExtensionMethod('stop', []);
386
-
387
- try {
388
- await this.permitJoin(0);
389
- await this.herdsman.stop();
390
- } catch (error) {
391
- if (this.herdsman_started)
392
- this.error(`Failed to stop zigbee (${error.stack})`);
393
- else {
394
- this.warn(`Failed to stop zigbee during startup`);
395
- }
396
- }
397
- }
398
-
399
- async handleDisconnected() {
400
- this.herdsman_started = false;
401
- this.emit('disconnect');
402
- }
403
-
404
- connected() {
405
- return this.herdsman_started;
406
- }
407
-
408
- // Permit join
409
- async permitJoin(permitTime, devid, failure) {
410
- let permitDev;
411
- if (isFunction(devid) && !isFunction(failure)) {
412
- failure = devid;
413
- } else {
414
- permitDev = this.getDevice(devid);
415
- }
416
-
417
- if (permitTime) {
418
- this.info('Zigbee: allowing new devices to join.');
419
- } else {
420
- this.info('Zigbee: disabling joining new devices.');
421
- }
422
- try
423
- {
424
- if (permitTime && !this.herdsman.getPermitJoin()) {
425
- clearInterval(this._permitJoinInterval);
426
- this._permitJoinTime = permitTime;
427
- await this.herdsman.permitJoin(true, permitDev);
428
- this._permitJoinInterval = setInterval(async () => {
429
- this.emit('pairing', 'Pairing time left', this._permitJoinTime);
430
- if (this._permitJoinTime === 0) {
431
- this.info('Zigbee: stop joining');
432
- clearInterval(this._permitJoinInterval);
433
- await this.herdsman.permitJoin(false);
434
- }
435
- this._permitJoinTime -= 1;
436
- }, 1000);
437
- } else if (this.herdsman.getPermitJoin()) {
438
- if (permitTime) {
439
- this.info('Joining already permitted');
440
- } else {
441
- clearInterval(this._permitJoinInterval);
442
- await this.herdsman.permitJoin(false, permitDev);
443
- }
444
- }
445
- }
446
- catch (e)
447
- {
448
- this.error(`Failed to open the network: ${e.stack}`);
449
- }
450
- }
451
-
452
- // Remove device
453
- async remove(deviceID, force, callback) {
454
- try {
455
- const device = await this.herdsman.getDeviceByIeeeAddr(deviceID);
456
- if (device) {
457
- try {
458
- await this.herdsman.adapter.removeDevice(device.networkAddress, device.ieeeAddr);
459
- } catch (error) {
460
- if (error)
461
- this.debug(`Failed to remove device ${error.stack}`);
462
- // skip error if force
463
- if (!force) {
464
- throw error;
465
- } else {
466
- this.debug(`Force remove`);
467
- }
468
- }
469
- try {
470
- await device.removeFromDatabase();
471
- } catch (error) {
472
- // skip error
473
- if (error)
474
- this.debug(`Failed to remove from DB ${error.stack}`);
475
- }
476
- this.debug('Remove successful.');
477
- if (callback) callback();
478
- this.callExtensionMethod(
479
- 'onDeviceRemove',
480
- [device],
481
- );
482
- }
483
- } catch (error) {
484
- this.error(`Failed to remove ${error.stack}`);
485
- if (callback) callback(`Failed to remove ${error.stack}`);
486
- }
487
- }
488
-
489
- // Zigbee events
490
- async handleDeviceLeave(message) {
491
- try {
492
- this.debug('handleDeviceLeave', message);
493
- const entity = await this.resolveEntity(message.device || message.ieeeAddr);
494
- const friendlyName = (entity) ? entity.name : message.ieeeAddr;
495
- this.debug(`Device '${friendlyName}' left the network`);
496
- this.emit('leave', message.ieeeAddr);
497
- // Call extensions
498
- this.callExtensionMethod(
499
- 'onDeviceLeave',
500
- [message, entity],
501
- );
502
- } catch (error) {
503
- this.error(`Failed to handleDeviceLeave ${error.stack}`);
504
- }
505
- }
506
-
507
- async handleDeviceAnnounce(message) {
508
- this.debug('handleDeviceAnnounce', message);
509
- const entity = await this.resolveEntity(message.device || message.ieeeAddr);
510
- const friendlyName = entity.name;
511
- this.warn(`Device '${friendlyName}' announced itself`);
512
- this.emit('pairing', `Device '${friendlyName}' announced itself`);
513
- if (!this.herdsman.getPermitJoin()) this.callExtensionMethod('registerDevicePing', [message.device, entity]);
514
- // if has modelID so can create device
515
- if (entity.device && entity.device._modelID) {
516
- entity.device.modelID = entity.device._modelID;
517
- this.emit('new', entity);
518
- }
519
- }
520
-
521
- async handleDeviceJoined(message) {
522
- this.debug('handleDeviceJoined', message);
523
- //const entity = await this.resolveEntity(message.device || message.ieeeAddr);
524
- //this.emit('new', entity);
525
- }
526
-
527
- async handleDeviceInterview(message) {
528
- this.debug('handleDeviceInterview', message);
529
- // safeguard: We do not allow to start an interview if the network is not opened
530
- if (message.status === 'started' && !this.herdsman.getPermitJoin()) {
531
- this.warn(`Blocked interview for '${message.ieeeAddr}' because the network is closed`);
532
- return;
533
- }
534
- const entity = await this.resolveEntity(message.device || message.ieeeAddr);
535
- const friendlyName = entity.name;
536
- if (message.status === 'successful') {
537
- this.info(`Successfully interviewed '${friendlyName}', device has succesfully been paired`);
538
-
539
- if (entity.mapped) {
540
- const {vendor, description, model} = entity.mapped;
541
- this.info(
542
- `Device '${friendlyName}' is supported, identified as: ${vendor} ${description} (${model})`
543
- );
544
-
545
- const log = {friendly_name: friendlyName, model, vendor, description, supported: true};
546
- this.emit('pairing', 'Interview successful', JSON.stringify(log));
547
- entity.device.modelID = entity.device._modelID;
548
- this.emit('new', entity);
549
- // send to extensions again (for configure)
550
- this.callExtensionMethod(
551
- 'onZigbeeEvent',
552
- [message, entity ? entity.mapped : null],
553
- );
554
- } else {
555
- this.debug(
556
- `Device '${friendlyName}' with Zigbee model '${message.device.modelID}' is NOT supported, ` +
557
- `please follow https://www.zigbee2mqtt.io/how_tos/how_to_support_new_devices.html`
558
- );
559
- this.emit('pairing', 'Interview successful', {friendly_name: friendlyName, supported: false});
560
- entity.device.modelID = entity.device._modelID;
561
- this.emit('new', entity);
562
- }
563
- } else if (message.status === 'failed') {
564
- this.error(`Failed to interview '${friendlyName}', device has not succesfully been paired. ${message.error}`);
565
- this.emit('pairing', 'Interview failed', friendlyName);
566
- } else {
567
- if (message.status === 'started') {
568
- this.info(`Starting interview of '${friendlyName}'`);
569
- this.emit('pairing', 'Interview started', friendlyName);
570
- }
571
- }
572
- }
573
-
574
- async handleMessage(data) {
575
- this.debug(`handleMessage`, data);
576
- const entity = await this.resolveEntity(data.device || data.ieeeAddr);
577
- const name = (entity && entity._modelID) ? entity._modelID : data.device.ieeeAddr;
578
- this.debug(
579
- `Received Zigbee message from '${name}', type '${data.type}', cluster '${data.cluster}'` +
580
- `, data '${JSON.stringify(data.data)}' from endpoint ${data.endpoint.ID}` +
581
- (data.hasOwnProperty('groupID') ? ` with groupID ${data.groupID}` : ``)
582
- );
583
- this.event(data.type, entity, data);
584
-
585
- // Call extensions
586
- this.callExtensionMethod(
587
- 'onZigbeeEvent',
588
- [data, entity ? entity.mapped : null],
589
- );
590
- }
591
-
592
- async getMap(callback) {
593
- try {
594
-
595
- const devices = this.herdsman.getDevices(true);
596
- const lqis = [];
597
- const routing = [];
598
-
599
- for (const device of devices.filter((d) => d.type != 'EndDevice')) {
600
- const resolved = await this.resolveEntity(device);
601
- let result;
602
-
603
- try {
604
- result = await device.lqi();
605
- } catch (error) {
606
- if (error)
607
- this.debug(`Failed to execute LQI for '${resolved.name}'. ${safeJsonStringify(error.stack)}`);
608
-
609
- lqis.push({
610
- parent: 'undefined',
611
- networkAddress: 0,
612
- ieeeAddr: device.ieeeAddr,
613
- lqi: 'undefined',
614
- relationship: 0,
615
- depth: 0,
616
- status: 'offline',
617
- });
618
- }
619
-
620
- if (result !== undefined) {
621
- for (const dev of result.neighbors) {
622
- if (dev.ieeeAddr !== '0xffffffffffffffff' && dev !== undefined) {
623
- lqis.push({
624
- parent: resolved.device.ieeeAddr,
625
- networkAddress: dev.networkAddress,
626
- ieeeAddr: dev.ieeeAddr,
627
- lqi: dev.linkquality,
628
- relationship: dev.relationship,
629
- depth: dev.depth,
630
- status: (dev.linkquality > 0) ? 'online' : 'offline',
631
- });
632
- }
633
- }
634
- }
635
-
636
- this.debug(`LQI succeeded for '${resolved.name}'`);
637
-
638
- try {
639
- result = await device.routingTable();
640
- } catch (error) {
641
- if (error) {
642
- this.debug(`Failed to execute routing table for '${resolved.name}'. ${safeJsonStringify(error.stack)}`);
643
- }
644
- }
645
-
646
- this.debug(`Routing for '${resolved.name}': ${safeJsonStringify(result)}`);
647
- if (result !== undefined) {
648
- if (result.table !== undefined) {
649
- for (const dev of result.table) {
650
- routing.push({
651
- source: resolved.device.ieeeAddr,
652
- destination: dev.destinationAddress,
653
- nextHop: dev.nextHop,
654
- status: dev.status,
655
- });
656
- }
657
- }
658
- }
659
- this.debug(`Routing table succeeded for '${resolved.name}'`);
660
-
661
- }
662
- this.debug(`Get map succeeded ${safeJsonStringify(lqis)}`);
663
-
664
- if (callback) callback({lqis: lqis, routing: routing});
665
- } catch (error) {
666
- this.debug(`Failed to get map: ${safeJsonStringify(error.stack)}`);
667
- }
668
- }
669
-
670
- async publish(deviceID, cid, cmd, zclData, cfg, ep, type, callback) {
671
- const entity = await this.resolveEntity(deviceID, ep);
672
- const device = entity.device;
673
- const endpoint = entity.endpoint;
674
- if (!device) {
675
- this.error(
676
- `Zigbee cannot publish message to device because '${deviceID}' is not known`
677
- );
678
- return;
679
- }
680
- if (!endpoint) {
681
- this.error(
682
- `Zigbee cannot publish message to endpoint because '${ep}' is not known`
683
- );
684
- return;
685
- }
686
-
687
- this.debug(`Zigbee publish to '${deviceID}', ${cid} - cmd ${cmd} - payload ${JSON.stringify(zclData)} - cfg ${JSON.stringify(cfg)} - endpoint ${ep}`);
688
-
689
- if (cfg == null) {
690
- cfg = {};
691
- }
692
-
693
- if (type === 'foundation') {
694
- cfg.disableDefaultResponse = true;
695
- if (cmd === 'read') {
696
- // needs to be iterateable (string[] | number [])
697
- zclData[Symbol.iterator] = function* () {
698
- let k;
699
- for (k in this) {
700
- yield k;
701
- }
702
- };
703
- }
704
- let result;
705
- if (cmd === 'configReport') {
706
- result = await endpoint.configureReporting(cid, zclData, cfg);
707
- } else {
708
- result = await endpoint[cmd](cid, zclData, cfg);
709
- }
710
- if (callback) callback(undefined, result);
711
- } else {
712
- cfg.disableDefaultResponse = false;
713
- const result = await endpoint.command(cid, cmd, zclData, cfg);
714
- if (callback) callback(undefined, result);
715
- }
716
- }
717
-
718
- async addDevToGroup(devId, groupId) {
719
- try {
720
- const entity = await this.resolveEntity(devId);
721
- const group = await this.resolveEntity(groupId);
722
- this.debug(`entity: ${safeJsonStringify(entity)}`);
723
- this.debug(`group: ${safeJsonStringify(group)}`);
724
- await entity.endpoint.addToGroup(group.mapped);
725
- }
726
- catch (error) {
727
- this.error(`Exception when trying to Add ${devId} to group ${groupId}`, error);
728
- return { error:`Failed to add ${devId} to group ${groupId}: ${JSON.stringify(error)}` };
729
- }
730
- return {};
731
- }
732
-
733
- async removeDevFromAllGroups(devId) {
734
- try {
735
- const entity = await this.resolveEntity(devId);
736
- this.debug(`entity: ${safeJsonStringify(entity)}`);
737
- await entity.endpoint.removeFromAllGroups();
738
- }
739
- catch (error) {
740
- this.error(`Exception when trying remove ${devId} from all groups`, error);
741
- return { error: `Failed to remove dev ${devId} from all groups: ${error}`};
742
- }
743
- return {};
744
- }
745
-
746
- bind(ep, cluster, target, callback) {
747
- const log = ` ${ep.device.ieeeAddr} - ${cluster}`;
748
- target = !target ? this.getCoordinator() : target;
749
-
750
- this.debug(`Binding ${log}`);
751
- ep.bind(cluster, target, (error) => {
752
- if (error) {
753
- this.error(`Failed to bind ${log} - (${error})`);
754
- } else {
755
- this.debug(`Successfully bound ${log}`);
756
- }
757
-
758
- callback(error);
759
- });
760
- }
761
-
762
- unbind(ep, cluster, target, callback) {
763
- const log = ` ${ep.device.ieeeAddr} - ${cluster}`;
764
- target = !target ? this.getCoordinator() : target;
765
-
766
- this.debug(`Unbinding ${log}`);
767
- ep.unbind(cluster, target, (error) => {
768
- if (error) {
769
- this.error(`Failed to unbind ${log} - (${error})`);
770
- } else {
771
- this.debug(`Successfully unbound ${log}`);
772
- }
773
-
774
- callback(error);
775
- });
776
- }
777
-
778
- reset(mode, callback) {
779
- try {
780
- this.herdsman.reset(mode);
781
- if (callback) callback();
782
- } catch (error) {
783
- this.error(`Failed to reset ${error.stack}`);
784
- if (callback) callback(error);
785
- }
786
- }
787
-
788
- async touchlinkReset(permitTime) {
789
- try {
790
- await this.herdsman.touchlinkFactoryResetFirst();
791
- this.permitJoin(permitTime);
792
- } catch (error) {
793
- this.error(`Failed to touchlinkReset ${error.stack}`);
794
- }
795
- }
796
-
797
- async getChannelsEnergy() {
798
- const payload = {
799
- dstaddr: 0x0,
800
- dstaddrmode: 0x02,
801
- channelmask: 0x07FFF800,
802
- scanduration: 0x5,
803
- scancount: 1,
804
- nwkmanageraddr: 0x0000
805
- };
806
- const energyScan = this.herdsman.adapter.znp.waitFor(
807
- 2, //unpi_1.Constants.Type.AREQ,
808
- 5, //Subsystem.ZDO,
809
- 'mgmtNwkUpdateNotify'
810
- );
811
- await this.herdsman.adapter.znp.request(
812
- 0x5, //Subsystem.ZDO
813
- 'mgmtNwkUpdateReq',
814
- payload,
815
- energyScan.ID
816
- );
817
- const result = await energyScan.start().promise;
818
- return result.payload;
819
- }
820
- }
821
-
822
- module.exports = ZigbeeController;
1
+ 'use strict';
2
+
3
+ const pathLib = require('path');
4
+ const ZigbeeHerdsman = require('zigbee-herdsman');
5
+ const zigbeeHerdsmanConverters = require('zigbee-herdsman-converters');
6
+ const EventEmitter = require('events').EventEmitter;
7
+ const safeJsonStringify = require('./json');
8
+ const DeviceAvailabilityExt = require('./zbDeviceAvailability');
9
+ const DeviceConfigureExt = require('./zbDeviceConfigure');
10
+ const DeviceEventExt = require('./zbDeviceEvent');
11
+ const DelayedActionExt = require('./zbDelayedAction');
12
+ const groupConverters = [
13
+ zigbeeHerdsmanConverters.toZigbeeConverters.light_onoff_brightness,
14
+ zigbeeHerdsmanConverters.toZigbeeConverters.light_colortemp,
15
+ zigbeeHerdsmanConverters.toZigbeeConverters.light_color,
16
+ zigbeeHerdsmanConverters.toZigbeeConverters.light_alert,
17
+ zigbeeHerdsmanConverters.toZigbeeConverters.ignore_transition,
18
+ zigbeeHerdsmanConverters.toZigbeeConverters.light_brightness_move,
19
+ zigbeeHerdsmanConverters.toZigbeeConverters.light_brightness_step,
20
+ zigbeeHerdsmanConverters.toZigbeeConverters.light_colortemp_move,
21
+ zigbeeHerdsmanConverters.toZigbeeConverters.light_colortemp_step,
22
+ zigbeeHerdsmanConverters.toZigbeeConverters.ight_colortemp_startup
23
+ ];
24
+
25
+ function isFunction(functionToCheck) {
26
+ return functionToCheck && {}.toString.call(functionToCheck) === '[object Function]';
27
+ }
28
+
29
+ class ZigbeeController extends EventEmitter {
30
+ /*
31
+ events:
32
+
33
+ log - log (level, msg, data)
34
+ event - preparsed device events (type, dev, msg, data)
35
+ new - new device connected to network (id, msg)
36
+ leave - device leave the network (id, msg)
37
+ join - join countdown (counter)
38
+ ready - connection successfull ()
39
+ */
40
+ constructor(adapter) {
41
+ super();
42
+ this.adapter = adapter;
43
+ this._permitJoinTime = 0;
44
+ this.herdsman_started = false;
45
+ this.extensions = [
46
+ new DeviceAvailabilityExt(this, {}),
47
+ new DeviceConfigureExt(this, {}),
48
+ new DeviceEventExt(this, {}),
49
+ new DelayedActionExt(this, {}),
50
+ ];
51
+ }
52
+
53
+ configure(options) {
54
+ const herdsmanSettings = {
55
+ network: {
56
+ panID: options.net.panId,
57
+ extendedPanID: options.net.extPanId,
58
+ channelList: options.net.channelList,
59
+ networkKey: options.net.precfgkey,
60
+ },
61
+ databasePath: pathLib.join(options.dbDir, options.dbPath),
62
+ backupPath: pathLib.join(options.dbDir, options.backupPath),
63
+ serialPort: {
64
+ baudRate: options.sp.baudRate,
65
+ rtscts: options.sp.rtscts,
66
+ path: options.sp.port,
67
+ adapter: options.sp.adapter,
68
+ },
69
+ adapter: {
70
+ forceStartWithInconsistentAdapterConfiguration: options.startWithInconsistent
71
+ },
72
+ };
73
+ // https://github.com/ioBroker/ioBroker.zigbee/issues/668
74
+ if (!options.extPanIdFix) {
75
+ delete herdsmanSettings.network.extendedPanID;
76
+ herdsmanSettings.network.extenedPanID = options.net.extPanId;
77
+ }
78
+
79
+ if (options.transmitPower == undefined) {
80
+ this.transmitPower = 0;
81
+ } else {
82
+ this.transmitPower = options.transmitPower;
83
+ }
84
+ this.disableLed = options.disableLed;
85
+
86
+ this.debug(`Using zigbee-herdsman with settings: ${JSON.stringify(herdsmanSettings)}`);
87
+ this.herdsman = new ZigbeeHerdsman.Controller(herdsmanSettings, this.adapter.log);
88
+ this.callExtensionMethod('setOptions', [{ disableActivePing: options.disablePing, disableForcedPing: false, pingTimeout:300, pingCount:3 }] );
89
+ }
90
+
91
+ // Start controller
92
+ async start() {
93
+ try {
94
+ //this.debug(`Using zigbee-herdsman with settings2: ${JSON.stringify(this)}`);
95
+ this.debug(`Starting zigbee-herdsman...`);
96
+ await this.herdsman.start();
97
+
98
+ this.herdsman.on('adapterDisconnected', this.handleDisconnected.bind(this));
99
+ this.herdsman.on('deviceAnnounce', this.handleDeviceAnnounce.bind(this));
100
+ this.herdsman.on('deviceInterview', this.handleDeviceInterview.bind(this));
101
+ this.herdsman.on('deviceJoined', this.handleDeviceJoined.bind(this));
102
+ this.herdsman.on('deviceLeave', this.handleDeviceLeave.bind(this));
103
+ this.herdsman.on('message', this.handleMessage.bind(this));
104
+
105
+ this.debug('zigbee-herdsman started');
106
+ this.herdsman_started = true;
107
+ this.info(`Coordinator firmware version: ${JSON.stringify(await this.herdsman.getCoordinatorVersion())}`);
108
+
109
+ // debug info from herdsman getNetworkParameters
110
+ const debNetworkParam = JSON.parse(JSON.stringify(await this.herdsman.getNetworkParameters()));
111
+ const extendedPanIDDebug = (typeof debNetworkParam.extendedPanID == 'string') ? debNetworkParam.extendedPanID.replace('0x','') : debNetworkParam.extendedPanID;
112
+
113
+ let extPanIDDebug = '';
114
+ for (let i = extendedPanIDDebug.length - 1; i >= 0; i--) {
115
+ extPanIDDebug += extendedPanIDDebug[i-1];
116
+ extPanIDDebug += extendedPanIDDebug[i];
117
+ i--;
118
+ }
119
+
120
+ this.debug(`Zigbee network parameters: panID=${debNetworkParam.panID} channel=${debNetworkParam.channel} extendedPanID=${extPanIDDebug}`);
121
+
122
+ } catch (e) {
123
+ this.sendError(e);
124
+ this.error('Starting zigbee-herdsman problem : ' + JSON.stringify(e.message));
125
+ throw 'Error herdsman start';
126
+ }
127
+ // Check if we have to turn off the led
128
+ try {
129
+ if (this.disableLed) {
130
+ this.info('Disable LED');
131
+ await this.herdsman.setLED(false);
132
+ } else {
133
+ await this.herdsman.setLED(true);
134
+ }
135
+ } catch (e) {
136
+ this.info('Unable to disable LED, unsupported function.');
137
+ this.sendError(e);
138
+ }
139
+
140
+ // only for CC1352P and CC26X2R1 transmit power
141
+ let powerText = 'normal';
142
+
143
+ if (this.transmitPower != '0') {
144
+ switch(this.transmitPower) {
145
+ case '-22':
146
+ powerText = 'low';
147
+ break;
148
+ case '19':
149
+ powerText = 'high';
150
+ break;
151
+ default:
152
+ powerText = 'normal';
153
+ }
154
+ }
155
+
156
+
157
+ this.info(' --> transmitPower : ' + powerText);
158
+ try {
159
+ await this.herdsman.setTransmitPower(this.transmitPower);
160
+ } catch (e) {
161
+ this.sendError(e);
162
+ this.info('Unable to set transmit power, unsupported function.');
163
+ }
164
+
165
+
166
+ // Call extensions
167
+ this.callExtensionMethod('onZigbeeStarted', []);
168
+
169
+ // Log zigbee clients on startup
170
+ const devices = await this.getClients();
171
+ if (devices.length > 0) {
172
+ this.info(`Currently ${devices.length} devices are joined:`);
173
+ } else {
174
+ this.info(`Currently no devices.`);
175
+ }
176
+ for (const device of devices) {
177
+ const entity = await this.resolveEntity(device);
178
+ // ensure that objects for all found clients are present
179
+ this.callExtensionMethod('registerDevicePing', [device, entity]);
180
+
181
+ if (entity.mapped) this.emit('new', entity);
182
+ this.info(
183
+ (entity.device.ieeeAddr) +
184
+ ` (addr ${entity.device.networkAddress}): ` +
185
+ (entity.mapped ?
186
+ `${entity.mapped.model} - ${entity.mapped.vendor} ${entity.mapped.description} ` :
187
+ `Not supported (model ${entity.device.modelID})`) +
188
+ `(${entity.device.type})`
189
+ );
190
+ }
191
+
192
+ this.emit('ready');
193
+ }
194
+
195
+ info(message, data) {
196
+ this.emit('log', 'info', message, data);
197
+ }
198
+
199
+ error(message, data) {
200
+ this.emit('log', 'error', message, data);
201
+ }
202
+
203
+ debug(message, data) {
204
+ this.emit('log', 'debug', message, data);
205
+ }
206
+
207
+ warn(message, data) {
208
+ this.emit('log', 'warn', message, data);
209
+ }
210
+
211
+ event(type, dev, message, data) {
212
+ this.emit('event', type, dev, message, data);
213
+ }
214
+
215
+ sendError(error, message) {
216
+ this.adapter.sendError(error, message);
217
+ }
218
+
219
+ callExtensionMethod(method, parameters) {
220
+ for (const extension of this.extensions) {
221
+ if (extension[method]) {
222
+ try {
223
+ if (parameters !== undefined) {
224
+ extension[method](...parameters);
225
+ } else {
226
+ extension[method]();
227
+ }
228
+ } catch (error) {
229
+ this.sendError(error);
230
+ this.error(`Failed to call '${extension.constructor.name}' '${method}' (${error.stack})`);
231
+ }
232
+ }
233
+ }
234
+ }
235
+
236
+ async getClients(all) {
237
+ if (this.herdsman.database) {
238
+ const devices = await this.herdsman.getDevices();
239
+ if (all) {
240
+ return devices;
241
+ } else {
242
+ return devices.filter((device) => device.type !== 'Coordinator');
243
+ }
244
+ } else {
245
+ return [];
246
+ }
247
+ }
248
+
249
+ async getGroups() {
250
+ try {
251
+ return this.herdsman.getGroups();
252
+ } catch (error) {
253
+ this.sendError(error);
254
+ return undefined;
255
+ }
256
+ }
257
+
258
+ async removeGroupById(id) {
259
+ const group = await this.getGroupByID(id);
260
+ try {
261
+ if (group) group.removeFromDatabase();
262
+ } catch (error) {
263
+ this.sendError(error);
264
+ this.error('error in removeGroupById: ' + error);
265
+ }
266
+ }
267
+
268
+ async getGroupByID(id) {
269
+ try {
270
+ return this.herdsman.getGroupByID(id);
271
+ } catch (error) {
272
+ this.sendError(error);
273
+ return undefined;
274
+ }
275
+ }
276
+
277
+ async getGroupMembersFromController(id) {
278
+ const members = [];
279
+ try {
280
+ const group = await this.getGroupByID(id);
281
+ if (group) {
282
+ const groupmembers = group.members;
283
+
284
+ for (const member of groupmembers) {
285
+ const nwk = member.deviceNetworkAddress;
286
+ const device = this.getDeviceByNetworkAddress(nwk);
287
+ if (device && device.ieeeAddr) members.push( { device:device.ieeeAddr, model:device.modelID } );
288
+ }
289
+ }
290
+ else {
291
+ return undefined;
292
+ }
293
+
294
+ } catch (error) {
295
+ this.sendError(error);
296
+ if (error) this.error('getGroupMembersFromController: error is ' + JSON.stringify(error) + ' ' + JSON.stringify(new Error().stack));
297
+ else this.error('unidentifed error in getGroupMembersFromController');
298
+ }
299
+ return members;
300
+ }
301
+
302
+ getDevice(key) {
303
+ return this.herdsman.getDeviceByIeeeAddr(key);
304
+ }
305
+
306
+ getDevicesByType(type) {
307
+ return this.herdsman.getDevicesByType(type);
308
+ }
309
+
310
+ getDeviceByNetworkAddress(networkAddress) {
311
+ return this.herdsman.getDeviceByNetworkAddress(networkAddress);
312
+ }
313
+
314
+ async resolveEntity(key, ep) {
315
+ //assert(typeof key === 'string' || key.constructor.name === 'Device', `Wrong type '${typeof key}'`);
316
+
317
+ if (typeof key === 'string') {
318
+ if (key === 'coordinator') {
319
+ const coordinator = this.herdsman.getDevicesByType('Coordinator')[0];
320
+ return {
321
+ type: 'device',
322
+ device: coordinator,
323
+ endpoint: coordinator.getEndpoint(1),
324
+ name: 'Coordinator',
325
+ };
326
+ } else {
327
+ const device = await this.herdsman.getDeviceByIeeeAddr(key);
328
+ if (device) {
329
+ const mapped = zigbeeHerdsmanConverters.findByDevice(device);
330
+ const endpoints = mapped && mapped.endpoint ? mapped.endpoint(device) : null;
331
+ let endpoint;
332
+ if (endpoints && ep != undefined && endpoints[ep]) {
333
+ endpoint = device.getEndpoint(endpoints[ep]);
334
+ } else if (endpoints && endpoints['default']) {
335
+ endpoint = device.getEndpoint(endpoints['default']);
336
+ } else {
337
+ const epNum = parseInt(ep);
338
+ if (!isNaN(epNum)) {
339
+ endpoint = device.getEndpoint(epNum);
340
+ } else {
341
+ endpoint = device.endpoints[0];
342
+ }
343
+ }
344
+ return {
345
+ type: 'device',
346
+ device,
347
+ mapped,
348
+ endpoint,
349
+ endpoints: device.endpoints,
350
+ name: key,
351
+ };
352
+ } else {
353
+ return;
354
+ }
355
+ }
356
+ } else if (typeof key === 'number') {
357
+ let group = await this.herdsman.getGroupByID(key);
358
+ if (!group) group = await this.herdsman.createGroup(key);
359
+ group.toZigbee = groupConverters;
360
+ group.model = 'group';
361
+ return {
362
+ type: 'group',
363
+ mapped: group,
364
+ group,
365
+ name: `Group ${key}`,
366
+ };
367
+ } else {
368
+ return {
369
+ type: 'device',
370
+ device: key,
371
+ mapped: zigbeeHerdsmanConverters.findByDevice(key),
372
+ name: key.type === 'Coordinator' ? 'Coordinator' : key.ieeeAddr,
373
+ };
374
+ }
375
+ }
376
+
377
+ async incMsgHandler(message){
378
+ this.debug('incoming msg', message);
379
+ const device = await this.herdsman.getDeviceByIeeeAddr(message.srcaddr);
380
+ if (!device) {
381
+ this.debug('Message without device!');
382
+ return;
383
+ }
384
+ // We can't handle devices without modelId.
385
+ if (!device.modelId) {
386
+ this.debug('Message without modelId!');
387
+ return;
388
+ }
389
+ this.event('msg', device.ieeeAddr, message, {
390
+ modelId: device.modelId
391
+ });
392
+ }
393
+
394
+ // Stop controller
395
+ async stop() {
396
+ // Call extensions
397
+ await this.callExtensionMethod('stop', []);
398
+
399
+ try {
400
+ await this.permitJoin(0);
401
+ await this.herdsman.stop();
402
+ } catch (error) {
403
+ this.sendError(error);
404
+ if (this.herdsman_started)
405
+ this.error(`Failed to stop zigbee (${error.stack})`);
406
+ else {
407
+ this.warn(`Failed to stop zigbee during startup`);
408
+ }
409
+ }
410
+ }
411
+
412
+ async handleDisconnected() {
413
+ this.herdsman_started = false;
414
+ this.emit('disconnect');
415
+ }
416
+
417
+ connected() {
418
+ return this.herdsman_started;
419
+ }
420
+
421
+ // Permit join
422
+ async permitJoin(permitTime, devid, failure) {
423
+ let permitDev;
424
+ if (isFunction(devid) && !isFunction(failure)) {
425
+ failure = devid;
426
+ } else {
427
+ permitDev = this.getDevice(devid);
428
+ }
429
+
430
+ if (permitTime) {
431
+ this.info('Zigbee: allowing new devices to join.');
432
+ } else {
433
+ this.info('Zigbee: disabling joining new devices.');
434
+ }
435
+ try
436
+ {
437
+ if (permitTime && !this.herdsman.getPermitJoin()) {
438
+ clearInterval(this._permitJoinInterval);
439
+ this._permitJoinTime = permitTime;
440
+ await this.herdsman.permitJoin(true, permitDev);
441
+ this._permitJoinInterval = setInterval(async () => {
442
+ this.emit('pairing', 'Pairing time left', this._permitJoinTime);
443
+ if (this._permitJoinTime === 0) {
444
+ this.info('Zigbee: stop joining');
445
+ clearInterval(this._permitJoinInterval);
446
+ await this.herdsman.permitJoin(false);
447
+ }
448
+ this._permitJoinTime -= 1;
449
+ }, 1000);
450
+ } else if (this.herdsman.getPermitJoin()) {
451
+ if (permitTime) {
452
+ this.info('Joining already permitted');
453
+ } else {
454
+ clearInterval(this._permitJoinInterval);
455
+ await this.herdsman.permitJoin(false, permitDev);
456
+ }
457
+ }
458
+ } catch (e) {
459
+ this.sendError(e);
460
+ this.error(`Failed to open the network: ${e.stack}`);
461
+ }
462
+ }
463
+
464
+ // Remove device
465
+ async remove(deviceID, force, callback) {
466
+ try {
467
+ const device = await this.herdsman.getDeviceByIeeeAddr(deviceID);
468
+ if (device) {
469
+ try {
470
+ await this.herdsman.adapter.removeDevice(device.networkAddress, device.ieeeAddr);
471
+ } catch (error) {
472
+ this.sendError(error);
473
+ if (error)
474
+ this.debug(`Failed to remove device ${error.stack}`);
475
+ // skip error if force
476
+ if (!force) {
477
+ throw error;
478
+ } else {
479
+ this.debug(`Force remove`);
480
+ }
481
+ }
482
+ try {
483
+ await device.removeFromDatabase();
484
+ } catch (error) {
485
+ this.sendError(error);
486
+ // skip error
487
+ if (error)
488
+ this.debug(`Failed to remove from DB ${error.stack}`);
489
+ }
490
+ this.debug('Remove successful.');
491
+ if (callback) callback();
492
+ this.callExtensionMethod(
493
+ 'onDeviceRemove',
494
+ [device],
495
+ );
496
+ }
497
+ } catch (error) {
498
+ this.sendError(error);
499
+ this.error(`Failed to remove ${error.stack}`);
500
+ if (callback) callback(`Failed to remove ${error.stack}`);
501
+ }
502
+ }
503
+
504
+ // Zigbee events
505
+ async handleDeviceLeave(message) {
506
+ try {
507
+ this.debug('handleDeviceLeave', message);
508
+ const entity = await this.resolveEntity(message.device || message.ieeeAddr);
509
+ const friendlyName = (entity) ? entity.name : message.ieeeAddr;
510
+ this.debug(`Device '${friendlyName}' left the network`);
511
+ this.emit('leave', message.ieeeAddr);
512
+ // Call extensions
513
+ this.callExtensionMethod(
514
+ 'onDeviceLeave',
515
+ [message, entity],
516
+ );
517
+ } catch (error) {
518
+ this.sendError(error);
519
+ this.error(`Failed to handleDeviceLeave ${error.stack}`);
520
+ }
521
+ }
522
+
523
+ async handleDeviceAnnounce(message) {
524
+ this.debug('handleDeviceAnnounce', message);
525
+ const entity = await this.resolveEntity(message.device || message.ieeeAddr);
526
+ const friendlyName = entity.name;
527
+ this.warn(`Device '${friendlyName}' announced itself`);
528
+ this.emit('pairing', `Device '${friendlyName}' announced itself`);
529
+ if (!this.herdsman.getPermitJoin()) this.callExtensionMethod('registerDevicePing', [message.device, entity]);
530
+ // if has modelID so can create device
531
+ if (entity.device && entity.device._modelID) {
532
+ entity.device.modelID = entity.device._modelID;
533
+ this.emit('new', entity);
534
+ }
535
+ }
536
+
537
+ async handleDeviceJoined(message) {
538
+ this.debug('handleDeviceJoined', message);
539
+ //const entity = await this.resolveEntity(message.device || message.ieeeAddr);
540
+ //this.emit('new', entity);
541
+ }
542
+
543
+ async handleDeviceInterview(message) {
544
+ this.debug('handleDeviceInterview', message);
545
+ // safeguard: We do not allow to start an interview if the network is not opened
546
+ if (message.status === 'started' && !this.herdsman.getPermitJoin()) {
547
+ this.warn(`Blocked interview for '${message.ieeeAddr}' because the network is closed`);
548
+ return;
549
+ }
550
+ const entity = await this.resolveEntity(message.device || message.ieeeAddr);
551
+ const friendlyName = entity.name;
552
+ if (message.status === 'successful') {
553
+ this.info(`Successfully interviewed '${friendlyName}', device has succesfully been paired`);
554
+
555
+ if (entity.mapped) {
556
+ const {vendor, description, model} = entity.mapped;
557
+ this.info(
558
+ `Device '${friendlyName}' is supported, identified as: ${vendor} ${description} (${model})`
559
+ );
560
+
561
+ const log = {friendly_name: friendlyName, model, vendor, description, supported: true};
562
+ this.emit('pairing', 'Interview successful', JSON.stringify(log));
563
+ entity.device.modelID = entity.device._modelID;
564
+ this.emit('new', entity);
565
+ // send to extensions again (for configure)
566
+ this.callExtensionMethod(
567
+ 'onZigbeeEvent',
568
+ [message, entity ? entity.mapped : null],
569
+ );
570
+ } else {
571
+ this.debug(
572
+ `Device '${friendlyName}' with Zigbee model '${message.device.modelID}' is NOT supported, ` +
573
+ `please follow https://www.zigbee2mqtt.io/how_tos/how_to_support_new_devices.html`
574
+ );
575
+ this.emit('pairing', 'Interview successful', {friendly_name: friendlyName, supported: false});
576
+ entity.device.modelID = entity.device._modelID;
577
+ this.emit('new', entity);
578
+ }
579
+ } else if (message.status === 'failed') {
580
+ this.error(`Failed to interview '${friendlyName}', device has not succesfully been paired. ${message.error}`);
581
+ this.emit('pairing', 'Interview failed', friendlyName);
582
+ } else {
583
+ if (message.status === 'started') {
584
+ this.info(`Starting interview of '${friendlyName}'`);
585
+ this.emit('pairing', 'Interview started', friendlyName);
586
+ }
587
+ }
588
+ }
589
+
590
+ async handleMessage(data) {
591
+ this.debug(`handleMessage`, data);
592
+ const entity = await this.resolveEntity(data.device || data.ieeeAddr);
593
+ const name = (entity && entity._modelID) ? entity._modelID : data.device.ieeeAddr;
594
+ this.debug(
595
+ `Received Zigbee message from '${name}', type '${data.type}', cluster '${data.cluster}'` +
596
+ `, data '${JSON.stringify(data.data)}' from endpoint ${data.endpoint.ID}` +
597
+ (data.hasOwnProperty('groupID') ? ` with groupID ${data.groupID}` : ``)
598
+ );
599
+ this.event(data.type, entity, data);
600
+
601
+ // Call extensions
602
+ this.callExtensionMethod(
603
+ 'onZigbeeEvent',
604
+ [data, entity ? entity.mapped : null],
605
+ );
606
+ }
607
+
608
+ async getMap(callback) {
609
+ try {
610
+
611
+ const devices = this.herdsman.getDevices(true);
612
+ const lqis = [];
613
+ const routing = [];
614
+
615
+ for (const device of devices.filter((d) => d.type != 'EndDevice')) {
616
+ const resolved = await this.resolveEntity(device);
617
+ let result;
618
+
619
+ try {
620
+ result = await device.lqi();
621
+ } catch (error) {
622
+ this.sendError(error);
623
+ if (error)
624
+ this.debug(`Failed to execute LQI for '${resolved.name}'. ${safeJsonStringify(error.stack)}`);
625
+
626
+ lqis.push({
627
+ parent: 'undefined',
628
+ networkAddress: 0,
629
+ ieeeAddr: device.ieeeAddr,
630
+ lqi: 'undefined',
631
+ relationship: 0,
632
+ depth: 0,
633
+ status: 'offline',
634
+ });
635
+ }
636
+
637
+ if (result !== undefined) {
638
+ for (const dev of result.neighbors) {
639
+ if (dev.ieeeAddr !== '0xffffffffffffffff' && dev !== undefined) {
640
+ lqis.push({
641
+ parent: resolved.device.ieeeAddr,
642
+ networkAddress: dev.networkAddress,
643
+ ieeeAddr: dev.ieeeAddr,
644
+ lqi: dev.linkquality,
645
+ relationship: dev.relationship,
646
+ depth: dev.depth,
647
+ status: (dev.linkquality > 0) ? 'online' : 'offline',
648
+ });
649
+ }
650
+ }
651
+ }
652
+
653
+ this.debug(`LQI succeeded for '${resolved.name}'`);
654
+
655
+ try {
656
+ result = await device.routingTable();
657
+ } catch (error) {
658
+ this.sendError(error);
659
+ if (error) {
660
+ this.debug(`Failed to execute routing table for '${resolved.name}'. ${safeJsonStringify(error.stack)}`);
661
+ }
662
+ }
663
+
664
+ this.debug(`Routing for '${resolved.name}': ${safeJsonStringify(result)}`);
665
+ if (result !== undefined) {
666
+ if (result.table !== undefined) {
667
+ for (const dev of result.table) {
668
+ routing.push({
669
+ source: resolved.device.ieeeAddr,
670
+ destination: dev.destinationAddress,
671
+ nextHop: dev.nextHop,
672
+ status: dev.status,
673
+ });
674
+ }
675
+ }
676
+ }
677
+ this.debug(`Routing table succeeded for '${resolved.name}'`);
678
+
679
+ }
680
+ this.debug(`Get map succeeded ${safeJsonStringify(lqis)}`);
681
+
682
+ if (callback) callback({lqis: lqis, routing: routing});
683
+ } catch (error) {
684
+ this.sendError(error);
685
+ this.debug(`Failed to get map: ${safeJsonStringify(error.stack)}`);
686
+ }
687
+ }
688
+
689
+ async publish(deviceID, cid, cmd, zclData, cfg, ep, type, callback, zclSeqNum) {
690
+ const entity = await this.resolveEntity(deviceID, ep);
691
+ const device = entity.device;
692
+ const endpoint = entity.endpoint;
693
+ if (!device) {
694
+ this.error(
695
+ `Zigbee cannot publish message to device because '${deviceID}' is not known`
696
+ );
697
+ return;
698
+ }
699
+ if (!endpoint) {
700
+ this.error(
701
+ `Zigbee cannot publish message to endpoint because '${ep}' is not known`
702
+ );
703
+ return;
704
+ }
705
+
706
+ this.debug(`Zigbee publish to '${deviceID}', ${cid} - cmd ${cmd} - payload ${JSON.stringify(zclData)} - cfg ${JSON.stringify(cfg)} - endpoint ${ep}`);
707
+
708
+ if (cfg == null) {
709
+ cfg = {};
710
+ }
711
+
712
+ if (type === 'foundation') {
713
+ cfg.disableDefaultResponse = true;
714
+ if (cmd === 'read' && !Array.isArray(zclData)) {
715
+ // needs to be iterateable (string[] | number [])
716
+ zclData[Symbol.iterator] = function* () {
717
+ let k;
718
+ for (k in this) {
719
+ yield k;
720
+ }
721
+ };
722
+ }
723
+ let result;
724
+ if (cmd === 'configReport') {
725
+ result = await endpoint.configureReporting(cid, zclData, cfg);
726
+ } else {
727
+ result = await endpoint[cmd](cid, zclData, cfg);
728
+ }
729
+ if (callback) callback(undefined, result);
730
+ }
731
+ else if(type === 'functionalResp'){
732
+ if (!zclSeqNum) {
733
+ this.error(
734
+ `Zigbee cannot publish commandResponse message to device because zclSeqNum is not known`
735
+ );
736
+ return;
737
+ }
738
+ cfg.disableDefaultResponse = false;
739
+ const result = await endpoint.commandResponse(cid, cmd, zclData, cfg, zclSeqNum);
740
+ if (callback) callback(undefined, result);
741
+ }
742
+ else {
743
+ cfg.disableDefaultResponse = false;
744
+ const result = await endpoint.command(cid, cmd, zclData, cfg);
745
+ if (callback) callback(undefined, result);
746
+ }
747
+ }
748
+
749
+ async addDevToGroup(devId, groupId) {
750
+ try {
751
+ const entity = await this.resolveEntity(devId);
752
+ const group = await this.resolveEntity(groupId);
753
+ this.debug(`entity: ${safeJsonStringify(entity)}`);
754
+ this.debug(`group: ${safeJsonStringify(group)}`);
755
+ await entity.endpoint.addToGroup(group.mapped);
756
+ } catch (error) {
757
+ this.sendError(error);
758
+ this.error(`Exception when trying to Add ${devId} to group ${groupId}`, error);
759
+ return { error:`Failed to add ${devId} to group ${groupId}: ${JSON.stringify(error)}` };
760
+ }
761
+ return {};
762
+ }
763
+
764
+ async removeDevFromAllGroups(devId) {
765
+ try {
766
+ const entity = await this.resolveEntity(devId);
767
+ this.debug(`entity: ${safeJsonStringify(entity)}`);
768
+ await entity.endpoint.removeFromAllGroups();
769
+ } catch (error) {
770
+ this.sendError(error);
771
+ this.error(`Exception when trying remove ${devId} from all groups`, error);
772
+ return { error: `Failed to remove dev ${devId} from all groups: ${error}`};
773
+ }
774
+ return {};
775
+ }
776
+
777
+ bind(ep, cluster, target, callback) {
778
+ const log = ` ${ep.device.ieeeAddr} - ${cluster}`;
779
+ target = !target ? this.getCoordinator() : target;
780
+
781
+ this.debug(`Binding ${log}`);
782
+ ep.bind(cluster, target, (error) => {
783
+ if (error) {
784
+ this.sendError(error);
785
+ this.error(`Failed to bind ${log} - (${error})`);
786
+ } else {
787
+ this.debug(`Successfully bound ${log}`);
788
+ }
789
+
790
+ callback(error);
791
+ });
792
+ }
793
+
794
+ unbind(ep, cluster, target, callback) {
795
+ const log = ` ${ep.device.ieeeAddr} - ${cluster}`;
796
+ target = !target ? this.getCoordinator() : target;
797
+
798
+ this.debug(`Unbinding ${log}`);
799
+ ep.unbind(cluster, target, (error) => {
800
+ if (error) {
801
+ this.error(`Failed to unbind ${log} - (${error})`);
802
+ } else {
803
+ this.debug(`Successfully unbound ${log}`);
804
+ }
805
+
806
+ callback(error);
807
+ });
808
+ }
809
+
810
+ reset(mode, callback) {
811
+ try {
812
+ this.herdsman.reset(mode);
813
+ if (callback) callback();
814
+ } catch (error) {
815
+ this.sendError(error);
816
+ this.error(`Failed to reset ${error.stack}`);
817
+ if (callback) callback(error);
818
+ }
819
+ }
820
+
821
+ async touchlinkReset(permitTime) {
822
+ try {
823
+ await this.herdsman.touchlinkFactoryResetFirst();
824
+ this.permitJoin(permitTime);
825
+ } catch (error) {
826
+ this.sendError(error);
827
+ this.error(`Failed to touchlinkReset ${error.stack}`);
828
+ }
829
+ }
830
+
831
+ async getChannelsEnergy() {
832
+ const payload = {
833
+ dstaddr: 0x0,
834
+ dstaddrmode: 0x02,
835
+ channelmask: 0x07FFF800,
836
+ scanduration: 0x5,
837
+ scancount: 1,
838
+ nwkmanageraddr: 0x0000
839
+ };
840
+ const energyScan = this.herdsman.adapter.znp.waitFor(
841
+ 2, //unpi_1.Constants.Type.AREQ,
842
+ 5, //Subsystem.ZDO,
843
+ 'mgmtNwkUpdateNotify'
844
+ );
845
+ await this.herdsman.adapter.znp.request(
846
+ 0x5, //Subsystem.ZDO
847
+ 'mgmtNwkUpdateReq',
848
+ payload,
849
+ energyScan.ID
850
+ );
851
+ const result = await energyScan.start().promise;
852
+ return result.payload;
853
+ }
854
+ }
855
+
856
+ module.exports = ZigbeeController;