iobroker.zigbee 3.0.5 → 3.1.2

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
@@ -37,6 +37,10 @@ const vm = require('vm');
37
37
  const util = require('util');
38
38
  const dmZigbee = require('./lib/devicemgmt.js');
39
39
  const DeviceDebug = require('./lib/DeviceDebug');
40
+ const { regexpCode } = require('ajv/dist/compile/codegen');
41
+ const dns = require('dns');
42
+ const net = require('net');
43
+ const { getNetAddress } = require('./lib/utils')
40
44
 
41
45
  const createByteArray = function (hexString) {
42
46
  const bytes = [];
@@ -68,6 +72,8 @@ class Zigbee extends utils.Adapter {
68
72
  name: 'zigbee',
69
73
  systemConfig: true,
70
74
  }));
75
+ this.zhversion = zigbeeHerdsmanPackage ? zigbeeHerdsmanPackage.version : 'unknown';
76
+ this.zhcversion = zigbeeHerdsmanConvertersPackage ? zigbeeHerdsmanConvertersPackage.version : 'unknown';
71
77
  this.on('ready', () => this.onReady());
72
78
  this.on('unload', callback => this.onUnload(callback));
73
79
  this.on('message', obj => this.onMessage(obj));
@@ -77,14 +83,13 @@ class Zigbee extends utils.Adapter {
77
83
 
78
84
  this.stController = new StatesController(this);
79
85
  this.stController.on('log', this.onLog.bind(this));
80
- this.stController.on('changed', this.publishFromState.bind(this));
86
+ this.stController.on('acknowledge_state', this.acknowledgeState.bind(this));
81
87
 
82
88
  this.deviceManagement = new dmZigbee(this);
83
89
  this.deviceDebug = new DeviceDebug(this),
84
90
  this.deviceDebug.on('log', this.onLog.bind(this));
85
91
  this.debugActive = true;
86
92
 
87
-
88
93
  this.plugins = [
89
94
  new SerialListPlugin(this),
90
95
  new CommandsPlugin(this),
@@ -202,7 +207,6 @@ class Zigbee extends utils.Adapter {
202
207
  debug.log = this.debugLog.bind(this);
203
208
  debug.enable('zigbee-herdsman*');
204
209
  }
205
-
206
210
  // external converters
207
211
  this.applyExternalConverters();
208
212
  // get devices from exposes
@@ -214,6 +218,9 @@ class Zigbee extends utils.Adapter {
214
218
  this.setState('info.connection', false, true);
215
219
  const zigbeeOptions = this.getZigbeeOptions();
216
220
  this.zbController = new ZigbeeController(this);
221
+
222
+
223
+
217
224
  this.zbController.on('log', this.onLog.bind(this));
218
225
  this.zbController.on('ready', this.onZigbeeAdapterReady.bind(this));
219
226
  this.zbController.on('disconnect', this.onZigbeeAdapterDisconnected.bind(this));
@@ -222,7 +229,12 @@ class Zigbee extends utils.Adapter {
222
229
  this.zbController.on('pairing', this.onPairing.bind(this));
223
230
  this.zbController.on('event', this.stController.onZigbeeEvent.bind(this.stController));
224
231
  this.zbController.on('msg', this.stController.onZigbeeEvent.bind(this.stController));
225
- this.zbController.on('publish', this.publishToState.bind(this));
232
+ this.zbController.on('publish', this.stController.publishToState.bind(this.stController));
233
+ this.stController.on('send_payload', this.zbController.publishPayload.bind(this.zbController));
234
+ this.stController.on('changed', this.zbController.publishFromState.bind(this.zbController));
235
+ this.stController.on('device_query', this.zbController.deviceQuery.bind(this.zbController));
236
+ this.zbController.on('acknowledge_state', this.acknowledgeState.bind(this));
237
+
226
238
  this.zbController.configure(zigbeeOptions);
227
239
  this.zbController.debugActive = this.debugActive;
228
240
  this.stController.debugActive = this.debugActive;
@@ -248,64 +260,57 @@ class Zigbee extends utils.Adapter {
248
260
 
249
261
  sandboxAdd(sandbox, item, module) {
250
262
  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()];
263
+ for(const singleItem of multipleItems) {
264
+ const message = `Adding code from '${module}' as '${singleItem.trim()}' to sandbox `;
265
+ if (!module.match(new RegExp(`/${sandbox.zhclibBase}/`)))
266
+ module = module.replace(/zigbee-herdsman-converters\//, `${sandbox.zhclibBase}/`);
267
+ try {
268
+ const m = require(module);
269
+ sandbox[singleItem.trim()] = m;
270
+ this.log.info(`${message} -- success`);
271
+ }
272
+ catch (error) {
273
+ this.log.warn(`${message} -- failed: ${error && error.message ? error.message : 'no reason given'}`);
255
274
  }
256
- }
257
- else {
258
- this.log.warn(`trying to add "${item} = require(${module})" to sandbox`)
259
- sandbox[item] = require(module);
260
275
  }
261
276
  }
262
277
 
263
278
  SandboxRequire(sandbox, items) {
264
279
  if (!items) return true;
265
- let converterLoaded = true;
280
+ //let converterLoaded = true;
266
281
  for (const item of items) {
267
282
  const modulePath = item[2].replace(/['"]/gm, '');
268
283
 
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
- }
284
+ const ZHCComponentMatch = modulePath.match(/\/(lib|converters|devices)\/(.+)/)
285
+
286
+ if (ZHCComponentMatch) {
287
+ const fullModulePath = '.' + path.sep + path.join('.',sandbox.zhclibBase, ZHCComponentMatch[1], ZHCComponentMatch[2]);
288
+ this.sandboxAdd(sandbox, item[1], fullModulePath);
290
289
  continue;
291
290
  }
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
- }
291
+ this.sandboxAdd(sandbox, item[1], modulePath);
299
292
 
300
293
  }
301
- return converterLoaded;
294
+ return true;
302
295
  }
303
296
 
297
+ checkExternalConverterExists(fn) {
298
+ if (fs.existsSync(fn)) return fn;
299
+ const fnD = this.expandFileName(fn)
300
+ if (fs.existsSync(fnD)) return fnD;
301
+ const fnL = path.join('converters', fn)
302
+ if (fs.existsSync(fnL)) return fnL;
303
+ this.log.error(`unable to load ${fn} - checked ${path.resolve(fn)}, ${path.resolve(fnD)} and ${path.resolve(fnL)}`);
304
+ return false;
305
+ }
304
306
 
305
307
  * getExternalDefinition() {
308
+
306
309
  if (this.config.external === undefined) {
307
310
  return;
308
311
  }
312
+ const zhcPackageFn = require.resolve('zigbee-herdsman-converters/package.json');
313
+ const zhcBaseDir = path.relative('.',path.dirname(zhcPackageFn));
309
314
  const extfiles = this.config.external.split(';');
310
315
  for (const moduleName of extfiles) {
311
316
  if (!moduleName) continue;
@@ -313,48 +318,43 @@ class Zigbee extends utils.Adapter {
313
318
  const sandbox = {
314
319
  require,
315
320
  module: {},
316
- zhclibBase : path.join('zigbee-herdsman-converters',(ZHCP && ZHCP.exports && ZHCP.exports['.'] ? path.dirname(ZHCP.exports['.']) : ''))
321
+ zhclibBase : path.join(zhcBaseDir,(ZHCP && ZHCP.exports && ZHCP.exports['.'] ? path.dirname(ZHCP.exports['.']) : ''))
317
322
  };
318
323
 
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 {
324
+ const mN = this.checkExternalConverterExists(moduleName.trim());
325
+
326
+ if (mN) {
324
327
  const converterCode = fs.readFileSync(mN, {encoding: 'utf8'}).toString();
325
328
  let converterLoaded = true;
326
329
  let modifiedCode = converterCode.replace(/\s+\/\/.+/gm, ''); // remove all lines starting with // (with the exception of the first.)
327
- //fs.writeFileSync(mN+'.tmp1', modifiedCode)
328
330
  modifiedCode = modifiedCode.replace(/^\/\/.+/gm, ''); // remove the fist line if it starts with //
329
- //fs.writeFileSync(mN+'.tmp2', modifiedCode)
330
331
 
331
332
  converterLoaded &= this.SandboxRequire(sandbox,[...modifiedCode.matchAll(/import\s+\*\s+as\s+(\S+)\s+from\s+(\S+);/gm)]);
332
333
  modifiedCode = modifiedCode.replace(/import\s+\*\s+as\s+\S+\s+from\s+\S+;/gm, '')
333
- //fs.writeFileSync(mN+'.tmp3', modifiedCode)
334
+
334
335
  converterLoaded &= this.SandboxRequire(sandbox,[...modifiedCode.matchAll(/import\s+\{(.+)\}\s+from\s+(\S+);/gm)]);
335
336
  modifiedCode = modifiedCode.replace(/import\s+\{.+\}\s+from\s+\S+;/gm, '');
336
337
 
337
338
  converterLoaded &= this.SandboxRequire(sandbox,[...modifiedCode.matchAll(/import\s+(.+)\s+from\s+(\S+);/gm)]);
338
339
  modifiedCode = modifiedCode.replace(/import\s+.+\s+from\s+\S+;/gm, '');
339
- //fs.writeFileSync(mN+'.tmp4', modifiedCode)
340
+
340
341
  converterLoaded &= this.SandboxRequire(sandbox,[...modifiedCode.matchAll(/const\s+\{(.+)\}\s+=\s+require\((.+)\)/gm)]);
341
342
  modifiedCode = modifiedCode.replace(/const\s+\{.+\}\s+=\s+require\(.+\)/gm, '');
342
- //fs.writeFileSync(mN+'.tmp5', modifiedCode)
343
+
343
344
  converterLoaded &= this.SandboxRequire(sandbox,[...modifiedCode.matchAll(/const\s+(\S+)\s+=\s+require\((.+)\)/gm)]);
344
345
  modifiedCode = modifiedCode.replace(/const\s+\S+\s+=\s+require\(.+\)/gm, '');
345
- //mfs.writeFileSync(mN+'.tmp', modifiedCode)
346
346
 
347
347
  for(const component of modifiedCode.matchAll(/const (.+):(.+)=/gm)) {
348
348
  modifiedCode = modifiedCode.replace(component[0], `const ${component[1]} = `);
349
349
  }
350
- modifiedCode = modifiedCode.replace(/export .+;/gm, '');
350
+ modifiedCode = modifiedCode.replace(/export .+ +/gm, 'module.exports = ');
351
351
 
352
352
  if (modifiedCode.indexOf('module.exports') < 0) {
353
353
  converterLoaded = false;
354
354
  this.log.error(`converter does not export any converter array, please add 'module.exports' statement to ${mN}`);
355
355
  }
356
356
 
357
- fs.writeFileSync(mN+'.tmp', modifiedCode)
357
+ //fs.writeFileSync(mN+'.tmp', modifiedCode)
358
358
 
359
359
  if (converterLoaded) {
360
360
  try {
@@ -364,6 +364,10 @@ class Zigbee extends utils.Adapter {
364
364
 
365
365
  if (Array.isArray(converter)) for (const item of converter) {
366
366
  this.log.info('Model ' + item.model + ' defined in external converter ' + mN);
367
+ if (item.hasOwnProperty('icon')) {
368
+ if (!item.icon.toLowerCase().startsWith('http') && !item.useadaptericon)
369
+ item.icon = path.join(path.dirname(mN), item.icon);
370
+ }
367
371
  yield item;
368
372
  }
369
373
  else {
@@ -423,7 +427,7 @@ class Zigbee extends utils.Adapter {
423
427
  //this.logToPairing('herdsman stopped !');
424
428
  this.sendTo(from, command, { status:true }, callback);
425
429
  } catch (error) {
426
- this.sendTo(from, command, { status:false }, callback);
430
+ this.sendTo(from, command, { status:true }, callback);
427
431
  }
428
432
  }
429
433
 
@@ -442,23 +446,16 @@ class Zigbee extends utils.Adapter {
442
446
  if (noReconnect) this.logToPairing(`Starting Adapter ${debugversion}`);
443
447
  this.log.info(`Starting Adapter ${debugversion}`);
444
448
 
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
- });
449
+ const obj = await this.getForeignObjectAsync(`system.adapter.${this.namespace}`);
450
+ if (!obj && obj.common.installedFrom && obj.common.installedFrom.includes('://')) {
451
+ const instFrom = obj.common.installedFrom;
452
+ gitVers = gitVers + instFrom.replace('tarball', 'commit');
453
+ } else {
454
+ gitVers = obj.common.installedFrom;
455
+ }
456
+ if (noReconnect) this.logToPairing(`Installed Version: ${gitVers} (Converters ${zigbeeHerdsmanConvertersPackage.version} Herdsman ${zigbeeHerdsmanPackage.version})`);
457
+ this.log.info(`Installed Version: ${gitVers} (Converters ${zigbeeHerdsmanConvertersPackage.version} Herdsman ${zigbeeHerdsmanPackage.version})`);
458
+ const result = await this.zbController.start(noReconnect);
462
459
  } catch (error) {
463
460
  this.setState('info.connection', false, true);
464
461
  this.logToPairing(`Failed to start Zigbee: ${error && error.message ? error.message : 'no message given'}`)
@@ -493,8 +490,14 @@ class Zigbee extends utils.Adapter {
493
490
  this.tryToReconnect();
494
491
  }
495
492
 
496
- tryToReconnect() {
497
- this.reconnectTimer = setTimeout(() => {
493
+ async tryToReconnect() {
494
+ this.reconnectTimer = setTimeout(async () => {
495
+ const result = await this.testConnection(this.config.port)
496
+ if (result.error) {
497
+ delete this.herdsman;
498
+ this.tryToReconnect();
499
+ return;
500
+ }
498
501
  if (this.config.port.includes('tcp://')) {
499
502
  // Controller connect though Wi-Fi.
500
503
  // Unlikely USB dongle, connection broken may only cause user unplugged the dongle,
@@ -510,6 +513,65 @@ class Zigbee extends utils.Adapter {
510
513
  }, 10 * 1000); // every 10 seconds
511
514
  }
512
515
 
516
+
517
+ async testConnection(address, interactive) {
518
+
519
+ function InteractivePairingMessage(msg, t) {
520
+ if (interactive) t.logToPairing(msg);
521
+ t.log.debug(msg);
522
+ }
523
+
524
+ this.log.debug(`Test connection for ${address}`);
525
+ const strMsg = '';
526
+
527
+ if (address) {
528
+ const netAddress = getNetAddress(address);
529
+ if (netAddress && netAddress.host) {
530
+ const netConnectPromise = new Promise((resolve) => {
531
+ InteractivePairingMessage(`attempting dns lookup for ${netAddress.host}`, this);
532
+ dns.lookup(netAddress.host, (err, ip, _) => {
533
+ if (err) {
534
+ resolve({error:`Unable to resolve name: ${err && err.message ? err.message : 'no message'}`});
535
+ }
536
+ InteractivePairingMessage(`dns lookup for ${address} produced ${ip}`, this );
537
+ const client = new net.Socket();
538
+ InteractivePairingMessage(`attempting to connect to ${ip} port ${netAddress.port ? netAddress.port : 80}`, this);
539
+ client.connect(netAddress.port, ip, () => {
540
+ client.destroy()
541
+ InteractivePairingMessage(`connected successfully to connect to ${ip} port ${netAddress.port ? netAddress.port : 80}`, this);
542
+ resolve({});
543
+ })
544
+ client.on('error', (error) => {
545
+ resolve({error:`unable to connect to ${ip} port ${netAddress.port ? netAddress.port : 80} : ${error && error.message ? error.message : 'no message given'}`});
546
+ });
547
+ })
548
+ });
549
+ return await netConnectPromise;
550
+ }
551
+ else
552
+ {
553
+ const serialConnectPromise = new Promise((resolve) => {
554
+ try {
555
+ const port =address.trim();
556
+ InteractivePairingMessage(`reading access rights for ${port}`, this);
557
+ fs.access(port, fs.constants.R_OK | fs.constants.W_OK, (error) => {
558
+ if (error) {
559
+ resolve({error:`unable to access ${port} : ${error && error.message ? error.message : 'no message given'}`});
560
+ }
561
+ InteractivePairingMessage(`read and write access available for ${port}`, this);
562
+ resolve({});
563
+ });
564
+ }
565
+ catch (error) {
566
+ resolve({error:`File access error: ${error && error.message ? error.message : 'no message given'}`});
567
+ }
568
+ });
569
+ return await serialConnectPromise;
570
+ }
571
+ }
572
+ return {error: `missing parameter: address`};
573
+ }
574
+
513
575
  async onZigbeeAdapterReady() {
514
576
  this.reconnectTimer && clearTimeout(this.reconnectTimer);
515
577
  this.log.info(`Zigbee started`);
@@ -614,10 +676,6 @@ class Zigbee extends utils.Adapter {
614
676
  });
615
677
  }
616
678
 
617
- publishToState(devId, model, payload) {
618
- this.stController.publishToState(devId, model, payload);
619
- }
620
-
621
679
  acknowledgeState(deviceId, model, stateDesc, value) {
622
680
  if (model === 'group') {
623
681
  const stateId = `${this.namespace}.group_${deviceId}.${stateDesc.id}`;
@@ -628,402 +686,14 @@ class Zigbee extends utils.Adapter {
628
686
  }
629
687
  }
630
688
 
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
689
  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)}`};
690
+ return await this.zbController.publishPayload(payload);
1020
691
  }
1021
692
 
1022
693
 
1023
694
  async newDevice(entity) {
1024
695
 
1025
696
  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
697
 
1028
698
  const dev = entity.device;
1029
699
  const model = (entity.mapped) ? entity.mapped.model : dev.modelID;