iobroker.zigbee 2.0.0 → 2.0.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.
@@ -6,8 +6,15 @@ const getAdId = require('./utils').getAdId;
6
6
  const getZbId = require('./utils').getZbId;
7
7
  const fs = require('fs');
8
8
  const axios = require('axios');
9
-
10
- let savedDeviceNamesDB = {};
9
+ const localConfig = require('./localConfig');
10
+ //const { deviceAddCustomCluster } = require('zigbee-herdsman-converters/lib/modernExtend');
11
+ //const { setDefaultAutoSelectFamilyAttemptTimeout } = require('net');
12
+ //const { runInThisContext } = require('vm');
13
+ //const { time } = require('console');
14
+ const { exec } = require('child_process');
15
+ const { tmpdir } = require('os');
16
+ const path = require('path');
17
+ const { numberWithinRange } = require('zigbee-herdsman-converters/lib/utils');
11
18
 
12
19
 
13
20
  class StatesController extends EventEmitter {
@@ -17,18 +24,14 @@ class StatesController extends EventEmitter {
17
24
  this.adapter.on('stateChange', this.onStateChange.bind(this));
18
25
  this.query_device_block = [];
19
26
  this.debugDevices = undefined;
20
- const fn = adapter.expandFileName('dev_names.json');
21
- this.dev_names_fn = fn.replace('.', '_');
22
27
  this.retTimeoutHandle = null;
23
- fs.readFile(this.dev_names_fn, (err, data) => {
24
- if (!err) {
25
- try {
26
- savedDeviceNamesDB = JSON.parse(data);
27
- } catch {
28
- savedDeviceNamesDB = {};
29
- }
30
- }
31
- });
28
+ this.localConfig = new localConfig(adapter);
29
+ this.compositeData = {};
30
+ this.cleanupRequired = false;
31
+ this.timeoutHandleUpload = null;
32
+ this.ImagesToDownload = [];
33
+ this.stashedErrors = {};
34
+ this.stashedUnknownModels = [];
32
35
  }
33
36
 
34
37
  info(message, data) {
@@ -51,30 +54,82 @@ class StatesController extends EventEmitter {
51
54
  this.adapter.sendError(error, message);
52
55
  }
53
56
 
54
- retainDeviceNames() {
55
- clearTimeout(this.retTimeoutHandle);
56
- this.retTimeoutHanlde = setTimeout(() => {
57
- fs.writeFile(this.dev_names_fn, JSON.stringify(savedDeviceNamesDB, null, 2), error => {
58
- if (error) {
59
- this.error(`error saving device names: ${(error && error.message ? error.message : 'no error message')} ${(error && error.stack ? error.stack : 'no call stack')}`);
60
- } else {
61
- this.debug('saved device names');
62
- }
63
- });
64
- }, 5000);
57
+ TriggerUpload(delay) {
58
+ if (this.timeoutHandleUpload) return;
59
+ this.warn('triggering upload')
60
+ this.timeoutHandleUpload = this.adapter.setTimeout(() => {
61
+ try {
62
+ this.warn('executing upload')
63
+ exec('iobroker upload zigbee', (error, stdout, stderr) => {
64
+ if (error) this.error('exec error: ' + JSON.stringify(error));
65
+ this.warn('upload done');
66
+ });
67
+ }
68
+ catch (error) {
69
+ this.error('error on upload exec');
70
+ }
71
+ }, delay);
65
72
  }
66
73
 
67
- getDebugDevices() {
68
- this.debugDevices = [];
69
- this.adapter.getState(`${this.adapter.namespace}.info.debugmessages`, (err, state) => {
74
+ getStashedErrors() {
75
+ const rv = [];
76
+ if (Object.keys(this.stashedErrors).length > 0)
77
+ {
78
+ rv.push('<p><b>Stashed Messages</b></p>')
79
+ rv.push(Object.values(this.stashedErrors).join('<br>'));
80
+ }
81
+ if (this.stashedUnknownModels.length > 0) {
82
+ rv.push('<p><b>Devices whithout Model definition</b></p>')
83
+ rv.push()
84
+ }
85
+ return rv;
86
+ }
87
+
88
+ async AddModelFromHerdsman(device, model) {
89
+ // this.warn('addModelFromHerdsman ' + JSON.stringify(model) + ' ' + JSON.stringify(this.localConfig.getOverrideWithKey(model, 'legacy', true)));
90
+ if (this.localConfig.getOverrideWithKey(model, 'legacy', true)) {
91
+ //this.warn('legacy for ' + model);
92
+ await this.addLegacyDevice(model);
93
+ }
94
+ else {
95
+ const pre = statesMapping.devices.length;
96
+ await statesMapping.addExposeToDevices(device, this, model);
97
+ const post = statesMapping.devices.length;
98
+ //this.warn('expose for ' + model + ' '+ pre + '->'+post);
99
+ }
100
+ }
101
+
102
+ async getDebugDevices(callback) {
103
+ if (this.debugDevices === undefined) {
104
+ this.debugDevices = [];
105
+ const state = await this.adapter.getStateAsync(`${this.adapter.namespace}.info.debugmessages`);
70
106
  if (state) {
71
107
  if (typeof state.val === 'string' && state.val.length > 2) {
72
108
  this.debugDevices = state.val.split(';');
73
109
  }
74
110
  this.info(`debug devices set to ${JSON.stringify(this.debugDevices)}`);
111
+ if (callback) callback(this.debugDevices);
75
112
  }
76
- });
113
+ } else {
114
+ // this.info(`debug devices was already set to ${JSON.stringify(this.debugDevices)}`);
115
+ callback(this.debugDevices)
116
+ }
117
+ }
77
118
 
119
+ async toggleDeviceDebug(id) {
120
+ const arr = /zigbee.[0-9].([^.]+)/gm.exec(id);
121
+ if (arr[1] === undefined) {
122
+ this.warn(`unable to extract id from state ${id}`);
123
+ return [];
124
+ }
125
+ const stateKey = arr[1];
126
+ if (this.debugDevices === undefined) this.debugDevices = await this.getDebugDevices()
127
+ const idx = this.debugDevices.indexOf(stateKey);
128
+ if (idx < 0) this.debugDevices.push(stateKey);
129
+ else this.debugDevices.splice(idx, 1);
130
+ await this.adapter.setStateAsync(`${this.adapter.namespace}.info.debugmessages`, this.debugDevices.join(';'), true);
131
+ this.info('debug devices set to ' + JSON.stringify(this.debugDevices));
132
+ return this.debugDevices;
78
133
  }
79
134
 
80
135
  checkDebugDevice(dev) {
@@ -82,9 +137,12 @@ class StatesController extends EventEmitter {
82
137
  if (this.debugDevices === undefined) {
83
138
  this.getDebugDevices();
84
139
  }
85
- for (const addressPart of this.debugDevices) {
86
- if (typeof dev === 'string' && dev.includes(addressPart)) {
87
- return true;
140
+ else
141
+ {
142
+ for (const addressPart of this.debugDevices) {
143
+ if (typeof dev === 'string' && dev.includes(addressPart)) {
144
+ return true;
145
+ }
88
146
  }
89
147
  }
90
148
  return false;
@@ -101,10 +159,10 @@ class StatesController extends EventEmitter {
101
159
  if (id.endsWith('debugmessages')) {
102
160
  if (typeof state.val === 'string' && state.val.length > 2) {
103
161
  this.debugDevices = state.val.split(';');
104
-
105
162
  } else {
106
163
  this.debugDevices = [];
107
164
  }
165
+ this.info('debug devices set to ' + JSON.stringify(this.debugDevices));
108
166
  return;
109
167
  }
110
168
 
@@ -132,7 +190,13 @@ class StatesController extends EventEmitter {
132
190
  return;
133
191
  }
134
192
  if (model === 'group') {
135
- deviceId = parseInt(deviceId.replace('0xgroup_', ''));
193
+ const match = deviceId.match(/group_(\d+)/gm);
194
+ if (match) {
195
+ deviceId = parseInt(match[1]);
196
+ this.publishFromState(deviceId, model, stateKey, state, {});
197
+ return;
198
+ }
199
+
136
200
  }
137
201
  this.collectOptions(id.split('.')[2], model, options =>
138
202
  this.publishFromState(deviceId, model, stateKey, state, options));
@@ -143,7 +207,7 @@ class StatesController extends EventEmitter {
143
207
 
144
208
  async collectOptions(devId, model, callback) {
145
209
  const result = {};
146
- // find model states for options and get it values
210
+ // find model states for options and get it values. No options for groups !!!
147
211
  const devStates = await this.getDevStates('0x' + devId, model);
148
212
  if (devStates == null || devStates == undefined || devStates.states == null || devStates.states == undefined) {
149
213
  callback(result);
@@ -188,7 +252,13 @@ class StatesController extends EventEmitter {
188
252
  } else {
189
253
  stateModel = statesMapping.findModel(model);
190
254
  if (!stateModel) {
191
- this.info(`Device ${deviceId} "${model}" not present in statesMapping - relying on exposes for device definition.`);
255
+ // try to get the model from the exposes
256
+ if (!this.stashedErrors.hasOwnProperty(`${deviceId}.nomodel`))
257
+ {
258
+ this.warn(`Device ${deviceId} "${model}" not found.`);
259
+ this.stashedErrors[`${deviceId}.nomodel`] = `Device ${deviceId} "${model}" not found.`;
260
+ }
261
+
192
262
  states = statesMapping.commonStates;
193
263
  } else {
194
264
  states = stateModel.states;
@@ -207,6 +277,34 @@ class StatesController extends EventEmitter {
207
277
  }
208
278
  }
209
279
 
280
+ async triggerComposite(_deviceId, model, stateDesc, interactive) {
281
+ const deviceId = (_deviceId.replace('0x', ''));
282
+ const idParts = stateDesc.id.split('.').slice(-2);
283
+ const key = `${deviceId}.${idParts[0]}`;
284
+ const handle = this.compositeData[key]
285
+ const factor = (interactive ? 10 : 1);
286
+ if (handle) this.adapter.clearTimeout(handle);
287
+ this.compositeData[key] = this.adapter.setTimeout(async () => {
288
+ delete this.compositeData[key];
289
+ const parts = stateDesc.id.split('.');
290
+ parts.pop();
291
+
292
+ this.adapter.getStatesOf(deviceId, parts.join('.'),async (err,states) => {
293
+ if (!err && states) {
294
+ const components = {};
295
+ for (const stateobj of states) {
296
+ const ckey = stateobj._id.split('.').pop();
297
+ const state = await this.adapter.getState(stateobj._id);
298
+ if (state && state.val != undefined) {
299
+ components[ckey] = state.val;
300
+ }
301
+ }
302
+ this.adapter.setState(`${deviceId}.${stateDesc.compositeState}`, JSON.stringify(components));
303
+ }
304
+ })
305
+ }, (stateDesc.compositeTimeout ? stateDesc.compositeTimeout : 100) * factor);
306
+ }
307
+
210
308
  async publishFromState(deviceId, model, stateKey, state, options) {
211
309
  this.debug(`Change state '${stateKey}' at device ${deviceId} type '${model}'`);
212
310
  const elevated = this.checkDebugDevice(deviceId);
@@ -232,7 +330,7 @@ class StatesController extends EventEmitter {
232
330
  this.error(`ELEVATED OE2: no value for device ${deviceId} type '${model}'`);
233
331
  return;
234
332
  }
235
- let stateList = [{stateDesc: stateDesc, value: value, index: 0, timeout: 0}];
333
+ let stateList = [{stateDesc: stateDesc, value: value, index: 0, timeout: 0, source:state.from}];
236
334
  if (stateModel && stateModel.linkedStates) {
237
335
  stateModel.linkedStates.forEach(linkedFunct => {
238
336
  try {
@@ -264,55 +362,51 @@ class StatesController extends EventEmitter {
264
362
  this.emit('changed', deviceId, model, stateModel, stateList, options);
265
363
  }
266
364
 
267
- renameDevice(id, newName) {
268
- this.storeDeviceName(id, newName);
269
- this.adapter.extendObject(id, {common: {name: newName}});
365
+ async renameDevice(id, newName) {
366
+ const stateId = id.replace(`${this.adapter.namespace}.`, '')
367
+ const obj = await this.adapter.getObjectAsync(id);
368
+ if (newName == null || newName == undefined) newName = '';
369
+ let objName = newName;
370
+ if (newName.length < 1 || newName == obj.common.type) {
371
+ objName = (obj.common.type == 'group' ? stateId.replace('_',' ') : obj.common.type);
372
+ }
373
+ this.localConfig.updateDeviceName(stateId, newName);
374
+ this.debug('rename device: newname ~' + newName + '~ objName ~' + objName + '~')
375
+ this.adapter.extendObject(id, {common: {name: objName}});
270
376
  }
271
377
 
272
- setDeviceActivated(id, active) {
273
- this.adapter.extendObject(id, {common: {deactivated: active}});
378
+ verifyDeviceName(id, model ,name) {
379
+ const savedId = id.replace(`${this.adapter.namespace}.`, '');
380
+ return this.localConfig.NameForId(id, model, name);
274
381
  }
275
382
 
276
- async rebuildRetainDeviceNames() {
277
- savedDeviceNamesDB = {};
278
- const devList = await this.adapter.getAdapterObjectsAsync();
279
383
 
280
- for (const key in devList) {
281
- if (devList[key].type == 'device') {
282
- savedDeviceNamesDB[devList[key]._id.replace(`${this.adapter.namespace}.`, '')] = devList[key].common.name;
283
- }
284
- }
285
- this.retainDeviceNames();
384
+ setDeviceActivated(id, active) {
385
+ this.adapter.extendObject(id, {common: {deactivated: active}});
286
386
  }
287
387
 
288
388
  storeDeviceName(id, name) {
289
- savedDeviceNamesDB[id.replace(`${this.adapter.namespace}.`, '')] = name;
290
- this.retainDeviceNames();
389
+ return this.localConfig.updateDeviceName(id, name);
291
390
  }
292
391
 
293
- verifyDeviceName(id, name) {
294
- const savedId = id.replace(`${this.adapter.namespace}.`, '');
295
- if (!savedDeviceNamesDB.hasOwnProperty(savedId)) {
296
- savedDeviceNamesDB[savedId] = name;
297
- this.retainDeviceNames();
298
- }
299
- return savedDeviceNamesDB[savedId];
300
- }
301
392
 
302
393
  async deleteObj(devId) {
303
394
  const options = { recursive:true };
304
395
  try {
305
- this.adapter.delObject(devId,options), (err) => { }
396
+ this.adapter.delObject(devId,options), (err) => {
397
+ this.adapter.log.info(`Cannot delete Object ${devId}: ${err}`);
398
+ }
306
399
 
307
- } catch (err) {
308
- this.adapter.log.info(`Cannot delete Group ${devId}: ${err}`);
400
+ } catch (error) {
401
+ this.adapter.log.info(`Cannot delete Object ${devId}: ${error && error.message ? error.message : 'without error message'}`);
309
402
  }
310
403
  }
311
404
 
312
- async deleteOrphanedDeviceStates(ieeeAddr, model, force, callback) {
405
+ async deleteOrphanedDeviceStates(ieeeAddr, model, force, callback, markOnly) {
313
406
  const devStates = await this.getDevStates(ieeeAddr, model);
314
407
  const commonStates = statesMapping.commonStates;
315
408
  const devId = ieeeAddr.substr(2);
409
+ const messages = [];
316
410
  this.adapter.getStatesOf(devId, (err, states) => {
317
411
  if (!err && states) {
318
412
  states.forEach((state) => {
@@ -330,17 +424,35 @@ class StatesController extends EventEmitter {
330
424
  if (commonStates.find(statedesc => statename === statedesc.id) === undefined &&
331
425
  devStates.states.find(statedesc => statename === statedesc.id) === undefined
332
426
  ) {
333
- if (state.common.hasOwnProperty('custom') && !force) {
334
- this.info(`keeping disconnected state ${JSON.stringify(statename)} of ${devId} `);
427
+ this.cleanupRequired |= markOnly;
428
+ if (state.common.hasOwnProperty('custom') && !force && !markOnly) {
429
+ messages.push(`keeping disconnected state ${JSON.stringify(statename)} of ${devId} `)
430
+ this.info(`keeping disconnected state ${JSON.stringify(statename)} of ${devId} `);
431
+ this.cleanupRequired == true;
335
432
  } else {
336
- this.info(`deleting disconnected state ${JSON.stringify(statename)} of ${devId} `);
337
- this.adapter.deleteState(devId, null, state._id);
433
+ if (markOnly) {
434
+ this.adapter.extendObjectAsync(devId.concat('.',statename), {common: {color:'#E67E22'}})
435
+
436
+ }
437
+ else
438
+ {
439
+ try {
440
+ this.info(`deleting disconnected state ${JSON.stringify(statename)} of ${devId} `);
441
+ messages.push(`deleting disconnected state ${JSON.stringify(statename)} of ${devId} `);
442
+ this.deleteObj(devId.concat('.',statename));
443
+ }
444
+ catch (error) {
445
+ //messages.push(`ERROR: failed to delete state ${JSON.stringify(statename)} of ${devId} `)
446
+ }
447
+ }
338
448
  }
339
449
  } else {
340
450
  this.debug(`keeping connecte state ${JSON.stringify(statename)} of ${devId} `);
451
+ messages.push(`keeping connecte state ${JSON.stringify(statename)} of ${devId} `);
341
452
  }
342
453
  });
343
454
  }
455
+ if (callback) callback(messages);
344
456
  });
345
457
  }
346
458
 
@@ -349,89 +461,134 @@ class StatesController extends EventEmitter {
349
461
  setTimeout(() => this.updateState(dev_id, name, outValue, common), timeout);
350
462
  }
351
463
 
352
- updateState(devId, name, value, common) {
353
- this.adapter.getObject(devId, (err, obj) => {
354
- if (obj) {
355
- if (!obj.common.deactivated) {
356
- const new_common = {name: name};
357
- const id = devId + '.' + name;
358
- const new_name = obj.common.name;
359
- if (common) {
360
- if (common.name !== undefined) {
361
- new_common.name = common.name;
362
- }
363
- if (common.type !== undefined) {
364
- new_common.type = common.type;
365
- }
366
- if (common.unit !== undefined) {
367
- new_common.unit = common.unit;
368
- }
369
- if (common.states !== undefined) {
370
- new_common.states = common.states;
371
- }
372
- if (common.read !== undefined) {
373
- new_common.read = common.read;
374
- }
375
- if (common.write !== undefined) {
376
- new_common.write = common.write;
377
- }
378
- if (common.role !== undefined) {
379
- new_common.role = common.role;
380
- }
381
- if (common.min !== undefined) {
382
- new_common.min = common.min;
383
- }
384
- if (common.max !== undefined) {
385
- new_common.max = common.max;
386
- }
387
- if (common.icon !== undefined) {
388
- new_common.icon = common.icon;
389
- }
464
+ async updateState(devId, name, value, common) {
465
+ const obj = await this.adapter.getObjectAsync(devId)
466
+ if (obj) {
467
+ if (!obj.common.deactivated) {
468
+ const new_common = {name: name, color:null};
469
+ const stateId = devId + '.' + name;
470
+ const new_name = obj.common.name;
471
+ if (common) {
472
+ if (common.name !== undefined) {
473
+ new_common.name = common.name;
390
474
  }
391
- // check if state exist
392
- this.adapter.getObject(id, (err, stobj) => {
393
- let hasChanges = false;
394
- if (stobj) {
395
- // update state - not change name and role (user can it changed)
396
- if (stobj.common.name) {
397
- delete new_common.name;
398
- } else {
399
- new_common.name = `${new_name} ${new_common.name}`;
400
- }
401
- delete new_common.role;
402
-
403
- // check whether any common property is different
404
- if (stobj.common) {
405
- for (const property in new_common) {
406
- if (stobj.common.hasOwnProperty(property)) {
407
- if (stobj.common[property] === new_common[property]) {
408
- delete new_common[property];
409
- } else {
410
- hasChanges = true;
411
- }
412
- }
475
+ if (common.type !== undefined) {
476
+ new_common.type = common.type;
477
+ }
478
+ if (common.unit !== undefined) {
479
+ new_common.unit = common.unit;
480
+ }
481
+ if (common.states !== undefined) {
482
+ new_common.states = common.states;
483
+ }
484
+ if (common.read !== undefined) {
485
+ new_common.read = common.read;
486
+ }
487
+ if (common.write !== undefined) {
488
+ new_common.write = common.write;
489
+ }
490
+ if (common.role !== undefined) {
491
+ new_common.role = common.role;
492
+ }
493
+ if (common.min !== undefined) {
494
+ new_common.min = common.min;
495
+ }
496
+ if (common.max !== undefined) {
497
+ new_common.max = common.max;
498
+ }
499
+ if (common.icon !== undefined) {
500
+ new_common.icon = common.icon;
501
+ }
502
+ }
503
+ // check if state exist
504
+ const stobj = await this.adapter.getObjectAsync(stateId);
505
+ let hasChanges = false;
506
+ if (stobj) {
507
+ // update state - not change name and role (user can it changed)
508
+ if (stobj.common.name) {
509
+ delete new_common.name;
510
+ } else {
511
+ new_common.name = `${new_name} ${new_common.name}`;
512
+ }
513
+ delete new_common.role;
514
+
515
+ // check whether any common property is different
516
+ if (stobj.common) {
517
+ for (const property in new_common) {
518
+ if (stobj.common.hasOwnProperty(property)) {
519
+ if (stobj.common[property] === new_common[property]) {
520
+ delete new_common[property];
521
+ } else {
522
+ hasChanges = true;
413
523
  }
414
524
  }
415
- } else {
416
- hasChanges = true;
417
525
  }
418
-
419
- // only change object when any common property has changed
420
- if (hasChanges) {
421
- this.adapter.extendObject(id, {type: 'state', common: new_common, native: {}}, () =>
422
- value !== undefined && this.setState_typed(id, value, true, stobj ? stobj.common.type : new_common.type));
423
- } else if (value !== undefined) {
424
- this.setState_typed(id, value, true, stobj.common.type);
526
+ }
527
+ } else {
528
+ const matches = stateId.match((/\./g));
529
+ if (matches && matches.length>1) {
530
+ const channels = stateId.split('.');
531
+ const SubChannels = [channels.shift()];
532
+ channels.pop();
533
+ for (const channel of channels) {
534
+ SubChannels.push(channel);
535
+ const id = SubChannels.join('.');
536
+ await this.adapter.extendObjectAsync(id, {type: 'channel', common: { name:channel}, native:{}})
425
537
  }
538
+ }
539
+ hasChanges = true;
540
+ }
426
541
 
427
- });
428
- } else {
429
- this.debug(`UpdateState: Device is deactivated ${devId} ${JSON.stringify(obj)}`);
542
+ // only change object when any common property has changed
543
+ // first check value
544
+ if (value !== undefined) {
545
+ const type = stobj ? stobj.common.type : new_common.type;
546
+ if (type === 'number') {
547
+ const minval = (stobj ? stobj.common.min: new_common.min) || -Number.MAX_VALUE;
548
+ const maxval = (stobj ? stobj.common.max: new_common.max) || Number.MAX_VALUE;
549
+ let nval = (typeof value == 'number' ? value : parseFloat(value));
550
+ if (isNaN(nval)) {
551
+ if (minval !== undefined && typeof minval === 'number')
552
+ nval = minval;
553
+ else
554
+ nval = 0;
555
+ }
556
+ if (nval < minval) {
557
+ hasChanges = true;
558
+ new_common.color = '#FF0000'
559
+ value = minval
560
+ if (!this.stashedErrors.hasOwnProperty(`${stateId}.min`))
561
+ {
562
+ this.error(`State value for ${stateId} has value "${nval}" less than min "${minval}". - this eror is recorded and not repeated`);
563
+ this.stashedErrors[`${stateId}.min`] = `State value for ${stateId} has value "${nval}." less than min "${minval}"`;
564
+ }
565
+ }
566
+ if (nval > maxval) {
567
+ hasChanges = true;
568
+ hasChanges = true;
569
+ new_common.color = '#FF0000'
570
+ value = maxval;
571
+ if (!this.stashedErrors.hasOwnProperty(`${stateId}.max`))
572
+ {
573
+ this.error(`State value for ${stateId} has value "${nval}" more than max "${maxval}". - this eror is recorded and not repeated`);
574
+ this.stashedErrors[`${stateId}.max`] = `State value for ${stateId} has value "${nval}" more than max "${maxval}".`;
575
+ }
576
+ }
577
+ }
578
+ }
579
+ //
580
+ if (hasChanges) {
581
+ this.adapter.extendObject(stateId, {type: 'state', common: new_common, native: {}}, () =>
582
+ value !== undefined && this.setState_typed(stateId, value, true, stobj ? stobj.common.type : new_common.type));
583
+ } else if (value !== undefined) {
584
+ this.setState_typed(stateId, value, true, stobj.common.type);
430
585
  }
431
586
  } else {
432
- this.debug(`UpdateState: missing device ${devId} ${JSON.stringify(obj)}`);
587
+ this.debug(`UpdateState: Device is deactivated ${devId} ${JSON.stringify(obj)}`);
433
588
  }
434
- });
589
+ } else {
590
+ this.debug(`UpdateState: missing device ${devId} ${JSON.stringify(obj)}`);
591
+ }
435
592
  }
436
593
 
437
594
  setState_typed(id, value, ack, type, callback) {
@@ -476,28 +633,56 @@ class StatesController extends EventEmitter {
476
633
  this.adapter.setState(id, value, ack, callback);
477
634
  }
478
635
 
479
- updateDev(dev_id, dev_name, model, callback) {
480
- const __dev_name = this.verifyDeviceName(dev_id, dev_name);
636
+ async applyLegacyDevices() {
637
+ const legacyModels = await this.localConfig.getLegacyModels();
638
+ const modelarr1 = [];
639
+ //this.warn('devices are' + modelarr1.join(','));
640
+ statesMapping.devices.forEach(item => modelarr1.push(item.models));
641
+ //this.warn('legacy models are ' + JSON.stringify(legacyModels));
642
+ statesMapping.setLegacyDevices(legacyModels);
643
+ const modelarr2 = [];
644
+ statesMapping.devices.forEach(item => modelarr2.push(item.models));
645
+ //this.warn('devices are' + modelarr2.join(','));
646
+ }
647
+
648
+ async addLegacyDevice(model) {
649
+ statesMapping.setLegacyDevices([model]);
650
+ statesMapping.getByModel();
651
+ }
652
+
653
+ async getDefaultGroupIcon(id) {
654
+ const regexResult = id.match(new RegExp(/group_(\d+)/));
655
+ if (!regexResult) return '';
656
+ const groupID = Number(regexResult[1]);
657
+ const group = await this.adapter.zbController.getGroupMembersFromController(groupID)
658
+ if (typeof group == 'object')
659
+ return `img/group_${Math.max(Math.min(group.length, 7), 0)}.png`
660
+ else
661
+ return 'img/group_x.png'
662
+ }
663
+
664
+ async updateDev(dev_id, dev_name, model, callback) {
665
+
666
+ const __dev_name = this.verifyDeviceName(dev_id, model, (dev_name ? dev_name : model));
667
+ this.debug(`UpdateDev called with ${dev_id}, ${dev_name}, ${model}, ${__dev_name}`);
481
668
  const id = '' + dev_id;
482
669
  const modelDesc = statesMapping.findModel(model);
483
- let icon = (modelDesc && modelDesc.icon) ? modelDesc.icon : 'img/unknown.png';
670
+ const modelIcon = (model == 'group' ? await this.getDefaultGroupIcon(dev_id) : modelDesc && modelDesc.icon ? modelDesc.icon : 'img/unknown.png');
671
+ let icon = this.localConfig.IconForId(dev_id, model, modelIcon);
484
672
 
485
- // download icon if it external and not undef
673
+ // download icon if it external and not undefined
486
674
  if (model === undefined) {
487
675
  this.warn(`download icon ${__dev_name} for undefined Device not available. Check your devices.`);
488
676
  } else {
489
677
  const model_modif = model.replace(/\//g, '-');
490
- const pathToIcon = `${this.adapter.adapterDir}/admin/img/${model_modif}.png`;
678
+ const pathToAdminIcon = `img/${model_modif}.png`;
491
679
 
492
680
  if (icon.startsWith('http')) {
493
681
  try {
494
- if (!fs.existsSync(pathToIcon)) {
495
- this.warn(`download icon from ${icon} saved into ${pathToIcon}`);
496
- this.downloadIcon(icon, pathToIcon);
497
- }
682
+ this.downloadIconToAdmin(icon, pathToAdminIcon)
498
683
  icon = `img/${model_modif}.png`;
499
684
  } catch (e) {
500
- this.warn(`ERROR : icon not found from ${icon} saved into ${pathToIcon}`);
685
+ this.warn(`ERROR : icon not found at ${icon}`);
501
686
  }
502
687
  }
503
688
  }
@@ -509,6 +694,7 @@ class StatesController extends EventEmitter {
509
694
  name: __dev_name,
510
695
  type: model,
511
696
  icon,
697
+ modelIcon: modelIcon,
512
698
  color: null,
513
699
  statusStates: {onlineId: `${this.adapter.namespace}.${dev_id}.available`}
514
700
  },
@@ -517,8 +703,10 @@ class StatesController extends EventEmitter {
517
703
  // update type and icon
518
704
  this.adapter.extendObject(id, {
519
705
  common: {
706
+ name: __dev_name,
520
707
  type: model,
521
708
  icon,
709
+ modelIcon: modelIcon,
522
710
  color: null,
523
711
  statusStates: {onlineId: `${this.adapter.namespace}.${dev_id}.available`}
524
712
  }
@@ -527,26 +715,84 @@ class StatesController extends EventEmitter {
527
715
  }
528
716
 
529
717
  async downloadIcon(url, image_path) {
530
- if (!fs.existsSync(image_path)) {
531
- return new Promise((resolve, reject) => {
532
- axios({
533
- method: 'get',
534
- url: url,
535
- responseType: 'stream' // Dies ist wichtig, um den Stream direkt zu erhalten
536
- }).then(response => {
537
- const writer = fs.createWriteStream(image_path);
538
- response.data.pipe(writer);
539
- writer.on('finish', resolve);
540
- writer.on('error', reject);
541
- }).catch(err => {
542
- // reject(err);
543
- this.warn(`ERROR : icon path not found ${image_path}`);
718
+ try {
719
+ if (!fs.existsSync(image_path)) {
720
+ this.ImagesToDownload.push(url);
721
+ return new Promise((resolve, reject) => {
722
+ this.info(`downloading ${url} to ${image_path}`);
723
+ axios({
724
+ method: 'get',
725
+ url: url,
726
+ responseType: 'stream' // Dies ist wichtig, um den Stream direkt zu erhalten
727
+ }).then(response => {
728
+ const writer = fs.createWriteStream(image_path);
729
+ response.data.pipe(writer);
730
+ writer.on('finish', resolve);
731
+ writer.on('error', reject);
732
+ }).catch(err => {
733
+ // reject(err);
734
+ this.warn(`ERROR : icon path not found ${image_path}`);
735
+ }).finally(() => {
736
+ const idx = this.ImagesToDownload.indexOf(url);
737
+ if (idx > -1) {
738
+ this.ImagesToDownload.splice(idx, 1);
739
+ }
740
+
741
+ });
544
742
  });
545
- });
743
+ }
744
+ else {
745
+ this.info(`not downloading ${image_path} - file exists`)
746
+ }
747
+ }
748
+ catch (error) {
749
+ this.error('downloadIcon ', error);
546
750
  }
547
751
  }
548
752
 
753
+ async downloadIconToAdmin(url, target) {
754
+ const namespace = `${this.adapter.name}.admin`;
755
+ this.adapter.fileExists(namespace, target, async (err,result) => {
756
+ if (result) return;
757
+ const src = `${tmpdir()}/${path.basename(target)}`;
758
+ const msg = `downloading ${url} to ${src}`;
759
+ if (this.ImagesToDownload.indexOf(url) ==-1) {
760
+ await this.downloadIcon(url, src)
761
+ try {
762
+ fs.readFile(src, (err, data) => {
763
+ if (err) {
764
+ this.error('unable to read ' + src + ' : '+ (err && err.message? err.message:' no message given'))
765
+ return;
766
+ }
767
+ if (data) {
768
+ this.adapter.writeFile(namespace, target, data, (err) => {
769
+ if (err) {
770
+ this.error('error writing file ' + target + JSON.stringify(err))
771
+ return;
772
+ }
773
+ this.info(`copied ${src} to ${target}.`)
774
+ fs.rm(src, (err) => {
775
+ if (err) this.warn(`error removing ${src} : ${JSON.stringify(err)}`);
776
+ else this.info(`removed ${src}`)
777
+ });
778
+ })
779
+ }
780
+ })
781
+ }
782
+ catch (error) {
783
+ this.error('fs.readfile error : ', error);
784
+ }
785
+ }
786
+ });
787
+ }
788
+
789
+ CleanupRequired(set) {
790
+ if (typeof set === 'boolean') this.cleanupRequired = set;
791
+ return this.cleanupRequired;
792
+ }
793
+
549
794
  async syncDevStates(dev, model) {
795
+ this.debug('synchronizing device states for ' + dev.ieeeAddr + ' (' + model + ')');
550
796
  const devId = dev.ieeeAddr.substr(2);
551
797
  // devId - iobroker device id
552
798
  const devStates = await this.getDevStates(dev.ieeeAddr, model);
@@ -590,24 +836,44 @@ class StatesController extends EventEmitter {
590
836
  };
591
837
  this.updateState(devId, statedesc.id, undefined, common);
592
838
  }
839
+ this.deleteOrphanedDeviceStates(dev.ieeeAddr, model, false, undefined, true);
840
+ }
841
+
842
+ async getExposes() {
843
+ await this.localConfig.init();
844
+ await this.applyLegacyDevices();
845
+ try {
846
+ statesMapping.fillStatesWithExposes(this);
847
+ }
848
+ catch (error) {
849
+ this.error(`Error applying exposes: ${error && error.message ? error.message : 'no error message'} ${error && error.stack ? error.stack : ''}`);
850
+ }
593
851
  }
594
852
 
595
- async getExcludeExposes(allExcludesObj) {
596
- statesMapping.fillStatesWithExposes(allExcludesObj);
853
+ async elevatedMessage(device, message, isError) {
854
+ if (isError) this.error(message); else this.warn(message);
855
+ // emit data here for debug tab later
856
+
857
+ }
858
+
859
+ async elevatedDebugMessage(id, message, isError) {
860
+ if (isError) this.error(message); else this.warn(message);
861
+ this.emit('debugmessage', {id: id, message:message});
597
862
  }
598
863
 
599
864
  async publishToState(devId, model, payload) {
600
865
  const devStates = await this.getDevStates(`0x${devId}`, model);
601
- let has_debug = false;
602
- if (this.checkDebugDevice(devId))
866
+
867
+ const has_elevated_debug = (this.checkDebugDevice(devId) && !payload.hasOwnProperty('msg_from_zigbee'));
868
+
869
+
870
+ if (has_elevated_debug)
603
871
  {
604
- if (!payload.hasOwnProperty('msg_from_zigbee')) {
605
- this.warn(`ELEVATED I1: message received '${JSON.stringify(payload)}' from device ${devId} type '${model}'`);
606
- has_debug = true;
607
- }
872
+ this.elevatedMessage(devId, `ELEVATED I01: message received '${JSON.stringify(payload)}' from device ${devId} type '${model}'`, false);
608
873
  }
609
874
  if (!devStates) {
610
- if (has_debug) this.error(`ELEVATED IE2: no device states for device ${devId} type '${model}'`)
875
+ if (has_elevated_debug)
876
+ this.elevatedMessage(devId, `ELEVATED IE02: no device states for device ${devId} type '${model}'`, true)
611
877
  return;
612
878
  }
613
879
  // find states for payload
@@ -634,8 +900,8 @@ class StatesController extends EventEmitter {
634
900
 
635
901
  let stateID = statedesc.id;
636
902
 
637
- if (has_debug && statedesc.id !== 'msg_from_zigbee') {
638
- this.warn(`ELEVATED I2: value generated '${JSON.stringify(value)}' from device ${devId} for '${statedesc.name}'`);
903
+ if (has_elevated_debug) {
904
+ this.elevatedMessage(devId, `ELEVATED I02: value generated '${JSON.stringify(value)}' from device ${devId} for '${statedesc.name}'`, false);
639
905
  }
640
906
 
641
907
  const common = {
@@ -662,7 +928,7 @@ class StatesController extends EventEmitter {
662
928
 
663
929
  // if needs to return value to back after timeout
664
930
  if (statedesc.isEvent) {
665
- this.updateStateWithTimeout(devId, statedesc.id, value, common, 300, !value);
931
+ this.updateStateWithTimeout(devId, statedesc.id, value, common, 300, (typeof value == typeof (!value) ? !value : ''));
666
932
  } else {
667
933
  if (statedesc.prepublish) {
668
934
  this.collectOptions(devId, model, options =>
@@ -677,19 +943,20 @@ class StatesController extends EventEmitter {
677
943
  }
678
944
  } catch (e) {
679
945
  this.debug(`No states in device ${devId} : payload ${JSON.stringify(payload)}`);
680
- if (has_debug)
681
- this.error(`ELEVATED IE3: error when enumerating states of ${devId} for payload ${JSON.stringify(payload)}, ${(e ? e.name : 'undefined')} (${(e ? e.message : '')}).`);
946
+ if (has_elevated_debug)
947
+ this.elevatedMessage(devId, `ELEVATED IE03: error when enumerating states of ${devId} for payload ${JSON.stringify(payload)}, ${(e ? e.name : 'undefined')} (${(e ? e.message : '')}).`, true);
682
948
  }
683
- if (!has_published && has_debug) {
684
- this.error(`ELEVATED IE4: No value published for device ${devId}`);
949
+ if (!has_published && has_elevated_debug) {
950
+ this.elevatedMessage(devId, `ELEVATED IE04: No value published for device ${devId}`, true);
685
951
 
686
952
  }
687
953
  }
688
954
  else {
689
- if (has_debug)
690
- this.error(`ELEVATED IE5: No states matching the payload ${JSON.stringify(payload)} for device ${devId}`);
955
+ if (has_elevated_debug)
956
+ this.elevatedMessage(devId, `ELEVATED IE05: No states matching the payload ${JSON.stringify(payload)} for device ${devId}`, true);
691
957
  }
692
958
  }
959
+
693
960
  }
694
961
 
695
962
  module.exports = StatesController;