iobroker.zigbee 3.0.5 → 3.1.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.
package/main.js CHANGED
@@ -13,8 +13,6 @@ try {
13
13
  }
14
14
  const originalLogMethod = debug.log;
15
15
 
16
- const zigbeeHerdsmanConvertersUtils = require('zigbee-herdsman-converters/lib/utils');
17
-
18
16
  const safeJsonStringify = require('./lib/json');
19
17
  const fs = require('fs');
20
18
  const path = require('path');
@@ -37,6 +35,9 @@ const vm = require('vm');
37
35
  const util = require('util');
38
36
  const dmZigbee = require('./lib/devicemgmt.js');
39
37
  const DeviceDebug = require('./lib/DeviceDebug');
38
+ const dns = require('dns');
39
+ const net = require('net');
40
+ const { getNetAddress } = require('./lib/utils')
40
41
 
41
42
  const createByteArray = function (hexString) {
42
43
  const bytes = [];
@@ -68,6 +69,8 @@ class Zigbee extends utils.Adapter {
68
69
  name: 'zigbee',
69
70
  systemConfig: true,
70
71
  }));
72
+ this.zhversion = zigbeeHerdsmanPackage ? zigbeeHerdsmanPackage.version : 'unknown';
73
+ this.zhcversion = zigbeeHerdsmanConvertersPackage ? zigbeeHerdsmanConvertersPackage.version : 'unknown';
71
74
  this.on('ready', () => this.onReady());
72
75
  this.on('unload', callback => this.onUnload(callback));
73
76
  this.on('message', obj => this.onMessage(obj));
