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/README.md +21 -0
- package/admin/admin.js +153 -86
- package/admin/i18n/de/translations.json +16 -16
- package/admin/index_m.html +59 -90
- package/admin/tab_m.html +7 -5
- package/docs/de/readme.md +1 -1
- package/docs/en/readme.md +4 -2
- package/io-package.json +45 -41
- package/lib/binding.js +1 -1
- package/lib/commands.js +112 -82
- package/lib/developer.js +1 -1
- package/lib/devices.js +11 -7
- package/lib/exposes.js +2 -0
- package/lib/groups.js +400 -63
- package/lib/localConfig.js +16 -5
- package/lib/states.js +32 -2
- package/lib/statescontroller.js +254 -146
- package/lib/utils.js +7 -5
- package/lib/zbDeviceAvailability.js +78 -21
- package/lib/zbDeviceEvent.js +1 -1
- package/lib/zigbeecontroller.js +485 -56
- package/main.js +139 -469
- package/package.json +5 -5
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('
|
|
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
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
321
|
+
zhclibBase : path.join(zhcBaseDir,(ZHCP && ZHCP.exports && ZHCP.exports['.'] ? path.dirname(ZHCP.exports['.']) : ''))
|
|
317
322
|
};
|
|
318
323
|
|
|
319
|
-
const mN =
|
|
320
|
-
|
|
321
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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.
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
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
|
-
|
|
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;
|