iobroker.zigbee 1.7.1 → 1.7.4

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 (55) hide show
  1. package/.eslintignore +1 -1
  2. package/.eslintrc.json +36 -36
  3. package/.github/FUNDING.yml +3 -3
  4. package/.github/stale.yml +13 -13
  5. package/.github/workflows/test-and-release.yml +151 -151
  6. package/.travis/wiki.sh +27 -27
  7. package/LICENSE +21 -21
  8. package/README.md +379 -373
  9. package/admin/adapter-settings.js +244 -244
  10. package/admin/admin.js +2926 -2926
  11. package/admin/img/MLI-404011-MLI-404049.png +0 -0
  12. package/admin/img/philips_hue_lom001.png +0 -0
  13. package/admin/index.html +159 -159
  14. package/admin/index_m.html +1161 -1161
  15. package/admin/moment.min.js +1 -1
  16. package/admin/shuffle.min.js +2 -2
  17. package/admin/tab_m.html +944 -944
  18. package/admin/vis-network.min.css +1 -1
  19. package/admin/vis-network.min.js +27 -27
  20. package/admin/words.js +112 -112
  21. package/docs/de/readme.md +27 -27
  22. package/docs/en/readme.md +30 -30
  23. package/docs/flashing_via_arduino_(en).md +110 -110
  24. package/docs/ru/readme.md +28 -28
  25. package/docs/tutorial/groups-1.png +0 -0
  26. package/docs/tutorial/groups-2.png +0 -0
  27. package/docs/tutorial/tab-dev-1.png +0 -0
  28. package/io-package.json +354 -330
  29. package/lib/backup.js +171 -171
  30. package/lib/binding.js +325 -325
  31. package/lib/colors.js +460 -460
  32. package/lib/commands.js +501 -501
  33. package/lib/developer.js +148 -148
  34. package/lib/devices.js +3145 -3145
  35. package/lib/exclude.js +168 -168
  36. package/lib/exposes.js +804 -804
  37. package/lib/groups.js +342 -342
  38. package/lib/json.js +60 -60
  39. package/lib/networkmap.js +56 -56
  40. package/lib/ota.js +179 -179
  41. package/lib/rgb.js +255 -255
  42. package/lib/seriallist.js +37 -37
  43. package/lib/states.js +6416 -6416
  44. package/lib/statescontroller.js +666 -658
  45. package/lib/tools.js +54 -54
  46. package/lib/utils.js +151 -151
  47. package/lib/zbBaseExtension.js +36 -36
  48. package/lib/zbDelayedAction.js +152 -152
  49. package/lib/zbDeviceAvailability.js +315 -315
  50. package/lib/zbDeviceConfigure.js +152 -152
  51. package/lib/zbDeviceEvent.js +49 -49
  52. package/lib/zigbeecontroller.js +946 -946
  53. package/package.json +2 -2
  54. package/support/docgen.js +93 -93
  55. package/admin/img/zbt_remote.png +0 -0