@@ -77,14 +80,13 @@ class Zigbee extends utils.Adapter {
77
80
 
78
81
  this.stController = new StatesController(this);
79
82
  this.stController.on('log', this.onLog.bind(this));
80
- this.stController.on('changed', this.publishFromState.bind(this));
83
+ this.stController.on('acknowledge_state', this.acknowledgeState.bind(this));
81
84
 
82
85
  this.deviceManagement = new dmZigbee(this);
83
86
  this.deviceDebug = new DeviceDebug(this),
84
87
  this.deviceDebug.on('log', this.onLog.bind(this));
85
88
  this.debugActive = true;
86
89
 
87
-
88
90
  this.plugins = [
89
91
  new SerialListPlugin(this),
90
92
  new CommandsPlugin(this),
@@ -202,7 +204,6 @@ class Zigbee extends utils.Adapter {
202
204
  debug.log = this.debugLog.bind(this);
203
205
  debug.enable('zigbee-herdsman*');
204
206
  }
205
-
206
207
  // external converters
207
208
  this.applyExternalConverters();
208
209
  // get devices from exposes
@@ -214,6 +215,9 @@ class Zigbee extends utils.Adapter {
214
215
  this.setState('info.connection', false, true);
215
216
  const zigbeeOptions = this.getZigbeeOptions();
216
217
  this.zbController = new ZigbeeController(this);
218
+
219
+
220
+
217
221
  this.zbController.on('log', this.onLog.bind(this));
218
222
  this.zbController.on('ready', this.onZigbeeAdapterReady.bind(this));
219
223
  this.zbController.on('disconnect', this.onZigbeeAdapterDisconnected.bind(this));
@@ -222,7 +226,12 @@ class Zigbee extends utils.Adapter {
222
226
  this.zbController.on('pairing', this.onPairing.bind(this));
223
227
  this.zbController.on('event', this.stController.onZigbeeEvent.bind(this.stController));
224
228
  this.zbController.on('msg', this.stController.onZigbeeEvent.bind(this.stController));
225
- this.zbController.on('publish', this.publishToState.bind(this));
229
+ this.zbController.on('publish', this.stController.publishToState.bind(this.stController));
230
+ this.stController.on('send_payload', this.zbController.publishPayload.bind(this.zbController));
231
+ this.stController.on('changed', this.zbController.publishFromState.bind(this.zbController));
232
+ this.stController.on('device_query', this.zbController.deviceQuery.bind(this.zbController));
233
+ this.zbController.on('acknowledge_state', this.acknowledgeState.bind(this));
234
+
226
235
  this.zbController.configure(zigbeeOptions);
227
236
  this.zbController.debugActive = this.debugActive;
228
237
  this.stController.debugActive = this.debugActive;
@@ -248,64 +257,57 @@ class Zigbee extends utils.Adapter {
248
257
 
249
258
  sandboxAdd(sandbox, item, module) {
250
259
  const multipleItems = item.split(',');
251
- if (multipleItems.length > 1) {
252
- for(const singleItem of multipleItems) {
253
- this.log.warn(`trying to add "${singleItem.trim()} = require(${module})[${singleItem.trim()}]" to sandbox`)
254
- sandbox[singleItem.trim()] = require(module)[singleItem.trim()];
260
+ for(const singleItem of multipleItems) {
261
+ const message = `Adding code from '${module}' as '${singleItem.trim()}' to sandbox `;
262
+ if (!module.match(new RegExp(`/${sandbox.zhclibBase}/`)))
263
+ module = module.replace(/zigbee-herdsman-converters\//, `${sandbox.zhclibBase}/`);
264
+ try {
265
+ const m = require(module);
266
+ sandbox[singleItem.trim()] = m;
267
+ this.log.info(`${message} -- success`);
268
+ }
269
+ catch (error) {
270
+ this.log.warn(`${message} -- failed: ${error && error.message ? error.message : 'no reason given'}`);
255
271
  }
256
- }
257
- else {
258
- this.log.warn(`trying to add "${item} = require(${module})" to sandbox`)
259
- sandbox[item] = require(module);
260
272
  }
261
273
  }
262
274
 
263
275
  SandboxRequire(sandbox, items) {
264
276
  if (!items) return true;
265
- let converterLoaded = true;
277
+ //let converterLoaded = true;
266
278
  for (const item of items) {
267
279
  const modulePath = item[2].replace(/['"]/gm, '');
268
280
 
269
- let zhcm1 = modulePath.match(/^zigbee-herdsman-converters\//);
270
- if (zhcm1) {
271
- try {
272
- const i2 = modulePath.replace(/^zigbee-herdsman-converters\//, `../${sandbox.zhclibBase}/`);
273
- this.sandboxAdd(sandbox, item[1], i2);
274
- }
275
- catch (error) {
276
- this.log.error(`Sandbox error: ${(error && error.message ? error.message : 'no error message given')}`)
277
- }
278
- continue;
279
- }
280
- zhcm1 = modulePath.match(/^..\//);
281
- if (zhcm1) {
282
- const i2 = modulePath.replace(/^..\//, `../${sandbox.zhclibBase}/`);
283
- try {
284
- this.sandboxAdd(sandbox, item[1], i2);
285
- }
286
- catch (error) {
287
- this.log.error(`Sandbox error: ${(error && error.message ? error.message : 'no error message given')}`);
288
- converterLoaded = false;
289
- }
281
+ const ZHCComponentMatch = modulePath.match(/\/(lib|converters|devices)\/(.+)/)
282
+
283
+ if (ZHCComponentMatch) {
284
+ const fullModulePath = '.' + path.sep + path.join('.',sandbox.zhclibBase, ZHCComponentMatch[1], ZHCComponentMatch[2]);
285
+ this.sandboxAdd(sandbox, item[1], fullModulePath);
290
286
  continue;
291
287
  }
292
- try {
293
- this.sandboxAdd(sandbox, item[1], modulePath);
294
- }
295
- catch (error) {
296
- this.log.error(`Sandbox error: ${(error && error.message ? error.message : 'no error message given')}`);
297
- converterLoaded = false;
298
- }
288
+ this.sandboxAdd(sandbox, item[1], modulePath);
299
289
 
300
290
  }
301
- return converterLoaded;
291
+ return true;
302
292
  }
303
293
 
294
+ checkExternalConverterExists(fn) {
295
+ if (fs.existsSync(fn)) return fn;
296
+ const fnD = this.expandFileName(fn)
297
+ if (fs.existsSync(fnD)) return fnD;
298
+ const fnL = path.join('converters', fn)
299
+ if (fs.existsSync(fnL)) return fnL;
300
+ this.log.error(`unable to load ${fn} - checked ${path.resolve(fn)}, ${path.resolve(fnD)} and ${path.resolve(fnL)}`);
301
+ return false;
302
+ }
304
303
 
305
304
  * getExternalDefinition() {
305
+
306
306
  if (this.config.external === undefined) {
307
307
  return;
308
308
  }
309
+ const zhcPackageFn = require.resolve('zigbee-herdsman-converters/package.json');
310
+ const zhcBaseDir = path.relative('.',path.dirname(zhcPackageFn));
309
311
  const extfiles = this.config.external.split(';');
310
312
  for (const moduleName of extfiles) {
311
313
  if (!moduleName) continue;
@@ -313,48 +315,43 @@ class Zigbee extends utils.Adapter {
313
315
  const sandbox = {
314
316
  require,
315
317
  module: {},
316
- zhclibBase : path.join('zigbee-herdsman-converters',(ZHCP && ZHCP.exports && ZHCP.exports['.'] ? path.dirname(ZHCP.exports['.']) : ''))
318
+ zhclibBase : path.join(zhcBaseDir,(ZHCP && ZHCP.exports && ZHCP.exports['.'] ? path.dirname(ZHCP.exports['.']) : ''))
317
319
  };
318
320
 
319
- const mN = (fs.existsSync(moduleName) ? moduleName : this.expandFileName(moduleName));
320
- if (!fs.existsSync(mN)) {
321
- this.log.warn(`External converter not loaded - neither ${moduleName} nor ${mN} exist.`);
322
- }
323
- else {
321
+ const mN = this.checkExternalConverterExists(moduleName.trim());
322
+
323
+ if (mN) {
324
324
  const converterCode = fs.readFileSync(mN, {encoding: 'utf8'}).toString();
325
325
  let converterLoaded = true;
326
326
  let modifiedCode = converterCode.replace(/\s+\/\/.+/gm, ''); // remove all lines starting with // (with the exception of the first.)
327
- //fs.writeFileSync(mN+'.tmp1', modifiedCode)
328
327
  modifiedCode = modifiedCode.replace(/^\/\/.+/gm, ''); // remove the fist line if it starts with //
329
- //fs.writeFileSync(mN+'.tmp2', modifiedCode)
330
328
 
331
329
  converterLoaded &= this.SandboxRequire(sandbox,[...modifiedCode.matchAll(/import\s+\*\s+as\s+(\S+)\s+from\s+(\S+);/gm)]);
332
330
  modifiedCode = modifiedCode.replace(/import\s+\*\s+as\s+\S+\s+from\s+\S+;/gm, '')
333
- //fs.writeFileSync(mN+'.tmp3', modifiedCode)
331
+
334
332
  converterLoaded &= this.SandboxRequire(sandbox,[...modifiedCode.matchAll(/import\s+\{(.+)\}\s+from\s+(\S+);/gm)]);
335
333
  modifiedCode = modifiedCode.replace(/import\s+\{.+\}\s+from\s+\S+;/gm, '');
336
334
 
337
335
  converterLoaded &= this.SandboxRequire(sandbox,[...modifiedCode.matchAll(/import\s+(.+)\s+from\s+(\S+);/gm)]);
338
336
  modifiedCode = modifiedCode.replace(/import\s+.+\s+from\s+\S+;/gm, '');
339
- //fs.writeFileSync(mN+'.tmp4', modifiedCode)
337
+
340
338
  converterLoaded &= this.SandboxRequire(sandbox,[...modifiedCode.matchAll(/const\s+\{(.+)\}\s+=\s+require\((.+)\)/gm)]);
341
339
  modifiedCode = modifiedCode.replace(/const\s+\{.+\}\s+=\s+require\(.+\)/gm, '');
342
- //fs.writeFileSync(mN+'.tmp5', modifiedCode)
340
+
343
341
  converterLoaded &= this.SandboxRequire(sandbox,[...modifiedCode.matchAll(/const\s+(\S+)\s+=\s+require\((.+)\)/gm)]);
344
342
  modifiedCode = modifiedCode.replace(/const\s+\S+\s+=\s+require\(.+\)/gm, '');
345
- //mfs.writeFileSync(mN+'.tmp', modifiedCode)
346
343
 
347
344
  for(const component of modifiedCode.matchAll(/const (.+):(.+)=/gm)) {
348
345
  modifiedCode = modifiedCode.replace(component[0], `const ${component[1]} = `);
349
346
  }
350
- modifiedCode = modifiedCode.replace(/export .+;/gm, '');
347
+ modifiedCode = modifiedCode.replace(/export .+ +/gm, 'module.exports = ');
351
348
 
352
349
  if (modifiedCode.indexOf('module.exports') < 0) {
353
350
  converterLoaded = false;
354
351
  this.log.error(`converter does not export any converter array, please add 'module.exports' statement to ${mN}`);
355
352
  }
356
353
 
357
- fs.writeFileSync(mN+'.tmp', modifiedCode)
354
+ //fs.writeFileSync(mN+'.tmp', modifiedCode)
358
355
 
359
356
  if (converterLoaded) {
360
357
  try {
@@ -364,6 +361,10 @@ class Zigbee extends utils.Adapter {
364
361
 
365
362
  if (Array.isArray(converter)) for (const item of converter) {
366
363
  this.log.info('Model ' + item.model + ' defined in external converter ' + mN);
364
+ if (item.hasOwnProperty('icon')) {
365
+ if (!item.icon.toLowerCase().startsWith('http') && !item.useadaptericon)
366
+ item.icon = path.join(path.dirname(mN), item.icon);
367
+ }
367
368
  yield item;
368
369
  }
369
370
  else {
@@ -412,10 +413,11 @@ class Zigbee extends utils.Adapter {
412
413
  }
413
414
  this.zbController.configure(this.getZigbeeOptions(message.zigbeeOptions));
414
415
  response.status = await this.doConnect(true);
416
+ if (!response.status) response.error = { message: 'Unable to start the Zigbee Network. Please check the previous messages.'}
415
417
  this.sendTo(from, command, response, callback);
416
418
  }
417
419
  catch (error) {
418
- this.sendTo(from, command, { status:false }, callback);
420
+ this.sendTo(from, command, { status:false, error }, callback);
419
421
  }
420
422
  }
421
423
  else try {
@@ -423,7 +425,7 @@ class Zigbee extends utils.Adapter {
423
425
  //this.logToPairing('herdsman stopped !');
424
426
  this.sendTo(from, command, { status:true }, callback);
425
427
  } catch (error) {
426
- this.sendTo(from, command, { status:false }, callback);
428
+ this.sendTo(from, command, { status:true, error }, callback);
427
429
  }
428
430
  }
429
431
 
@@ -442,23 +444,16 @@ class Zigbee extends utils.Adapter {
442
444
  if (noReconnect) this.logToPairing(`Starting Adapter ${debugversion}`);
443
445
  this.log.info(`Starting Adapter ${debugversion}`);
444
446
 
445
- this.getForeignObject(`system.adapter.${this.namespace}`,async (err, obj) => {
446
- try {
447
- if (!err && obj && obj.common.installedFrom && obj.common.installedFrom.includes('://')) {
448
- const instFrom = obj.common.installedFrom;
449
- gitVers = gitVers + instFrom.replace('tarball', 'commit');
450
- } else {
451
- gitVers = obj.common.installedFrom;
452
- }
453
- if (noReconnect) this.logToPairing(`Installed Version: ${gitVers} (Converters ${zigbeeHerdsmanConvertersPackage.version} Herdsman ${zigbeeHerdsmanPackage.version})`);
454
- this.log.info(`Installed Version: ${gitVers} (Converters ${zigbeeHerdsmanConvertersPackage.version} Herdsman ${zigbeeHerdsmanPackage.version})`);
455
- await this.zbController.start(noReconnect);
456
- } catch (error) {
457
- this.logToPairing(error && error.message ? error.message : error);
458
- this.log.error(error && error.message ? error.message : error);
459
- }
460
- return false;
461
- });
447
+ const obj = await this.getForeignObjectAsync(`system.adapter.${this.namespace}`);
448
+ if (!obj && obj.common.installedFrom && obj.common.installedFrom.includes('://')) {
449
+ const instFrom = obj.common.installedFrom;
450
+ gitVers = gitVers + instFrom.replace('tarball', 'commit');
451
+ } else {
452
+ gitVers = obj.common.installedFrom;
453
+ }
454
+ if (noReconnect) this.logToPairing(`Installed Version: ${gitVers} (Converters ${zigbeeHerdsmanConvertersPackage.version} Herdsman ${zigbeeHerdsmanPackage.version})`);
455
+ this.log.info(`Installed Version: ${gitVers} (Converters ${zigbeeHerdsmanConvertersPackage.version} Herdsman ${zigbeeHerdsmanPackage.version})`);
456
+ const result = await this.zbController.start(noReconnect);
462
457
  } catch (error) {
463
458
  this.setState('info.connection', false, true);
464
459
  this.logToPairing(`Failed to start Zigbee: ${error && error.message ? error.message : 'no message given'}`)
@@ -493,8 +488,14 @@ class Zigbee extends utils.Adapter {
493
488
  this.tryToReconnect();
494
489
  }
495
490
 
496
- tryToReconnect() {
497
- this.reconnectTimer = setTimeout(() => {
491
+ async tryToReconnect() {
492
+ this.reconnectTimer = setTimeout(async () => {
493
+ const result = await this.testConnection(this.config.port)
494
+ if (result.error) {
495
+ delete this.herdsman;
496
+ this.tryToReconnect();
497
+ return;
498
+ }
498
499
  if (this.config.port.includes('tcp://')) {
499
500
  // Controller connect though Wi-Fi.
500
501
  // Unlikely USB dongle, connection broken may only cause user unplugged the dongle,
@@ -510,6 +511,65 @@ class Zigbee extends utils.Adapter {
510
511
  }, 10 * 1000); // every 10 seconds
511
512
  }
512
513
 
514
+
515
+ async testConnection(address, interactive) {
516
+
517
+ function InteractivePairingMessage(msg, t) {
518
+ if (interactive) t.logToPairing(msg);
519
+ t.log.debug(msg);
520
+ }
521
+
522
+ this.log.debug(`Test connection for ${address}`);
523
+ const strMsg = '';
524
+
525
+ if (address) {
526
+ const netAddress = getNetAddress(address);
527
+ if (netAddress && netAddress.host) {
528
+ const netConnectPromise = new Promise((resolve) => {
529
+ InteractivePairingMessage(`attempting dns lookup for ${netAddress.host}`, this);
530
+ dns.lookup(netAddress.host, (err, ip, _) => {
531
+ if (err) {
532
+ resolve({error:`Unable to resolve name: ${err && err.message ? err.message : 'no message'}`});
533
+ }
534
+ InteractivePairingMessage(`dns lookup for ${address} produced ${ip}`, this );
535
+ const client = new net.Socket();
536
+ InteractivePairingMessage(`attempting to connect to ${ip} port ${netAddress.port ? netAddress.port : 80}`, this);
537
+ client.connect(netAddress.port, ip, () => {
538
+ client.destroy()
539
+ InteractivePairingMessage(`connected successfully to connect to ${ip} port ${netAddress.port ? netAddress.port : 80}`, this);
540
+ resolve({});
541
+ })
542
+ client.on('error', (error) => {
543
+ resolve({error:`unable to connect to ${ip} port ${netAddress.port ? netAddress.port : 80} : ${error && error.message ? error.message : 'no message given'}`});
544
+ });
545
+ })
546
+ });
547
+ return await netConnectPromise;
548
+ }
549
+ else
550
+ {
551
+ const serialConnectPromise = new Promise((resolve) => {
552
+ try {
553
+ const port =address.trim();
554
+ InteractivePairingMessage(`reading access rights for ${port}`, this);
555
+ fs.access(port, fs.constants.R_OK | fs.constants.W_OK, (error) => {
556
+ if (error) {
557
+ resolve({error:`unable to access ${port} : ${error && error.message ? error.message : 'no message given'}`});
558
+ }
559
+ InteractivePairingMessage(`read and write access available for ${port}`, this);
560
+ resolve({});
561
+ });
562
+ }
563
+ catch (error) {
564
+ resolve({error:`File access error: ${error && error.message ? error.message : 'no message given'}`});
565
+ }
566
+ });
567
+ return await serialConnectPromise;
568
+ }
569
+ }
570
+ return {error: `missing parameter: address`};
571
+ }
572
+
513
573
  async onZigbeeAdapterReady() {
514
574
  this.reconnectTimer && clearTimeout(this.reconnectTimer);
515
575
  this.log.info(`Zigbee started`);
@@ -568,54 +628,43 @@ class Zigbee extends utils.Adapter {
568
628
 
569
629
  await this.setState('info.connection', true, true);
570
630
  this.stController.CleanupRequired(false);
631
+ const devicesFromObjects = (await this.getDevicesAsync()).filter(item => item.native.id.length ==16).map((item) => `0x${item.native.id}`);
571
632
  const devicesFromDB = this.zbController.getClientIterator(false);
572
633
  for (const device of devicesFromDB) {
573
634
  const entity = await this.zbController.resolveEntity(device);
574
635
  if (entity) {
575
636
  const model = entity.mapped ? entity.mapped.model : entity.device.modelID;
637
+ const idx = devicesFromObjects.indexOf(device.ieeeAddr);
638
+ if (idx > -1) devicesFromObjects.splice(idx, 1);
576
639
  this.stController.updateDev(device.ieeeAddr.substr(2), model, model, () =>
577
640
  this.stController.syncDevStates(device, model));
578
641
  }
579
642
  else (this.log.warn('resolveEntity returned no entity'));
580
643
  }
644
+ for (const id of devicesFromObjects) {
645
+ try {
646
+ this.log.warn(`removing object for device ${id} - it is no longer in the zigbee database`);
647
+ await this.delObjectAsync(id.substring(2), { recursive:true })
648
+ }
649
+ catch {
650
+ this.log.warn(`error removing ${id}`)
651
+ }
652
+ }
581
653
  await this.callPluginMethod('start', [this.zbController, this.stController]);
582
654
  }
583
655
 
656
+
584
657
  async checkIfModelUpdate(entity) {
585
658
  const model = entity.mapped ? entity.mapped.model : entity.device.modelID;
586
659
  const device = entity.device;
587
660
  const devId = device.ieeeAddr.substr(2);
588
661
 
589
- return new Promise((resolve) => {
590
- this.getObject(devId, (err, obj) => {
591
- if (obj && obj.common.type !== model) {
592
- // let's change model
593
- this.getStatesOf(devId, (err, states) => {
594
- if (!err && states) {
595
- const chain = [];
596
- states.forEach((state) =>
597
- chain.push(this.deleteStateAsync(devId, null, state._id)));
598
-
599
- Promise.all(chain)
600
- .then(() =>
601
- this.stController.deleteObj(devId, () =>
602
- this.stController.updateDev(devId, model, model, async () => {
603
- await this.stController.syncDevStates(device, model);
604
- resolve();
605
- })));
606
- } else {
607
- resolve();
608
- }
609
- });
610
- } else {
611
- resolve();
612
- }
613
- });
614
- });
615
- }
616
-
617
- publishToState(devId, model, payload) {
618
- this.stController.publishToState(devId, model, payload);
662
+ const obj = await this.getObjectAsync(devId);
663
+ if (obj && obj.common.type !== model) {
664
+ await this.stController.deleteObj(devId);
665
+ await this.stController.updateDev(devId, model, model);
666
+ await this.stController.syncDevStates(device, model);
667
+ }
619
668
  }
620
669
 
621
670
  acknowledgeState(deviceId, model, stateDesc, value) {
@@ -628,402 +677,14 @@ class Zigbee extends utils.Adapter {
628
677
  }
629
678
  }
630
679
 
631
- processSyncStatesList(deviceId, model, syncStateList) {
632
- syncStateList.forEach((syncState) => {
633
- this.acknowledgeState(deviceId, model, syncState.stateDesc, syncState.value);
634
- });
635
- }
636
-
637
- async publishFromState(deviceId, model, stateModel, stateList, options, debugID) {
638
- let isGroup = false;
639
- const has_elevated_debug = this.stController.checkDebugDevice(deviceId)
640
-
641
- if (has_elevated_debug)
642
- {
643
- const stateNames = [];
644
- for (const state of stateList) {
645
- stateNames.push(state.stateDesc.id);
646
- }
647
- const message = `Publishing to ${deviceId} of model ${model} with ${stateNames.join(', ')}`;
648
- this.emit('device_debug', { ID:debugID, data: { ID: deviceId, flag: '03', IO:false }, message: message});
649
- }
650
- else
651
- if (this.debugActive) this.log.debug(`publishFromState : ${deviceId} ${model} ${safeJsonStringify(stateList)}`);
652
- if (model === 'group') {
653
- isGroup = true;
654
- deviceId = parseInt(deviceId);
655
- }
656
- try {
657
- const entity = await this.zbController.resolveEntity(deviceId);
658
- if (this.debugActive) this.log.debug(`entity: ${deviceId} ${model} ${safeJsonStringify(entity)}`);
659
- const mappedModel = entity ? entity.mapped : undefined;
660
-
661
- if (!mappedModel) {
662
- if (this.debugActive) this.log.debug(`No mapped model for ${model}`);
663
- if (has_elevated_debug) {
664
- const message=`No mapped model ${deviceId} (model ${model})`;
665
- this.emit('device_debug', { ID:debugID, data: { error: 'NOMODEL' , IO:false }, message: message});
666
- }
667
- return;
668
- }
669
-
670
- if (!mappedModel.toZigbee)
671
- {
672
- this.log.error(`No toZigbee in mapped model for ${model}`);
673
- return;
674
- }
675
-
676
- stateList.forEach(async changedState => {
677
- const stateDesc = changedState.stateDesc;
678
- const value = changedState.value;
679
-
680
- if (stateDesc.id === 'send_payload') {
681
- try {
682
- const json_value = JSON.parse(value);
683
- const payload = {device: deviceId.replace('0x', ''), payload: json_value, model:model, stateModel:stateModel};
684
- if (has_elevated_debug) this.emit('device_debug', { ID:debugID, data: { flag: '04' ,payload:value ,states:[{id:stateDesc.id, value:json_value, payload:'none'}], IO:false }});
685
-
686
- const result = await this.sendPayload(payload);
687
- if (result.hasOwnProperty('success') && result.success) {
688
- this.acknowledgeState(deviceId, model, stateDesc, value);
689
- }
690
- else {
691
- this.log.error('Error in SendPayload: '+result.error.message);
692
- }
693
- } catch (error) {
694
- const message = `send_payload: ${value} does not parse as JSON Object : ${error.message}`;
695
- if (has_elevated_debug) this.emit('device_debug', { ID:debugID, data: { error: 'EXSEND' ,states:[{id:stateDesc.id, value:value, payload:error.message}], IO:false }, message:message});
696
- else this.log.error(message);
697
- return;
698
- }
699
- return;
700
- }
701
-
702
- if (stateDesc.isOption || stateDesc.compositeState) {
703
- // acknowledge state with given value
704
- if (has_elevated_debug) {
705
- const message = 'changed state: ' + JSON.stringify(changedState);
706
- this.emit('device_debug', { ID:debugID, data: { flag: 'cc', states:[{id:stateDesc.id, value:value, payload:'none (OC State)'}] , IO:false }, message:message});
707
- }
708
- else
709
- if (this.debugActive) this.log.debug('changed composite state: ' + JSON.stringify(changedState));
710
-
711
- this.acknowledgeState(deviceId, model, stateDesc, value);
712
- if (stateDesc.compositeState && stateDesc.compositeTimeout) {
713
- this.stController.triggerComposite(deviceId, model, stateDesc, changedState.source.includes('.admin.'));
714
- }
715
- // on activation of the 'device_query' state trigger hardware query where possible
716
- if (stateDesc.id === 'device_query') {
717
- if (this.query_device_block.indexOf(deviceId) > -1) {
718
- this.log.info(`Device query for '${entity.device.ieeeAddr}' blocked`);
719
- return;
720
- }
721
- if (mappedModel) {
722
- this.query_device_block.push(deviceId);
723
- if (has_elevated_debug) {
724
- const message = `Device query for '${entity.device.ieeeAddr}/${entity.device.endpoints[0].ID}' triggered`;
725
- this.emit('device_debug', { ID:debugID, data: { flag: 'qs' ,states:[{id:stateDesc.id, value:value, payload:'none for device query'}], IO:false }, message:message});
726
- }
727
- else
728
- if (this.debugActive) this.log.debug(`Device query for '${entity.device.ieeeAddr}' started`);
729
- for (const converter of mappedModel.toZigbee) {
730
- if (converter.hasOwnProperty('convertGet')) {
731
- for (const ckey of converter.key) {
732
- try {
733
- await converter.convertGet(entity.device.endpoints[0], ckey, {});
734
- } catch (error) {
735
- if (has_elevated_debug) {
736
- const message = `Failed to read state '${JSON.stringify(ckey)}'of '${entity.device.ieeeAddr}/${entity.device.endpoints[0].ID}' from query with '${error && error.message ? error.message : 'no error message'}`;
737
- this.log.warn(`ELEVATED OE02.1 ${message}`);
738
- this.emit('device_debug', { ID:debugID, data: { error: 'NOTREAD' , IO:false }, message:message });
739
- }
740
- else
741
- this.log.info(`failed to read state ${JSON.stringify(ckey)} of ${entity.device.ieeeAddr}/${entity.device.endpoints[0].ID} after device query`);
742
- }
743
- }
744
- }
745
- }
746
- if (has_elevated_debug) {
747
- const message = `ELEVATED O07: Device query for '${entity.device.ieeeAddr}/${entity.device.endpoints[0].ID}' complete`;
748
- this.emit('device_debug', { ID:debugID, data: { flag: 'qe' , IO:false }, message:message});
749
- }
750
- else
751
- this.log.info(`Device query for '${entity.device.ieeeAddr}' done`);
752
- const idToRemove = deviceId;
753
- setTimeout(() => {
754
- const idx = this.query_device_block.indexOf(idToRemove);
755
- if (idx > -1) {
756
- this.query_device_block.splice(idx);
757
- }
758
- }, 10000);
759
- }
760
- return;
761
- }
762
- return;
763
- }
764
-
765
- let converter = undefined;
766
- let msg_counter = 0;
767
- for (const c of mappedModel.toZigbee) {
768
-
769
- if (!c.hasOwnProperty('convertSet')) continue;
770
- if (this.debugActive) this.log.debug(`Type of toZigbee is '${typeof c}', Contains key ${(c.hasOwnProperty('key')?JSON.stringify(c.key):'false ')}`)
771
- if (!c.hasOwnProperty('key'))
772
- {
773
- if (converter === undefined)
774
- {
775
- converter = c;
776
- if (has_elevated_debug) {
777
- const message = `Setting converter to keyless converter for ${deviceId} of type ${model}`;
778
- this.emit('device_debug', { ID:debugID, data: { flag: `s4.${msg_counter}` , IO:false }, message:message});
779
- }
780
- else
781
- if (this.debugActive) this.log.debug(`Setting converter to keyless converter for ${deviceId} of type ${model}`);
782
- msg_counter++;
783
- }
784
- else
785
- {
786
- if (has_elevated_debug)
787
- {
788
- const message = `ignoring keyless converter for ${deviceId} of type ${model}`;
789
- this.emit('device_debug', { ID:debugID, data: { flag: `i4.${msg_counter}` , IO:false} , message:message});
790
- }
791
- else
792
- if (this.debugActive) this.log.debug(`ignoring keyless converter for ${deviceId} of type ${model}`);
793
- msg_counter++;
794
- }
795
- continue;
796
- }
797
- if (c.key.includes(stateDesc.prop) || c.key.includes(stateDesc.setattr) || c.key.includes(stateDesc.id))
798
- {
799
- const message = `${(converter===undefined?'Setting':'Overriding')}' converter to converter with key(s)'${JSON.stringify(c.key)}}`;
800
- if (has_elevated_debug) {
801
- this.emit('device_debugug', { ID:debugID, data: { flag: `${converter===undefined ? 's' : 'o'}4.${msg_counter}` , IO:false }, message:message});
802
-
803
- }
804
- else
805
- if (this.debugActive) this.log.debug(message);
806
- converter = c;
807
- msg_counter++;
808
- }
809
- }
810
- if (converter === undefined) {
811
- const message = `No converter available for '${model}' with key '${stateDesc.id}' `;
812
- if (has_elevated_debug) {
813
- this.emit('device_debug', { ID:debugID, data: { error: 'NOCONV',states:[{id:stateDesc.id, value:value, payload:'no converter'}] , IO:false }, message:message});
814
- }
815
- else {
816
- this.log.info(message);
817
- }
818
- return;
819
- }
820
-
821
- const preparedValue = (stateDesc.setter) ? stateDesc.setter(value, options) : value;
822
- const preparedOptions = (stateDesc.setterOpt) ? stateDesc.setterOpt(value, options) : {};
823
-
824
- let syncStateList = [];
825
- if (stateModel && stateModel.syncStates) {
826
- stateModel.syncStates.forEach(syncFunct => {
827
- const res = syncFunct(stateDesc, value, options);
828
- if (res) {
829
- syncStateList = syncStateList.concat(res);
830
- }
831
- });
832
- }
833
-
834
- const epName = stateDesc.epname !== undefined ? stateDesc.epname : (stateDesc.prop || stateDesc.id);
835
- const key = stateDesc.setattr || stateDesc.prop || stateDesc.id;
836
- const message = `convert ${key}, ${safeJsonStringify(preparedValue)}, ${safeJsonStringify(preparedOptions)} for device ${deviceId} with Endpoint ${epName}`;
837
- if (has_elevated_debug) {
838
- this.emit('device_debug', { ID:debugID, data: { flag: '04', payload: {key:key, ep: stateDesc.epname, value:preparedValue, options:preparedOptions}, IO:false }, message:message});
839
- }
840
- else
841
- if (this.debugActive) this.log.debug(message);
842
-
843
- let target;
844
- if (model === 'group') {
845
- target = entity.mapped;
846
- } else {
847
- target = await this.zbController.resolveEntity(deviceId, epName);
848
- target = target.endpoint;
849
- }
850
-
851
- if (this.debugActive) this.log.debug(`target: ${safeJsonStringify(target)}`);
852
-
853
- const meta = {
854
- endpoint_name: epName,
855
- options: preparedOptions,
856
- device: entity.device,
857
- mapped: model === 'group' ? [] : mappedModel,
858
- message: {[key]: preparedValue},
859
- logger: this.log,
860
- state: {},
861
- };
862
-
863
- // new toZigbee
864
- if (preparedValue !== undefined && Object.keys(meta.message).filter(p => p.startsWith('state')).length > 0) {
865
- if (typeof preparedValue === 'number') {
866
- meta.message.state = preparedValue > 0 ? 'ON' : 'OFF';
867
- } else {
868
- meta.message.state = preparedValue;
869
- }
870
- }
871
- if (has_elevated_debug) {
872
- this.emit('device_debug', { ID:debugID, data: { states:[{id:stateDesc.id, value:value, payload:preparedValue, ep:stateDesc.epname}] , IO:false }});
873
- }
874
-
875
- if (preparedOptions !== undefined) {
876
- if (preparedOptions.hasOwnProperty('state')) {
877
- meta.state = preparedOptions.state;
878
- }
879
- }
880
-
881
- try {
882
- const result = await converter.convertSet(target, key, preparedValue, meta);
883
- const message = `convert result ${safeJsonStringify(result)} for device ${deviceId}`;
884
- if (has_elevated_debug) {
885
- this.emit('device_debug', { ID:debugID, data: { flag: 'SUCCESS' , IO:false }, message:message});
886
- }
887
- else
888
- if (this.debugActive) this.log.debug(message);
889
- if (result !== undefined) {
890
- if (stateModel && !isGroup && !stateDesc.noack) {
891
- this.acknowledgeState(deviceId, model, stateDesc, value);
892
- }
893
- // process sync state list
894
- this.processSyncStatesList(deviceId, model, syncStateList);
895
- }
896
- else {
897
- if (has_elevated_debug) {
898
- const message = `Convert does not return a result result for ${key} with ${safeJsonStringify(preparedValue)} on device ${deviceId}.`;
899
- this.emit('device_debug', { ID:debugID, data: { flag: '06' , IO:false }, message:message});
900
- }
901
- }
902
- } catch (error) {
903
- if (has_elevated_debug) {
904
- const message = `caught error ${safeJsonStringify(error)} when setting value for device ${deviceId}.`;
905
- this.emit('device_debug', { ID:debugID, data: { error: 'EXSET' , IO:false },message:message});
906
- }
907
- this.filterError(`Error ${error.code} on send command to ${deviceId}.` +
908
- ` Error: ${error.stack}`, `Send command to ${deviceId} failed with`, error);
909
- }
910
- });
911
- } catch (err) {
912
- const message = `No entity for ${deviceId} : ${err && err.message ? err.message : 'no error message'}`;
913
- this.emit('device_debug', { ID:debugID, data: { error: 'EXPUB' , IO:false }, message:message});
914
- }
915
- }
916
-
917
-
918
- extractEP(key, endpoints) {
919
- try {
920
- if (endpoints) for (const ep of Object.keys(endpoints)) {
921
- if (key.endsWith('_'+ep)) return { setattr: key.replace('_'+ep, ''), epname:ep }
922
- }
923
- }
924
- catch {
925
- return {};
926
- }
927
- return {};
928
- }
929
- // This function is introduced to explicitly allow user level scripts to send Commands
930
- // directly to the zigbee device. It utilizes the zigbee-herdsman-converters to generate
931
- // the exact zigbee message to be sent and can be used to set device options which are
932
- // not exposed as states. It serves as a wrapper function for "publishFromState" with
933
- // extended parameter checking
934
- //
935
- // The payload can either be a JSON object or the string representation of a JSON object
936
- // The following keys are supported in the object:
937
- // device: name of the device. For a device zigbee.0.0011223344556677 this would be 0011223344556677
938
- // payload: The data to send to the device as JSON object (key/Value pairs)
939
- // endpoint: optional: the endpoint to send the data to, if supported.
940
- //
941
680
  async sendPayload(payload) {
942
- let payloadObj = {};
943
- if (typeof payload === 'string') {
944
- try {
945
- payloadObj = JSON.parse(payload);
946
- } catch (e) {
947
- this.log.error(`Unable to parse ${safeJsonStringify(payload)}: ${safeJsonStringify(e)}`);
948
- this.sendError(e, `Unable to parse ${safeJsonStringify(payload)}: ${safeJsonStringify(e)}`);
949
- return {
950
- success: false,
951
- error: `Unable to parse ${safeJsonStringify(payload)}: ${safeJsonStringify(e)}`
952
- };
953
- }
954
- } else if (typeof payload === 'object') {
955
- payloadObj = payload;
956
- } else return { success: false, error: 'illegal type of payload: ' + typeof payload};
957
-
958
- if (payloadObj.hasOwnProperty('device') && payloadObj.hasOwnProperty('payload')) {
959
- try {
960
- const isDevice = !payload.device.includes('group_');
961
- const stateList = [];
962
- const devID = isDevice ? `0x${payload.device}` : parseInt(payload.device.replace('group_', ''));
963
-
964
- const entity = await this.zbController.resolveEntity(devID);
965
- if (!entity) {
966
- this.log.error(`Device ${safeJsonStringify(payloadObj.device)} not found`);
967
- this.sendError(`Device ${safeJsonStringify(payloadObj.device)} not found`);
968
- return {success: false, error: `Device ${safeJsonStringify(payloadObj.device)} not found`};
969
- }
970
- const mappedModel = entity.mapped;
971
- if (!mappedModel) {
972
- this.log.error(`No Model for Device ${safeJsonStringify(payloadObj.device)}`);
973
- this.sendError(`No Model for Device ${safeJsonStringify(payloadObj.device)}`);
974
- return {success: false, error: `No Model for Device ${safeJsonStringify(payloadObj.device)}`};
975
- }
976
- if (typeof payloadObj.payload !== 'object') {
977
- this.log.error(`Illegal payload type for ${safeJsonStringify(payloadObj.device)}`);
978
- this.sendError(`Illegal payload type for ${safeJsonStringify(payloadObj.device)}`);
979
- return {success: false, error: `Illegal payload type for ${safeJsonStringify(payloadObj.device)}`};
980
- }
981
- const endpoints = mappedModel && mappedModel.endpoint ? mappedModel.endpoint(entity.device) : null;
982
- for (const key in payloadObj.payload) {
983
- if (payloadObj.payload[key] != undefined) {
984
- const datatype = typeof payloadObj.payload[key];
985
- const epobj = this.extractEP(key, endpoints);
986
- if (payloadObj.endpoint) {
987
- epobj.epname = payloadObj.endpoint;
988
- delete epobj.setattr;
989
- }
990
- stateList.push({
991
- stateDesc: {
992
- id: key,
993
- prop: key,
994
- role: 'state',
995
- type: datatype,
996
- noack:true,
997
- epname: epobj.epname,
998
- setattr: epobj.setattr,
999
- },
1000
- value: payloadObj.payload[key],
1001
- index: 0,
1002
- timeout: 0,
1003
- });
1004
- }
1005
- }
1006
- try {
1007
- await this.publishFromState(`0x${payload.device}`, payload.model, payload.stateModel, stateList, payload.options, Date.now());
1008
- return {success: true};
1009
- } catch (error) {
1010
- this.log.error(`Error ${error.code} on send command to ${payload.device}.` + ` Error: ${error.stack} ` + `Send command to ${payload.device} failed with ` + error);
1011
- this.filterError(`Error ${error.code} on send command to ${payload.device}.` + ` Error: ${error.stack}`, `Send command to ${payload.device} failed with`, error);
1012
- return {success: false, error};
1013
- }
1014
- } catch (e) {
1015
- return {success: false, error: e};
1016
- }
1017
- }
1018
-
1019
- return {success: false, error: `missing parameter device or payload in message ${JSON.stringify(payload)}`};
681
+ return await this.zbController.publishPayload(payload);
1020
682
  }
1021
683
 
1022
684
 
1023
685
  async newDevice(entity) {
1024
686
 
1025
687
  if (this.debugActive) this.log.debug(`New device event: ${safeJsonStringify(entity)}`);
1026
- //this.stController.AddModelFromHerdsman(entity.device, entity.mapped ? entity.mapped.model : entity.device.modelID)
1027
688
 
1028
689
  const dev = entity.device;
1029
690
  const model = (entity.mapped) ? entity.mapped.model : dev.modelID;
@@ -1084,17 +745,19 @@ class Zigbee extends utils.Adapter {
1084
745
  async onUnload(callback) {
1085
746
  try {
1086
747
  this.log.info(`Halting zigbee adapter. Restart delay is at least ${this.ioPack.common.stopTimeout / 1000} seconds.`)
748
+ this.setState('info.connection', false, true);
749
+ const chain = [];
1087
750
  if (this.config.debugHerdsman) {
1088
751
  debug.disable();
1089
752
  debug.log = originalLogMethod;
1090
753
  }
1091
-
1092
- this.log.info('cleaning everything up...');
754
+ this.log.info('cleaning everything up');
1093
755
  await this.callPluginMethod('stop');
1094
- await this.stController.stop();
756
+ if (this.stController) chain.push(this.stController.stop());
1095
757
  if (this.zbController) {
1096
- await this.zbController.stop();
758
+ chain.push(this.zbController.stop());
1097
759
  }
760
+ Promise.all(chain);
1098
761
  this.log.info('cleanup successful');
1099
762
  callback();
1100
763
  } catch (error) {
@@ -1106,8 +769,8 @@ class Zigbee extends utils.Adapter {
1106
769
  }
1107
770
  }
1108
771
 
1109
- getZigbeeOptions(_overrideOptions) {
1110
- const override = (_overrideOptions ? _overrideOptions:{});
772
+ getZigbeeOptions(overrideOptions) {
773
+ const override = (overrideOptions ? overrideOptions:{});
1111
774
  // file path for db
1112
775
  const dbDir = this.expandFileName('');
1113
776
 
@@ -1153,7 +816,7 @@ class Zigbee extends utils.Adapter {
1153
816
  dbPath: 'shepherd.db',
1154
817
  backupPath: 'nvbackup.json',
1155
818
  disableLed: this.config.disableLed,
1156
- disablePing: this.config.disablePing,
819
+ disablePing: (this.config.pingCluster=='off'),
1157
820
  transmitPower: this.config.transmitPower,
1158
821
  disableBackup: this.config.disableBackup,
1159
822
  extPanIdFix: extPanIdFix,