@@ -1,946 +1,946 @@
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
- case '20':
152
- powerText = 'high+';
153
- break;
154
- default:
155
- powerText = 'normal';
156
- }
157
- }
158
-
159
-
160
- this.info(' --> transmitPower : ' + powerText);
161
- try {
162
- await this.herdsman.setTransmitPower(this.transmitPower);
163
- } catch (e) {
164
- this.sendError(e);
165
- this.info('Unable to set transmit power, unsupported function.');
166
- }
167
-
168
-
169
- // Call extensions
170
- this.callExtensionMethod('onZigbeeStarted', []);
171
-
172
- // Log zigbee clients on startup
173
- const devices = await this.getClients();
174
- if (devices.length > 0) {
175
- this.info(`Currently ${devices.length} devices are joined:`);
176
- } else {
177
- this.info(`Currently no devices.`);
178
- }
179
- for (const device of devices) {
180
- const entity = await this.resolveEntity(device);
181
- this.adapter.getObject(device.ieeeAddr.substr(2),(err, obj) => {
182
- if (obj && obj.common && obj.common.deactivated)
183
- {
184
- this.callExtensionMethod('deregisterDevicePing', [device, entity]);
185
- }
186
- else {
187
- this.callExtensionMethod('registerDevicePing', [device, entity]);
188
- }
189
- });
190
- // ensure that objects for all found clients are present
191
-
192
- if (entity.mapped) this.emit('new', entity);
193
- this.info(
194
- (entity.device.ieeeAddr) +
195
- ` (addr ${entity.device.networkAddress}): ` +
196
- (entity.mapped ?
197
- `${entity.mapped.model} - ${entity.mapped.vendor} ${entity.mapped.description} ` :
198
- `Not supported (model ${entity.device.modelID})`) +
199
- `(${entity.device.type})`
200
- );
201
- }
202
-
203
- this.emit('ready');
204
- }
205
-
206
- info(message, data) {
207
- this.emit('log', 'info', message, data);
208
- }
209
-
210
- error(message, data) {
211
- this.emit('log', 'error', message, data);
212
- }
213
-
214
- debug(message, data) {
215
- this.emit('log', 'debug', message, data);
216
- }
217
-
218
- warn(message, data) {
219
- this.emit('log', 'warn', message, data);
220
- }
221
-
222
- event(type, dev, message, data) {
223
- this.emit('event', type, dev, message, data);
224
- }
225
-
226
- sendError(error, message) {
227
- this.adapter.sendError(error, message);
228
- }
229
-
230
- callExtensionMethod(method, parameters) {
231
- const result = [];
232
- for (const extension of this.extensions) {
233
- if (extension[method]) {
234
- try {
235
- if (parameters !== undefined) {
236
- result.push(extension[method](...parameters));
237
- } else {
238
- result.push(extension[method]());
239
- }
240
- } catch (error) {
241
- this.sendError(error);
242
- this.error(`Failed to call '${extension.constructor.name}' '${method}' (${error.stack})`);
243
- }
244
- }
245
- }
246
- return Promise.all(result);
247
- }
248
-
249
- async getClients(all) {
250
- if (this.herdsman.database) {
251
- const devices = await this.herdsman.getDevices();
252
- if (all) {
253
- return devices;
254
- } else {
255
- return devices.filter((device) => device.type !== 'Coordinator');
256
- }
257
- } else {
258
- return [];
259
- }
260
- }
261
-
262
- async getGroups() {
263
- try {
264
- const rv = await this.herdsman.getGroups();
265
- return rv;
266
- } catch (error) {
267
- this.sendError(error);
268
- this.error(JSON.stringify(error));
269
- return undefined;
270
- }
271
- }
272
-
273
- async removeGroupById(id) {
274
- const group = await this.getGroupByID(id);
275
- try {
276
- if (group) group.removeFromDatabase();
277
- } catch (error) {
278
- this.sendError(error);
279
- this.error('error in removeGroupById: ' + error);
280
- }
281
- }
282
-
283
- async getGroupByID(id) {
284
- try {
285
- return this.herdsman.getGroupByID(id);
286
- } catch (error) {
287
- this.sendError(error);
288
- return undefined;
289
- }
290
- }
291
-
292
- async verifyGroupExists(id) {
293
- const nid = (typeof(id) === 'number' ? id:parseInt(id));
294
- let group = await this.herdsman.getGroupByID(nid);
295
- if (!group) {
296
- group = await this.herdsman.createGroup(nid);
297
- group.toZigbee = groupConverters;
298
- group.model = 'group';
299
- this.debug('verifyGroupExists: created group ' + nid);
300
- }
301
- else {
302
- this.debug('verifyGroupExists: group ' + nid + ' exists');
303
- }
304
-
305
- }
306
-
307
- async getGroupMembersFromController(id) {
308
- const members = [];
309
- try {
310
- const group = await this.getGroupByID(id);
311
- if (group) {
312
- const groupmembers = group.members;
313
- for (const member of groupmembers) {
314
- const epid = (member.ID ? member.ID:-1);
315
- const nwk = member.deviceNetworkAddress;
316
- const device = this.getDeviceByNetworkAddress(nwk);
317
- if (device && device.ieeeAddr) members.push( { ieee:device.ieeeAddr, model:device.modelID, epid:epid, ep:member } );
318
- }
319
- }
320
- else {
321
- return undefined;
322
- }
323
-
324
- } catch (error) {
325
- this.sendError(error);
326
- if (error) this.error('getGroupMembersFromController: error is ' + JSON.stringify(error) + ' ' + JSON.stringify(new Error().stack));
327
- else this.error('unidentifed error in getGroupMembersFromController');
328
- }
329
- return members;
330
- }
331
-
332
- getDevice(key) {
333
- return this.herdsman.getDeviceByIeeeAddr(key);
334
- }
335
-
336
- getDevicesByType(type) {
337
- return this.herdsman.getDevicesByType(type);
338
- }
339
-
340
- getDeviceByNetworkAddress(networkAddress) {
341
- return this.herdsman.getDeviceByNetworkAddress(networkAddress);
342
- }
343
-
344
- async resolveEntity(key, ep) {
345
- //assert(typeof key === 'string' || key.constructor.name === 'Device', `Wrong type '${typeof key}'`);
346
-
347
- if (typeof key === 'string') {
348
- if (key === 'coordinator') {
349
- const coordinator = this.herdsman.getDevicesByType('Coordinator')[0];
350
- return {
351
- type: 'device',
352
- device: coordinator,
353
- endpoint: coordinator.getEndpoint(1),
354
- name: 'Coordinator',
355
- };
356
- } else {
357
- const device = await this.herdsman.getDeviceByIeeeAddr(key);
358
- if (device) {
359
- const mapped = zigbeeHerdsmanConverters.findByDevice(device);
360
- const endpoints = mapped && mapped.endpoint ? mapped.endpoint(device) : null;
361
- let endpoint;
362
- if (endpoints && ep != undefined && endpoints[ep]) {
363
- endpoint = device.getEndpoint(endpoints[ep]);
364
- } else if (endpoints && endpoints['default']) {
365
- endpoint = device.getEndpoint(endpoints['default']);
366
- } else {
367
- const epNum = parseInt(ep);
368
- if (!isNaN(epNum)) {
369
- endpoint = device.getEndpoint(epNum);
370
- } else {
371
- endpoint = device.endpoints[0];
372
- }
373
- }
374
- return {
375
- type: 'device',
376
- device,
377
- mapped,
378
- endpoint,
379
- endpoints: device.endpoints,
380
- name: key,
381
- };
382
- } else {
383
- return;
384
- }
385
- }
386
- } else if (typeof key === 'number') {
387
- let group = await this.herdsman.getGroupByID(key);
388
- if (!group) group = await this.herdsman.createGroup(key);
389
- group.toZigbee = groupConverters;
390
- group.model = 'group';
391
- return {
392
- type: 'group',
393
- mapped: group,
394
- group,
395
- name: `Group ${key}`,
396
- };
397
- } else {
398
- return {
399
- type: 'device',
400
- device: key,
401
- mapped: zigbeeHerdsmanConverters.findByDevice(key),
402
- name: key.type === 'Coordinator' ? 'Coordinator' : key.ieeeAddr,
403
- };
404
- }
405
- }
406
-
407
- async incMsgHandler(message){
408
- this.debug('incoming msg', message);
409
- const device = await this.herdsman.getDeviceByIeeeAddr(message.srcaddr);
410
- if (!device) {
411
- this.debug('Message without device!');
412
- return;
413
- }
414
- // We can't handle devices without modelId.
415
- if (!device.modelId) {
416
- this.debug('Message without modelId!');
417
- return;
418
- }
419
- this.event('msg', device.ieeeAddr, message, {
420
- modelId: device.modelId
421
- });
422
- }
423
-
424
- // Stop controller
425
- async stop() {
426
- // Call extensions
427
- await this.callExtensionMethod('stop', []);
428
-
429
- try {
430
- await this.permitJoin(0);
431
- await this.herdsman.stop();
432
- } catch (error) {
433
- this.sendError(error);
434
- if (this.herdsman_started)
435
- this.error(`Failed to stop zigbee (${error.stack})`);
436
- else {
437
- this.warn(`Failed to stop zigbee during startup`);
438
- }
439
- }
440
- }
441
-
442
- async handleDisconnected() {
443
- this.herdsman_started = false;
444
- this.emit('disconnect');
445
- }
446
-
447
- connected() {
448
- return this.herdsman_started;
449
- }
450
-
451
- // Permit join
452
- async permitJoin(permitTime, devid, failure) {
453
- let permitDev;
454
- if (isFunction(devid) && !isFunction(failure)) {
455
- failure = devid;
456
- } else {
457
- permitDev = this.getDevice(devid);
458
- }
459
-
460
- if (permitTime) {
461
- this.info('Zigbee: allowing new devices to join.');
462
- } else {
463
- this.info('Zigbee: disabling joining new devices.');
464
- }
465
- try
466
- {
467
- if (permitTime && !this.herdsman.getPermitJoin()) {
468
- clearInterval(this._permitJoinInterval);
469
- this._permitJoinTime = permitTime;
470
- await this.herdsman.permitJoin(true, permitDev);
471
- this._permitJoinInterval = setInterval(async () => {
472
- this.emit('pairing', 'Pairing time left', this._permitJoinTime);
473
- if (this._permitJoinTime === 0) {
474
- this.info('Zigbee: stop joining');
475
- clearInterval(this._permitJoinInterval);
476
- await this.herdsman.permitJoin(false);
477
- }
478
- this._permitJoinTime -= 1;
479
- }, 1000);
480
- } else if (this.herdsman.getPermitJoin()) {
481
- if (permitTime) {
482
- this.info('Joining already permitted');
483
- } else {
484
- clearInterval(this._permitJoinInterval);
485
- await this.herdsman.permitJoin(false, permitDev);
486
- }
487
- }
488
- } catch (e) {
489
- this.sendError(e);
490
- this.error(`Failed to open the network: ${e.stack}`);
491
- }
492
- }
493
-
494
- // Remove device
495
- async remove(deviceID, force, callback) {
496
- try {
497
- const device = await this.herdsman.getDeviceByIeeeAddr(deviceID);
498
- if (device) {
499
- try {
500
- await this.herdsman.adapter.removeDevice(device.networkAddress, device.ieeeAddr);
501
- } catch (error) {
502
- this.sendError(error);
503
- if (error)
504
- this.debug(`Failed to remove device ${error.stack}`);
505
- // skip error if force
506
- if (!force) {
507
- throw error;
508
- } else {
509
- this.debug(`Force remove`);
510
- }
511
- }
512
- try {
513
- await device.removeFromDatabase();
514
- } catch (error) {
515
- this.sendError(error);
516
- // skip error
517
- if (error)
518
- this.debug(`Failed to remove from DB ${error.stack}`);
519
- }
520
- this.debug('Remove successful.');
521
- if (callback) callback();
522
- this.callExtensionMethod(
523
- 'onDeviceRemove',
524
- [device],
525
- );
526
- }
527
- } catch (error) {
528
- this.sendError(error);
529
- this.error(`Failed to remove ${error.stack}`);
530
- if (callback) callback(`Failed to remove ${error.stack}`);
531
- }
532
- }
533
-
534
- // Zigbee events
535
- async handleDeviceLeave(message) {
536
- try {
537
- this.debug('handleDeviceLeave', message);
538
- const entity = await this.resolveEntity(message.device || message.ieeeAddr);
539
- const friendlyName = (entity) ? entity.name : message.ieeeAddr;
540
- this.debug(`Device '${friendlyName}' left the network`);
541
- this.emit('leave', message.ieeeAddr);
542
- // Call extensions
543
- this.callExtensionMethod(
544
- 'onDeviceLeave',
545
- [message, entity],
546
- );
547
- } catch (error) {
548
- this.sendError(error);
549
- this.error(`Failed to handleDeviceLeave ${error.stack}`);
550
- }
551
- }
552
-
553
- async handleDeviceAnnounce(message) {
554
- this.debug('handleDeviceAnnounce', message);
555
- const entity = await this.resolveEntity(message.device || message.ieeeAddr);
556
- const friendlyName = entity.name;
557
- this.warn(`Device '${friendlyName}' announced itself`);
558
-
559
- if (entity && entity.mapped) {
560
- this.callExtensionMethod(
561
- 'onZigbeeEvent',
562
- [ {'device': message.device, 'type': 'deviceAnnounce'}, (entity ? entity.mapped : null)]);
563
- }
564
-
565
- this.emit('pairing', `Device '${friendlyName}' announced itself`);
566
- if (!this.herdsman.getPermitJoin()) this.callExtensionMethod('registerDevicePing', [message.device, entity]);
567
- // if has modelID so can create device
568
- if (entity.device && entity.device._modelID) {
569
- entity.device.modelID = entity.device._modelID;
570
- this.emit('new', entity);
571
- }
572
- }
573
-
574
-
575
- async handleDeviceJoined(message) {
576
- this.debug('handleDeviceJoined', message);
577
- //const entity = await this.resolveEntity(message.device || message.ieeeAddr);
578
- //this.emit('new', entity);
579
- }
580
-
581
- async handleDeviceInterview(message) {
582
- this.debug('handleDeviceInterview', message);
583
- // safeguard: We do not allow to start an interview if the network is not opened
584
- if (message.status === 'started' && !this.herdsman.getPermitJoin()) {
585
- this.warn(`Blocked interview for '${message.ieeeAddr}' because the network is closed`);
586
- return;
587
- }
588
- const entity = await this.resolveEntity(message.device || message.ieeeAddr);
589
- const friendlyName = entity.name;
590
- if (message.status === 'successful') {
591
- this.info(`Successfully interviewed '${friendlyName}', device has succesfully been paired`);
592
-
593
- if (entity.mapped) {
594
- const {vendor, description, model} = entity.mapped;
595
- this.info(
596
- `Device '${friendlyName}' is supported, identified as: ${vendor} ${description} (${model})`
597
- );
598
-
599
- const log = {friendly_name: friendlyName, model, vendor, description, supported: true};
600
- this.emit('pairing', 'Interview successful', JSON.stringify(log));
601
- entity.device.modelID = entity.device._modelID;
602
- this.emit('new', entity);
603
- // send to extensions again (for configure)
604
- this.callExtensionMethod(
605
- 'onZigbeeEvent',
606
- [message, entity ? entity.mapped : null],
607
- );
608
- } else {
609
- this.debug(
610
- `Device '${friendlyName}' with Zigbee model '${message.device.modelID}' is NOT supported, ` +
611
- `please follow https://www.zigbee2mqtt.io/how_tos/how_to_support_new_devices.html`
612
- );
613
- this.emit('pairing', 'Interview successful', {friendly_name: friendlyName, supported: false});
614
- entity.device.modelID = entity.device._modelID;
615
- this.emit('new', entity);
616
- }
617
- } else if (message.status === 'failed') {
618
- this.error(`Failed to interview '${friendlyName}', device has not succesfully been paired. ${message.error}`);
619
- this.emit('pairing', 'Interview failed', friendlyName);
620
- } else {
621
- if (message.status === 'started') {
622
- this.info(`Starting interview of '${friendlyName}'`);
623
- this.emit('pairing', 'Interview started', friendlyName);
624
- }
625
- }
626
- }
627
-
628
- async handleMessage(data) {
629
- this.debug(`handleMessage`, data);
630
- const entity = await this.resolveEntity(data.device || data.ieeeAddr);
631
- const name = (entity && entity._modelID) ? entity._modelID : data.device.ieeeAddr;
632
- this.debug(
633
- `Received Zigbee message from '${name}', type '${data.type}', cluster '${data.cluster}'` +
634
- `, data '${JSON.stringify(data.data)}' from endpoint ${data.endpoint.ID}` +
635
- (data.hasOwnProperty('groupID') ? ` with groupID ${data.groupID}` : ``)
636
- );
637
- this.event(data.type, entity, data);
638
-
639
- // Call extensions
640
- this.callExtensionMethod(
641
- 'onZigbeeEvent',
642
- [data, entity ? entity.mapped : null],
643
- );
644
- }
645
-
646
- async getMap(callback) {
647
- try {
648
-
649
- const devices = this.herdsman.getDevices(true);
650
- const lqis = [];
651
- const routing = [];
652
-
653
- for (const device of devices.filter((d) => d.type != 'EndDevice')) {
654
- const resolved = await this.resolveEntity(device);
655
- let result;
656
-
657
- try {
658
- result = await device.lqi();
659
- } catch (error) {
660
- this.sendError(error);
661
- if (error)
662
- this.debug(`Failed to execute LQI for '${resolved.name}'. ${safeJsonStringify(error.stack)}`);
663
-
664
- lqis.push({
665
- parent: 'undefined',
666
- networkAddress: 0,
667
- ieeeAddr: device.ieeeAddr,
668
- lqi: 'undefined',
669
- relationship: 0,
670
- depth: 0,
671
- status: 'offline',
672
- });
673
- }
674
-
675
- if (result !== undefined) {
676
- for (const dev of result.neighbors) {
677
- if (dev.ieeeAddr !== '0xffffffffffffffff' && dev !== undefined) {
678
- lqis.push({
679
- parent: resolved.device.ieeeAddr,
680
- networkAddress: dev.networkAddress,
681
- ieeeAddr: dev.ieeeAddr,
682
- lqi: dev.linkquality,
683
- relationship: dev.relationship,
684
- depth: dev.depth,
685
- status: (dev.linkquality > 0) ? 'online' : 'offline',
686
- });
687
- }
688
- }
689
- }
690
-
691
- this.debug(`LQI succeeded for '${resolved.name}'`);
692
-
693
- try {
694
- result = await device.routingTable();
695
- } catch (error) {
696
- this.sendError(error);
697
- if (error) {
698
- this.debug(`Failed to execute routing table for '${resolved.name}'. ${safeJsonStringify(error.stack)}`);
699
- }
700
- }
701
-
702
- this.debug(`Routing for '${resolved.name}': ${safeJsonStringify(result)}`);
703
- if (result !== undefined) {
704
- if (result.table !== undefined) {
705
- for (const dev of result.table) {
706
- routing.push({
707
- source: resolved.device.ieeeAddr,
708
- destination: dev.destinationAddress,
709
- nextHop: dev.nextHop,
710
- status: dev.status,
711
- });
712
- }
713
- }
714
- }
715
- this.debug(`Routing table succeeded for '${resolved.name}'`);
716
-
717
- }
718
- this.debug(`Get map succeeded ${safeJsonStringify(lqis)}`);
719
-
720
- if (callback) callback({lqis: lqis, routing: routing});
721
- } catch (error) {
722
- this.sendError(error);
723
- this.debug(`Failed to get map: ${safeJsonStringify(error.stack)}`);
724
- }
725
- }
726
-
727
- async publish(deviceID, cid, cmd, zclData, cfg, ep, type, callback, zclSeqNum) {
728
- const entity = await this.resolveEntity(deviceID, ep);
729
- const device = entity.device;
730
- const endpoint = entity.endpoint;
731
- if (!device) {
732
- this.error(
733
- `Zigbee cannot publish message to device because '${deviceID}' is not known`
734
- );
735
- return;
736
- }
737
- if (!endpoint) {
738
- this.error(
739
- `Zigbee cannot publish message to endpoint because '${ep}' is not known`
740
- );
741
- return;
742
- }
743
-
744
- this.debug(`Zigbee publish to '${deviceID}', ${cid} - cmd ${cmd} - payload ${JSON.stringify(zclData)} - cfg ${JSON.stringify(cfg)} - endpoint ${ep}`);
745
-
746
- if (cfg == null) {
747
- cfg = {};
748
- }
749
-
750
- if (type === 'foundation') {
751
- cfg.disableDefaultResponse = true;
752
- if (cmd === 'read' && !Array.isArray(zclData)) {
753
- // needs to be iterateable (string[] | number [])
754
- zclData[Symbol.iterator] = function* () {
755
- let k;
756
- for (k in this) {
757
- yield k;
758
- }
759
- };
760
- }
761
- let result;
762
- if (cmd === 'configReport') {
763
- result = await endpoint.configureReporting(cid, zclData, cfg);
764
- } else {
765
- result = await endpoint[cmd](cid, zclData, cfg);
766
- }
767
- if (callback) callback(undefined, result);
768
- }
769
- else if(type === 'functionalResp'){
770
- cfg.disableDefaultResponse = false;
771
- const result = await endpoint.commandResponse(cid, cmd, zclData, cfg, zclSeqNum);
772
- if (callback) callback(undefined, result);
773
- }
774
- else {
775
- cfg.disableDefaultResponse = false;
776
- const result = await endpoint.command(cid, cmd, zclData, cfg);
777
- if (callback) callback(undefined, result);
778
- }
779
- }
780
-
781
- async addDevToGroup(devId, groupId, epid) {
782
- try {
783
- const entity = await this.resolveEntity(devId);
784
- const group = await this.resolveEntity(groupId);
785
- this.debug(`addDevFromGroup - entity: ${safeJsonStringify(entity)}`);
786
- this.debug(`addDevFromGroup - group: ${safeJsonStringify(group)}`);
787
- if (epid != undefined) {
788
- for (const ep of entity.endpoints) {
789
- if (ep.ID == epid && (ep.inputClusters.includes(4) || ep.outputClusters.includes(4)))
790
- {
791
- this.debug(`adding endpoint ${ep.ID} (${epid}) to group ${groupId}`)
792
- await(ep.addToGroup(group.mapped));
793
- }
794
- }
795
-
796
- }
797
- else
798
- {
799
- if (entity.endpoint.inputClusters.includes(4))
800
- {
801
- this.debug(`adding endpoint ${entity.endpoint.ID} to group`)
802
- await entity.endpoint.addToGroup(group.mapped);
803
- }
804
- else {
805
- let added = false;
806
- for (const ep of entity.endpoints)
807
- {
808
- if (ep.inputClusters.includes(4))
809
- {
810
- this.debug(`adding endpoint ${ep.ID} to group`)
811
- await ep.addToGroup(group.mapped);
812
- added = true;
813
- break;
814
- }
815
- }
816
- if (!added) throw ('cluster genGroups not supported');
817
- }
818
- }
819
- } catch (error) {
820
- this.sendError(error);
821
- this.error(`Exception when trying to Add ${devId} to group ${groupId}`, error);
822
- return { error:`Failed to add ${devId} to group ${groupId}: ${JSON.stringify(error)}` };
823
- }
824
- return {};
825
- }
826
-
827
- async removeDevFromGroup(devId, groupId, epid) {
828
- try {
829
- const entity = await this.resolveEntity(devId);
830
- const group = await this.resolveEntity(groupId);
831
- this.debug(`removeDevFromGroup - entity: ${safeJsonStringify(entity)}`);
832
- this.debug(`removeDevFromGroup - group: ${safeJsonStringify(group)}`);
833
- if (epid != undefined) {
834
- for (const ep of entity.endpoints) {
835
- if (ep.ID == epid && (ep.inputClusters.includes(4) || ep.outputClusters.includes(4)))
836
- {
837
- this.debug(`removing endpoint ${ep.ID} (${epid}) group ${groupId}`)
838
- await(ep.removeFromGroup(group.mapped))
839
- }
840
- }
841
- } else await entity.endpoint.removeFromGroup(group.mapped);
842
- } catch (error) {
843
- this.sendError(error);
844
- this.error(`Exception when trying remove ${devId} (ep ${epid?epid:entity.endpoint.ID}) from group ${devId}`, error);
845
- return { error: `Failed to remove dev ${devId} (ep ${epid?epid:entity.endpoint.ID}) from group ${devId}`};
846
- }
847
- return {};
848
- }
849
-
850
- async removeDevFromAllGroups(devId) {
851
- try {
852
- const entity = await this.resolveEntity(devId);
853
- this.debug(`entity: ${safeJsonStringify(entity)}`);
854
- for (const ep of entity.endpoints) {
855
- if (ep.inputClusters.includes(4) || ep.outputClusters.includes(4))
856
- await ep.removefromAllGroups();
857
- }
858
- //await entity.endpoint.removeFromAllGroups();
859
- } catch (error) {
860
- this.sendError(error);
861
- this.error(`Exception when trying remove ${devId} from all groups`, error);
862
- return { error: `Failed to remove dev ${devId} from all groups: ${error}`};
863
- }
864
- return {};
865
- }
866
-
867
- bind(ep, cluster, target, callback) {
868
- const log = ` ${ep.device.ieeeAddr} - ${cluster}`;
869
- target = !target ? this.getCoordinator() : target;
870
-
871
- this.debug(`Binding ${log}`);
872
- ep.bind(cluster, target, (error) => {
873
- if (error) {
874
- this.sendError(error);
875
- this.error(`Failed to bind ${log} - (${error})`);
876
- } else {
877
- this.debug(`Successfully bound ${log}`);
878
- }
879
-
880
- callback(error);
881
- });
882
- }
883
-
884
- unbind(ep, cluster, target, callback) {
885
- const log = ` ${ep.device.ieeeAddr} - ${cluster}`;
886
- target = !target ? this.getCoordinator() : target;
887
-
888
- this.debug(`Unbinding ${log}`);
889
- ep.unbind(cluster, target, (error) => {
890
- if (error) {
891
- this.error(`Failed to unbind ${log} - (${error})`);
892
- } else {
893
- this.debug(`Successfully unbound ${log}`);
894
- }
895
-
896
- callback(error);
897
- });
898
- }
899
-
900
- reset(mode, callback) {
901
- try {
902
- this.herdsman.reset(mode);
903
- if (callback) callback();
904
- } catch (error) {
905
- this.sendError(error);
906
- this.error(`Failed to reset ${error.stack}`);
907
- if (callback) callback(error);
908
- }
909
- }
910
-
911
- async touchlinkReset(permitTime) {
912
- try {
913
- await this.herdsman.touchlinkFactoryResetFirst();
914
- this.permitJoin(permitTime);
915
- } catch (error) {
916
- this.sendError(error);
917
- this.error(`Failed to touchlinkReset ${error.stack}`);
918
- }
919
- }
920
-
921
- async getChannelsEnergy() {
922
- const payload = {
923
- dstaddr: 0x0,
924
- dstaddrmode: 0x02,
925
- channelmask: 0x07FFF800,
926
- scanduration: 0x5,
927
- scancount: 1,
928
- nwkmanageraddr: 0x0000
929
- };
930
- const energyScan = this.herdsman.adapter.znp.waitFor(
931
- 2, //unpi_1.Constants.Type.AREQ,
932
- 5, //Subsystem.ZDO,
933
- 'mgmtNwkUpdateNotify'
934
- );
935
- await this.herdsman.adapter.znp.request(
936
- 0x5, //Subsystem.ZDO
937
- 'mgmtNwkUpdateReq',
938
- payload,
939
- energyScan.ID
940
- );
941
- const result = await energyScan.start().promise;
942
- return result.payload;
943
- }
944
- }
945
-
946
- 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
+ case '20':
152
+ powerText = 'high+';
153
+ break;
154
+ default:
155
+ powerText = 'normal';
156
+ }
157
+ }
158
+
159
+
160
+ this.info(' --> transmitPower : ' + powerText);
161
+ try {
162
+ await this.herdsman.setTransmitPower(this.transmitPower);
163
+ } catch (e) {
164
+ this.sendError(e);
165
+ this.info('Unable to set transmit power, unsupported function.');
166
+ }
167
+
168
+
169
+ // Call extensions
170
+ this.callExtensionMethod('onZigbeeStarted', []);
171
+
172
+ // Log zigbee clients on startup
173
+ const devices = await this.getClients();
174
+ if (devices.length > 0) {
175
+ this.info(`Currently ${devices.length} devices are joined:`);
176
+ } else {
177
+ this.info(`Currently no devices.`);
178
+ }
179
+ for (const device of devices) {
180
+ const entity = await this.resolveEntity(device);
181
+ this.adapter.getObject(device.ieeeAddr.substr(2),(err, obj) => {
182
+ if (obj && obj.common && obj.common.deactivated)
183
+ {
184
+ this.callExtensionMethod('deregisterDevicePing', [device, entity]);
185
+ }
186
+ else {
187
+ this.callExtensionMethod('registerDevicePing', [device, entity]);
188
+ }
189
+ });
190
+ // ensure that objects for all found clients are present
191
+
192
+ if (entity.mapped) this.emit('new', entity);
193
+ this.info(
194
+ (entity.device.ieeeAddr) +
195
+ ` (addr ${entity.device.networkAddress}): ` +
196
+ (entity.mapped ?
197
+ `${entity.mapped.model} - ${entity.mapped.vendor} ${entity.mapped.description} ` :
198
+ `Not supported (model ${entity.device.modelID})`) +
199
+ `(${entity.device.type})`
200
+ );
201
+ }
202
+
203
+ this.emit('ready');
204
+ }
205
+
206
+ info(message, data) {
207
+ this.emit('log', 'info', message, data);
208
+ }
209
+
210
+ error(message, data) {
211
+ this.emit('log', 'error', message, data);
212
+ }
213
+
214
+ debug(message, data) {
215
+ this.emit('log', 'debug', message, data);
216
+ }
217
+
218
+ warn(message, data) {
219
+ this.emit('log', 'warn', message, data);
220
+ }
221
+
222
+ event(type, dev, message, data) {
223
+ this.emit('event', type, dev, message, data);
224
+ }
225
+
226
+ sendError(error, message) {
227
+ this.adapter.sendError(error, message);
228
+ }
229
+
230
+ callExtensionMethod(method, parameters) {
231
+ const result = [];
232
+ for (const extension of this.extensions) {
233
+ if (extension[method]) {
234
+ try {
235
+ if (parameters !== undefined) {
236
+ result.push(extension[method](...parameters));
237
+ } else {
238
+ result.push(extension[method]());
239
+ }
240
+ } catch (error) {
241
+ this.sendError(error);
242
+ this.error(`Failed to call '${extension.constructor.name}' '${method}' (${error.stack})`);
243
+ }
244
+ }
245
+ }
246
+ return Promise.all(result);
247
+ }
248
+
249
+ async getClients(all) {
250
+ if (this.herdsman.database) {
251
+ const devices = await this.herdsman.getDevices();
252
+ if (all) {
253
+ return devices;
254
+ } else {
255
+ return devices.filter((device) => device.type !== 'Coordinator');
256
+ }
257
+ } else {
258
+ return [];
259
+ }
260
+ }
261
+
262
+ async getGroups() {
263
+ try {
264
+ const rv = await this.herdsman.getGroups();
265
+ return rv;
266
+ } catch (error) {
267
+ this.sendError(error);
268
+ this.error(JSON.stringify(error));
269
+ return undefined;
270
+ }
271
+ }
272
+
273
+ async removeGroupById(id) {
274
+ const group = await this.getGroupByID(id);
275
+ try {
276
+ if (group) group.removeFromDatabase();
277
+ } catch (error) {
278
+ this.sendError(error);
279
+ this.error('error in removeGroupById: ' + error);
280
+ }
281
+ }
282
+
283
+ async getGroupByID(id) {
284
+ try {
285
+ return this.herdsman.getGroupByID(id);
286
+ } catch (error) {
287
+ this.sendError(error);
288
+ return undefined;
289
+ }
290
+ }
291
+
292
+ async verifyGroupExists(id) {
293
+ const nid = (typeof(id) === 'number' ? id:parseInt(id));
294
+ let group = await this.herdsman.getGroupByID(nid);
295
+ if (!group) {
296
+ group = await this.herdsman.createGroup(nid);
297
+ group.toZigbee = groupConverters;
298
+ group.model = 'group';
299
+ this.debug('verifyGroupExists: created group ' + nid);
300
+ }
301
+ else {
302
+ this.debug('verifyGroupExists: group ' + nid + ' exists');
303
+ }
304
+
305
+ }
306
+
307
+ async getGroupMembersFromController(id) {
308
+ const members = [];
309
+ try {
310
+ const group = await this.getGroupByID(id);
311
+ if (group) {
312
+ const groupmembers = group.members;
313
+ for (const member of groupmembers) {
314
+ const epid = (member.ID ? member.ID:-1);
315
+ const nwk = member.deviceNetworkAddress;
316
+ const device = this.getDeviceByNetworkAddress(nwk);
317
+ if (device && device.ieeeAddr) members.push( { ieee:device.ieeeAddr, model:device.modelID, epid:epid, ep:member } );
318
+ }
319
+ }
320
+ else {
321
+ return undefined;
322
+ }
323
+
324
+ } catch (error) {
325
+ this.sendError(error);
326
+ if (error) this.error('getGroupMembersFromController: error is ' + JSON.stringify(error) + ' ' + JSON.stringify(new Error().stack));
327
+ else this.error('unidentifed error in getGroupMembersFromController');
328
+ }
329
+ return members;
330
+ }
331
+
332
+ getDevice(key) {
333
+ return this.herdsman.getDeviceByIeeeAddr(key);
334
+ }
335
+
336
+ getDevicesByType(type) {
337
+ return this.herdsman.getDevicesByType(type);
338
+ }
339
+
340
+ getDeviceByNetworkAddress(networkAddress) {
341
+ return this.herdsman.getDeviceByNetworkAddress(networkAddress);
342
+ }
343
+
344
+ async resolveEntity(key, ep) {
345
+ //assert(typeof key === 'string' || key.constructor.name === 'Device', `Wrong type '${typeof key}'`);
346
+
347
+ if (typeof key === 'string') {
348
+ if (key === 'coordinator') {
349
+ const coordinator = this.herdsman.getDevicesByType('Coordinator')[0];
350
+ return {
351
+ type: 'device',
352
+ device: coordinator,
353
+ endpoint: coordinator.getEndpoint(1),
354
+ name: 'Coordinator',
355
+ };
356
+ } else {
357
+ const device = await this.herdsman.getDeviceByIeeeAddr(key);
358
+ if (device) {
359
+ const mapped = zigbeeHerdsmanConverters.findByDevice(device);
360
+ const endpoints = mapped && mapped.endpoint ? mapped.endpoint(device) : null;
361
+ let endpoint;
362
+ if (endpoints && ep != undefined && endpoints[ep]) {
363
+ endpoint = device.getEndpoint(endpoints[ep]);
364
+ } else if (endpoints && endpoints['default']) {
365
+ endpoint = device.getEndpoint(endpoints['default']);
366
+ } else {
367
+ const epNum = parseInt(ep);
368
+ if (!isNaN(epNum)) {
369
+ endpoint = device.getEndpoint(epNum);
370
+ } else {
371
+ endpoint = device.endpoints[0];
372
+ }
373
+ }
374
+ return {
375
+ type: 'device',
376
+ device,
377
+ mapped,
378
+ endpoint,
379
+ endpoints: device.endpoints,
380
+ name: key,
381
+ };
382
+ } else {
383
+ return;
384
+ }
385
+ }
386
+ } else if (typeof key === 'number') {
387
+ let group = await this.herdsman.getGroupByID(key);
388
+ if (!group) group = await this.herdsman.createGroup(key);
389
+ group.toZigbee = groupConverters;
390
+ group.model = 'group';
391
+ return {
392
+ type: 'group',
393
+ mapped: group,
394
+ group,
395
+ name: `Group ${key}`,
396
+ };
397
+ } else {
398
+ return {
399
+ type: 'device',
400
+ device: key,
401
+ mapped: zigbeeHerdsmanConverters.findByDevice(key),
402
+ name: key.type === 'Coordinator' ? 'Coordinator' : key.ieeeAddr,
403
+ };
404
+ }
405
+ }
406
+
407
+ async incMsgHandler(message){
408
+ this.debug('incoming msg', message);
409
+ const device = await this.herdsman.getDeviceByIeeeAddr(message.srcaddr);
410
+ if (!device) {
411
+ this.debug('Message without device!');
412
+ return;
413
+ }
414
+ // We can't handle devices without modelId.
415
+ if (!device.modelId) {
416
+ this.debug('Message without modelId!');
417
+ return;
418
+ }
419
+ this.event('msg', device.ieeeAddr, message, {
420
+ modelId: device.modelId
421
+ });
422
+ }
423
+
424
+ // Stop controller
425
+ async stop() {
426
+ // Call extensions
427
+ await this.callExtensionMethod('stop', []);
428
+
429
+ try {
430
+ await this.permitJoin(0);
431
+ await this.herdsman.stop();
432
+ } catch (error) {
433
+ this.sendError(error);
434
+ if (this.herdsman_started)
435
+ this.error(`Failed to stop zigbee (${error.stack})`);
436
+ else {
437
+ this.warn(`Failed to stop zigbee during startup`);
438
+ }
439
+ }
440
+ }
441
+
442
+ async handleDisconnected() {
443
+ this.herdsman_started = false;
444
+ this.emit('disconnect');
445
+ }
446
+
447
+ connected() {
448
+ return this.herdsman_started;
449
+ }
450
+
451
+ // Permit join
452
+ async permitJoin(permitTime, devid, failure) {
453
+ let permitDev;
454
+ if (isFunction(devid) && !isFunction(failure)) {
455
+ failure = devid;
456
+ } else {
457
+ permitDev = this.getDevice(devid);
458
+ }
459
+
460
+ if (permitTime) {
461
+ this.info('Zigbee: allowing new devices to join.');
462
+ } else {
463
+ this.info('Zigbee: disabling joining new devices.');
464
+ }
465
+ try
466
+ {
467
+ if (permitTime && !this.herdsman.getPermitJoin()) {
468
+ clearInterval(this._permitJoinInterval);
469
+ this._permitJoinTime = permitTime;
470
+ await this.herdsman.permitJoin(true, permitDev);
471
+ this._permitJoinInterval = setInterval(async () => {
472
+ this.emit('pairing', 'Pairing time left', this._permitJoinTime);
473
+ if (this._permitJoinTime === 0) {
474
+ this.info('Zigbee: stop joining');
475
+ clearInterval(this._permitJoinInterval);
476
+ await this.herdsman.permitJoin(false);
477
+ }
478
+ this._permitJoinTime -= 1;
479
+ }, 1000);
480
+ } else if (this.herdsman.getPermitJoin()) {
481
+ if (permitTime) {
482
+ this.info('Joining already permitted');
483
+ } else {
484
+ clearInterval(this._permitJoinInterval);
485
+ await this.herdsman.permitJoin(false, permitDev);
486
+ }
487
+ }
488
+ } catch (e) {
489
+ this.sendError(e);
490
+ this.error(`Failed to open the network: ${e.stack}`);
491
+ }
492
+ }
493
+
494
+ // Remove device
495
+ async remove(deviceID, force, callback) {
496
+ try {
497
+ const device = await this.herdsman.getDeviceByIeeeAddr(deviceID);
498
+ if (device) {
499
+ try {
500
+ await this.herdsman.adapter.removeDevice(device.networkAddress, device.ieeeAddr);
501
+ } catch (error) {
502
+ this.sendError(error);
503
+ if (error)
504
+ this.debug(`Failed to remove device ${error.stack}`);
505
+ // skip error if force
506
+ if (!force) {
507
+ throw error;
508
+ } else {
509
+ this.debug(`Force remove`);
510
+ }
511
+ }
512
+ try {
513
+ await device.removeFromDatabase();
514
+ } catch (error) {
515
+ this.sendError(error);
516
+ // skip error
517
+ if (error)
518
+ this.debug(`Failed to remove from DB ${error.stack}`);
519
+ }
520
+ this.debug('Remove successful.');
521
+ if (callback) callback();
522
+ this.callExtensionMethod(
523
+ 'onDeviceRemove',
524
+ [device],
525
+ );
526
+ }
527
+ } catch (error) {
528
+ this.sendError(error);
529
+ this.error(`Failed to remove ${error.stack}`);
530
+ if (callback) callback(`Failed to remove ${error.stack}`);
531
+ }
532
+ }
533
+
534
+ // Zigbee events
535
+ async handleDeviceLeave(message) {
536
+ try {
537
+ this.debug('handleDeviceLeave', message);
538
+ const entity = await this.resolveEntity(message.device || message.ieeeAddr);
539
+ const friendlyName = (entity) ? entity.name : message.ieeeAddr;
540
+ this.debug(`Device '${friendlyName}' left the network`);
541
+ this.emit('leave', message.ieeeAddr);
542
+ // Call extensions
543
+ this.callExtensionMethod(
544
+ 'onDeviceLeave',
545
+ [message, entity],
546
+ );
547
+ } catch (error) {
548
+ this.sendError(error);
549
+ this.error(`Failed to handleDeviceLeave ${error.stack}`);
550
+ }
551
+ }
552
+
553
+ async handleDeviceAnnounce(message) {
554
+ this.debug('handleDeviceAnnounce', message);
555
+ const entity = await this.resolveEntity(message.device || message.ieeeAddr);
556
+ const friendlyName = entity.name;
557
+ this.warn(`Device '${friendlyName}' announced itself`);
558
+
559
+ if (entity && entity.mapped) {
560
+ this.callExtensionMethod(
561
+ 'onZigbeeEvent',
562
+ [ {'device': message.device, 'type': 'deviceAnnounce'}, (entity ? entity.mapped : null)]);
563
+ }
564
+
565
+ this.emit('pairing', `Device '${friendlyName}' announced itself`);
566
+ if (!this.herdsman.getPermitJoin()) this.callExtensionMethod('registerDevicePing', [message.device, entity]);
567
+ // if has modelID so can create device
568
+ if (entity.device && entity.device._modelID) {
569
+ entity.device.modelID = entity.device._modelID;
570
+ this.emit('new', entity);
571
+ }
572
+ }
573
+
574
+
575
+ async handleDeviceJoined(message) {
576
+ this.debug('handleDeviceJoined', message);
577
+ //const entity = await this.resolveEntity(message.device || message.ieeeAddr);
578
+ //this.emit('new', entity);
579
+ }
580
+
581
+ async handleDeviceInterview(message) {
582
+ this.debug('handleDeviceInterview', message);
583
+ // safeguard: We do not allow to start an interview if the network is not opened
584
+ if (message.status === 'started' && !this.herdsman.getPermitJoin()) {
585
+ this.warn(`Blocked interview for '${message.ieeeAddr}' because the network is closed`);
586
+ return;
587
+ }
588
+ const entity = await this.resolveEntity(message.device || message.ieeeAddr);
589
+ const friendlyName = entity.name;
590
+ if (message.status === 'successful') {
591
+ this.info(`Successfully interviewed '${friendlyName}', device has succesfully been paired`);
592
+
593
+ if (entity.mapped) {
594
+ const {vendor, description, model} = entity.mapped;
595
+ this.info(
596
+ `Device '${friendlyName}' is supported, identified as: ${vendor} ${description} (${model})`
597
+ );
598
+
599
+ const log = {friendly_name: friendlyName, model, vendor, description, supported: true};
600
+ this.emit('pairing', 'Interview successful', JSON.stringify(log));
601
+ entity.device.modelID = entity.device._modelID;
602
+ this.emit('new', entity);
603
+ // send to extensions again (for configure)
604
+ this.callExtensionMethod(
605
+ 'onZigbeeEvent',
606
+ [message, entity ? entity.mapped : null],
607
+ );
608
+ } else {
609
+ this.debug(
610
+ `Device '${friendlyName}' with Zigbee model '${message.device.modelID}' is NOT supported, ` +
611
+ `please follow https://www.zigbee2mqtt.io/how_tos/how_to_support_new_devices.html`
612
+ );
613
+ this.emit('pairing', 'Interview successful', {friendly_name: friendlyName, supported: false});
614
+ entity.device.modelID = entity.device._modelID;
615
+ this.emit('new', entity);
616
+ }
617
+ } else if (message.status === 'failed') {
618
+ this.error(`Failed to interview '${friendlyName}', device has not succesfully been paired. ${message.error}`);
619
+ this.emit('pairing', 'Interview failed', friendlyName);
620
+ } else {
621
+ if (message.status === 'started') {
622
+ this.info(`Starting interview of '${friendlyName}'`);
623
+ this.emit('pairing', 'Interview started', friendlyName);
624
+ }
625
+ }
626
+ }
627
+
628
+ async handleMessage(data) {
629
+ this.debug(`handleMessage`, data);
630
+ const entity = await this.resolveEntity(data.device || data.ieeeAddr);
631
+ const name = (entity && entity._modelID) ? entity._modelID : data.device.ieeeAddr;
632
+ this.debug(
633
+ `Received Zigbee message from '${name}', type '${data.type}', cluster '${data.cluster}'` +
634
+ `, data '${JSON.stringify(data.data)}' from endpoint ${data.endpoint.ID}` +
635
+ (data.hasOwnProperty('groupID') ? ` with groupID ${data.groupID}` : ``)
636
+ );
637
+ this.event(data.type, entity, data);
638
+
639
+ // Call extensions
640
+ this.callExtensionMethod(
641
+ 'onZigbeeEvent',
642
+ [data, entity ? entity.mapped : null],
643
+ );
644
+ }
645
+
646
+ async getMap(callback) {
647
+ try {
648
+
649
+ const devices = this.herdsman.getDevices(true);
650
+ const lqis = [];
651
+ const routing = [];
652
+
653
+ for (const device of devices.filter((d) => d.type != 'EndDevice')) {
654
+ const resolved = await this.resolveEntity(device);
655
+ let result;
656
+
657
+ try {
658
+ result = await device.lqi();
659
+ } catch (error) {
660
+ this.sendError(error);
661
+ if (error)
662
+ this.debug(`Failed to execute LQI for '${resolved.name}'. ${safeJsonStringify(error.stack)}`);
663
+
664
+ lqis.push({
665
+ parent: 'undefined',
666
+ networkAddress: 0,
667
+ ieeeAddr: device.ieeeAddr,
668
+ lqi: 'undefined',
669
+ relationship: 0,
670
+ depth: 0,
671
+ status: 'offline',
672
+ });
673
+ }
674
+
675
+ if (result !== undefined) {
676
+ for (const dev of result.neighbors) {
677
+ if (dev.ieeeAddr !== '0xffffffffffffffff' && dev !== undefined) {
678
+ lqis.push({
679
+ parent: resolved.device.ieeeAddr,
680
+ networkAddress: dev.networkAddress,
681
+ ieeeAddr: dev.ieeeAddr,
682
+ lqi: dev.linkquality,
683
+ relationship: dev.relationship,
684
+ depth: dev.depth,
685
+ status: (dev.linkquality > 0) ? 'online' : 'offline',
686
+ });
687
+ }
688
+ }
689
+ }
690
+
691
+ this.debug(`LQI succeeded for '${resolved.name}'`);
692
+
693
+ try {
694
+ result = await device.routingTable();
695
+ } catch (error) {
696
+ this.sendError(error);
697
+ if (error) {
698
+ this.debug(`Failed to execute routing table for '${resolved.name}'. ${safeJsonStringify(error.stack)}`);
699
+ }
700
+ }
701
+
702
+ this.debug(`Routing for '${resolved.name}': ${safeJsonStringify(result)}`);
703
+ if (result !== undefined) {
704
+ if (result.table !== undefined) {
705
+ for (const dev of result.table) {
706
+ routing.push({
707
+ source: resolved.device.ieeeAddr,
708
+ destination: dev.destinationAddress,
709
+ nextHop: dev.nextHop,
710
+ status: dev.status,
711
+ });
712
+ }
713
+ }
714
+ }
715
+ this.debug(`Routing table succeeded for '${resolved.name}'`);
716
+
717
+ }
718
+ this.debug(`Get map succeeded ${safeJsonStringify(lqis)}`);
719
+
720
+ if (callback) callback({lqis: lqis, routing: routing});
721
+ } catch (error) {
722
+ this.sendError(error);
723
+ this.debug(`Failed to get map: ${safeJsonStringify(error.stack)}`);
724
+ }
725
+ }
726
+
727
+ async publish(deviceID, cid, cmd, zclData, cfg, ep, type, callback, zclSeqNum) {
728
+ const entity = await this.resolveEntity(deviceID, ep);
729
+ const device = entity.device;
730
+ const endpoint = entity.endpoint;
731
+ if (!device) {
732
+ this.error(
733
+ `Zigbee cannot publish message to device because '${deviceID}' is not known`
734
+ );
735
+ return;
736
+ }
737
+ if (!endpoint) {
738
+ this.error(
739
+ `Zigbee cannot publish message to endpoint because '${ep}' is not known`
740
+ );
741
+ return;
742
+ }
743
+
744
+ this.debug(`Zigbee publish to '${deviceID}', ${cid} - cmd ${cmd} - payload ${JSON.stringify(zclData)} - cfg ${JSON.stringify(cfg)} - endpoint ${ep}`);
745
+
746
+ if (cfg == null) {
747
+ cfg = {};
748
+ }
749
+
750
+ if (type === 'foundation') {
751
+ cfg.disableDefaultResponse = true;
752
+ if (cmd === 'read' && !Array.isArray(zclData)) {
753
+ // needs to be iterateable (string[] | number [])
754
+ zclData[Symbol.iterator] = function* () {
755
+ let k;
756
+ for (k in this) {
757
+ yield k;
758
+ }
759
+ };
760
+ }
761
+ let result;
762
+ if (cmd === 'configReport') {
763
+ result = await endpoint.configureReporting(cid, zclData, cfg);
764
+ } else {
765
+ result = await endpoint[cmd](cid, zclData, cfg);
766
+ }
767
+ if (callback) callback(undefined, result);
768
+ }
769
+ else if(type === 'functionalResp'){
770
+ cfg.disableDefaultResponse = false;
771
+ const result = await endpoint.commandResponse(cid, cmd, zclData, cfg, zclSeqNum);
772
+ if (callback) callback(undefined, result);
773
+ }
774
+ else {
775
+ cfg.disableDefaultResponse = false;
776
+ const result = await endpoint.command(cid, cmd, zclData, cfg);
777
+ if (callback) callback(undefined, result);
778
+ }
779
+ }
780
+
781
+ async addDevToGroup(devId, groupId, epid) {
782
+ try {
783
+ const entity = await this.resolveEntity(devId);
784
+ const group = await this.resolveEntity(groupId);
785
+ this.debug(`addDevFromGroup - entity: ${safeJsonStringify(entity)}`);
786
+ this.debug(`addDevFromGroup - group: ${safeJsonStringify(group)}`);
787
+ if (epid != undefined) {
788
+ for (const ep of entity.endpoints) {
789
+ if (ep.ID == epid && (ep.inputClusters.includes(4) || ep.outputClusters.includes(4)))
790
+ {
791
+ this.debug(`adding endpoint ${ep.ID} (${epid}) to group ${groupId}`)
792
+ await(ep.addToGroup(group.mapped));
793
+ }
794
+ }
795
+
796
+ }
797
+ else
798
+ {
799
+ if (entity.endpoint.inputClusters.includes(4))
800
+ {
801
+ this.debug(`adding endpoint ${entity.endpoint.ID} to group`)
802
+ await entity.endpoint.addToGroup(group.mapped);
803
+ }
804
+ else {
805
+ let added = false;
806
+ for (const ep of entity.endpoints)
807
+ {
808
+ if (ep.inputClusters.includes(4))
809
+ {
810
+ this.debug(`adding endpoint ${ep.ID} to group`)
811
+ await ep.addToGroup(group.mapped);
812
+ added = true;
813
+ break;
814
+ }
815
+ }
816
+ if (!added) throw ('cluster genGroups not supported');
817
+ }
818
+ }
819
+ } catch (error) {
820
+ this.sendError(error);
821
+ this.error(`Exception when trying to Add ${devId} to group ${groupId}`, error);
822
+ return { error:`Failed to add ${devId} to group ${groupId}: ${JSON.stringify(error)}` };
823
+ }
824
+ return {};
825
+ }
826
+
827
+ async removeDevFromGroup(devId, groupId, epid) {
828
+ try {
829
+ const entity = await this.resolveEntity(devId);
830
+ const group = await this.resolveEntity(groupId);
831
+ this.debug(`removeDevFromGroup - entity: ${safeJsonStringify(entity)}`);
832
+ this.debug(`removeDevFromGroup - group: ${safeJsonStringify(group)}`);
833
+ if (epid != undefined) {
834
+ for (const ep of entity.endpoints) {
835
+ if (ep.ID == epid && (ep.inputClusters.includes(4) || ep.outputClusters.includes(4)))
836
+ {
837
+ this.debug(`removing endpoint ${ep.ID} (${epid}) group ${groupId}`)
838
+ await(ep.removeFromGroup(group.mapped))
839
+ }
840
+ }
841
+ } else await entity.endpoint.removeFromGroup(group.mapped);
842
+ } catch (error) {
843
+ this.sendError(error);
844
+ this.error(`Exception when trying remove ${devId} (ep ${epid?epid:entity.endpoint.ID}) from group ${devId}`, error);
845
+ return { error: `Failed to remove dev ${devId} (ep ${epid?epid:entity.endpoint.ID}) from group ${devId}`};
846
+ }
847
+ return {};
848
+ }
849
+
850
+ async removeDevFromAllGroups(devId) {
851
+ try {
852
+ const entity = await this.resolveEntity(devId);
853
+ this.debug(`entity: ${safeJsonStringify(entity)}`);
854
+ for (const ep of entity.endpoints) {
855
+ if (ep.inputClusters.includes(4) || ep.outputClusters.includes(4))
856
+ await ep.removefromAllGroups();
857
+ }
858
+ //await entity.endpoint.removeFromAllGroups();
859
+ } catch (error) {
860
+ this.sendError(error);
861
+ this.error(`Exception when trying remove ${devId} from all groups`, error);
862
+ return { error: `Failed to remove dev ${devId} from all groups: ${error}`};
863
+ }
864
+ return {};
865
+ }
866
+
867
+ bind(ep, cluster, target, callback) {
868
+ const log = ` ${ep.device.ieeeAddr} - ${cluster}`;
869
+ target = !target ? this.getCoordinator() : target;
870
+
871
+ this.debug(`Binding ${log}`);
872
+ ep.bind(cluster, target, (error) => {
873
+ if (error) {
874
+ this.sendError(error);
875
+ this.error(`Failed to bind ${log} - (${error})`);
876
+ } else {
877
+ this.debug(`Successfully bound ${log}`);
878
+ }
879
+
880
+ callback(error);
881
+ });
882
+ }
883
+
884
+ unbind(ep, cluster, target, callback) {
885
+ const log = ` ${ep.device.ieeeAddr} - ${cluster}`;
886
+ target = !target ? this.getCoordinator() : target;
887
+
888
+ this.debug(`Unbinding ${log}`);
889
+ ep.unbind(cluster, target, (error) => {
890
+ if (error) {
891
+ this.error(`Failed to unbind ${log} - (${error})`);
892
+ } else {
893
+ this.debug(`Successfully unbound ${log}`);
894
+ }
895
+
896
+ callback(error);
897
+ });
898
+ }
899
+
900
+ reset(mode, callback) {
901
+ try {
902
+ this.herdsman.reset(mode);
903
+ if (callback) callback();
904
+ } catch (error) {
905
+ this.sendError(error);
906
+ this.error(`Failed to reset ${error.stack}`);
907
+ if (callback) callback(error);
908
+ }
909
+ }
910
+
911
+ async touchlinkReset(permitTime) {
912
+ try {
913
+ await this.herdsman.touchlinkFactoryResetFirst();
914
+ this.permitJoin(permitTime);
915
+ } catch (error) {
916
+ this.sendError(error);
917
+ this.error(`Failed to touchlinkReset ${error.stack}`);
918
+ }
919
+ }
920
+
921
+ async getChannelsEnergy() {
922
+ const payload = {
923
+ dstaddr: 0x0,
924
+ dstaddrmode: 0x02,
925
+ channelmask: 0x07FFF800,
926
+ scanduration: 0x5,
927
+ scancount: 1,
928
+ nwkmanageraddr: 0x0000
929
+ };
930
+ const energyScan = this.herdsman.adapter.znp.waitFor(
931
+ 2, //unpi_1.Constants.Type.AREQ,
932
+ 5, //Subsystem.ZDO,
933
+ 'mgmtNwkUpdateNotify'
934
+ );
935
+ await this.herdsman.adapter.znp.request(
936
+ 0x5, //Subsystem.ZDO
937
+ 'mgmtNwkUpdateReq',
938
+ payload,
939
+ energyScan.ID
940
+ );
941
+ const result = await energyScan.start().promise;
942
+ return result.payload;
943
+ }
944
+ }
945
+
946
+ module.exports = ZigbeeController;