iobroker.zigbee 3.0.5 → 3.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,19 +5,11 @@ const { EventEmitter } = require('events');
5
5
  const statesMapping = require('./devices');
6
6
  const { getAdId, getZbId } = require('./utils');
7
7
  const fs = require('fs');
8
- const axios = require('axios');
9
8
  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
9
  const path = require('path');
17
- const { throwDeprecation } = require('process');
10
+ const axios = require('axios');
18
11
  const zigbeeHerdsmanConvertersUtils = require('zigbee-herdsman-converters/lib/utils');
19
12
 
20
-
21
13
  class StatesController extends EventEmitter {
22
14
  constructor(adapter) {
23
15
  super();
@@ -35,6 +27,7 @@ class StatesController extends EventEmitter {
35
27
  this.stashedUnknownModels = [];
36
28
  this.debugMessages = { nodevice:{ in:[], out: []} };
37
29
  this.debugActive = true;
30
+ this.deviceQueryBlock = [];
38
31
  }
39
32
 
40
33
  info(message, data) {
@@ -81,7 +74,8 @@ class StatesController extends EventEmitter {
81
74
  }
82
75
 
83
76
  async AddModelFromHerdsman(device, model) {
84
- // this.warn('addModelFromHerdsman ' + JSON.stringify(model) + ' ' + JSON.stringify(this.localConfig.getOverrideWithKey(model, 'legacy', true)));
77
+ const namespace = `${this.adapter.name}.admin`;
78
+
85
79
  if (this.localConfig.getOverrideWithTargetAndKey(model, 'legacy', true)) {
86
80
  this.debug('Applying legacy definition for ' + model);
87
81
  await this.addLegacyDevice(model);
@@ -97,52 +91,56 @@ class StatesController extends EventEmitter {
97
91
  const srcIcon = (modelDesc ? modelDesc.icon : '');
98
92
  const model_modif = model.replace(/\//g, '-');
99
93
  const pathToAdminIcon = `img/${model_modif}.png`;
100
-
101
- const namespace = `${this.adapter.name}.admin`;
94
+ // source is a web address
102
95
  if (srcIcon.startsWith('http')) {
103
- this.adapter.fileExists(namespace, srcIcon, async(err, result) => {
104
- if (result) {
105
- this.debug(`icon ${modelDesc.icon} found - no copy needed`);
106
- return;
107
- }
108
- try {
109
- this.downloadIconToAdmin(srcIcon, pathToAdminIcon)
110
- modelDesc.icon = pathToAdminIcon;
111
- } catch (e) {
112
- this.warn(`ERROR : icon not found at ${srcIcon}`);
113
- }
114
- return;
115
- });
96
+ try {
97
+ //if (modelDesc) modelDesc.icon = pathToAdminIcon;
98
+ this.downloadIconToAdmin(srcIcon, pathToAdminIcon)
99
+ } catch (err) {
100
+ this.warn(`ERROR : unable to download ${srcIcon}: ${err && err.message ? err.message : 'no reason given'}`);
101
+ }
116
102
  return;
117
103
  }
104
+ // source is inline basee64
118
105
  const base64Match = srcIcon.match(/data:image\/(.+);base64,/);
119
106
  if (base64Match) {
120
107
  this.warn(`base 64 Icon matched, trying to save it to disk as ${pathToAdminIcon}`);
121
- modelDesc.icon = pathToAdminIcon;
108
+ if (modelDesc) modelDesc.icon = pathToAdminIcon;
122
109
  this.adapter.fileExists(namespace, pathToAdminIcon, async (err,result) => {
123
110
  if (result) {
124
111
  this.warn(`no need to save icon to ${pathToAdminIcon}`);
125
112
  return;
126
113
  }
127
- this.warn(`Saving base64 data to ${pathToAdminIcon}`)
128
- const buffer = new Buffer(srcIcon.replace(base64Match[0],''), 'base64');
129
- this.adapter.writeFile(pathToAdminIcon, buffer);
130
- this.warn('write file complete.');
114
+ const msg = `Saving base64 Data to ${pathToAdminIcon}`
115
+ try {
116
+ const buffer = Buffer.from(srcIcon.replace(base64Match[0],''), 'base64');
117
+ this.adapter.writeFile(namespace, pathToAdminIcon, buffer, (err) => {
118
+ if (err) {
119
+ this.warn(`${msg} -- failed: ${err && err.message ? err.message : 'no reason given'}`);
120
+ return;
121
+ }
122
+ this.info(`${msg} -- success`);
123
+ });
124
+ }
125
+ catch (err) {
126
+ this.warn(`${msg} -- failed: ${err && err.message ? err.message : 'no reason given'}`)
127
+ }
131
128
  });
132
129
  return;
133
130
  }
131
+ // path is absolute
134
132
  if (modelDesc) modelDesc.icon = pathToAdminIcon;
135
133
  this.adapter.fileExists(namespace, pathToAdminIcon, async(err, result) => {
136
134
  if (result) {
137
- this.debug(`icon ${modelDesc.icon} found - no copy needed`);
135
+ this.debug(`icon ${modelDesc ? modelDesc.icon : 'unknown icon'} found - no copy needed`);
138
136
  return;
139
137
  }
140
138
  // try 3 options for source file
141
139
  let src = srcIcon; // as given
142
140
  const locations=[];
143
141
  if (!fs.existsSync(src)) {
144
- locations.push(src);
145
142
  src = path.normalize(this.adapter.expandFileName(src));
143
+ locations.push(src);
146
144
  } // assumed relative to data folder
147
145
  if (!fs.existsSync(src)) {
148
146
  locations.push(src);
@@ -158,36 +156,46 @@ class StatesController extends EventEmitter {
158
156
  }
159
157
  fs.readFile(src, (err, data) => {
160
158
  if (err) {
161
- this.error('unable to read ' + src + ' : '+ (err.message? err.message:' no message given'))
159
+ this.warn(`unable to read ${src}: ${(err.message? err.message:' no message given')}`);
162
160
  return;
163
161
  }
164
162
  if (data) {
165
163
  this.adapter.writeFile(namespace, pathToAdminIcon, data, (err) => {
166
164
  if (err) {
167
- this.error('error writing file ' + path + JSON.stringify(err))
165
+ this.error(`error writing file ${path}: ${err.message ? err.message : 'no reason given'}`);
168
166
  return;
169
167
  }
170
168
  this.info('Updated image file ' + pathToAdminIcon);
171
169
  });
170
+ return;
172
171
  }
172
+ this.error(`fs.readFile failed - neither error nor data is returned!`);
173
173
  });
174
174
  });
175
175
  }
176
176
  }
177
177
  }
178
178
 
179
+ async updateDebugDevices(debugDevices) {
180
+ if (debugDevices != undefined)
181
+ this.debugDevices = debugDevices;
182
+ this.adapter.zbController.callExtensionMethod('setLocalVariable', ['debugDevices', this.debugDevices]);
183
+
184
+ }
185
+
179
186
  async getDebugDevices(callback) {
180
187
  if (this.debugDevices === undefined) {
181
188
  this.debugDevices = [];
182
189
  const state = await this.adapter.getStateAsync(`${this.adapter.namespace}.info.debugmessages`);
183
190
  if (state) {
184
191
  if (typeof state.val === 'string' && state.val.length > 2) {
185
- this.debugDevices = state.val.split(';');
192
+ this.updateDebugDevices(state.val.split(';'));
186
193
  }
187
194
  this.info(`debug devices set to ${JSON.stringify(this.debugDevices)}`);
188
195
  if (callback) callback(this.debugDevices);
189
196
  }
190
197
  } else {
198
+ this.updateDebugDevices();
191
199
  // this.info(`debug devices was already set to ${JSON.stringify(this.debugDevices)}`);
192
200
  callback(this.debugDevices)
193
201
  }
@@ -195,15 +203,20 @@ class StatesController extends EventEmitter {
195
203
 
196
204
  async toggleDeviceDebug(id) {
197
205
  const arr = /zigbee.[0-9].([^.]+)/gm.exec(id);
206
+ if (!arr) {
207
+ this.warn(`unable to toggle debug for device ${id}: there was no matc (${JSON.stringify(arr)}) `);
208
+ return this.debugDevices;
209
+ }
198
210
  if (arr[1] === undefined) {
199
211
  this.warn(`unable to extract id from state ${id}`);
200
- return [];
212
+ return this.debugDevices;
201
213
  }
202
214
  const stateKey = arr[1];
203
215
  if (this.debugDevices === undefined) this.debugDevices = await this.getDebugDevices()
204
216
  const idx = this.debugDevices.indexOf(stateKey);
205
217
  if (idx < 0) this.debugDevices.push(stateKey);
206
218
  else this.debugDevices.splice(idx, 1);
219
+ this.updateDebugDevices()
207
220
  await this.adapter.setStateAsync(`${this.adapter.namespace}.info.debugmessages`, this.debugDevices.join(';'), true);
208
221
  this.info('debug devices set to ' + JSON.stringify(this.debugDevices));
209
222
  return this.debugDevices;
@@ -243,22 +256,22 @@ class StatesController extends EventEmitter {
243
256
  } else {
244
257
  this.debugDevices = [];
245
258
  }
259
+ this.updateDebugDevices();
246
260
  this.info('debug devices set to ' + JSON.stringify(this.debugDevices));
247
261
  return;
248
262
  }
249
263
 
250
264
  const devId = getAdId(this.adapter, id); // iobroker device id
251
- let deviceId = getZbId(id); // zigbee device id
265
+ const deviceId = getZbId(id); // zigbee device id
252
266
 
253
267
  if (this.checkDebugDevice(id)) {
254
268
  const message = `User state change of state ${id} with value ${state.val} (ack: ${state.ack}) from ${state.from}`;
255
269
  this.emit('device_debug', { ID:debugId, data: { ID: deviceId, flag:'01' }, message:message});
256
270
  } else
257
271
  if (this.debugActive) this.debug(`User stateChange ${id} ${JSON.stringify(state)}`);
258
- // const stateKey = id.split('.')[3];
259
272
  const arr = /zigbee.[0-9].[^.]+.(\S+)/gm.exec(id);
260
273
  if (arr[1] === undefined) {
261
- //this.warn(`unable to extract id from state ${id}`);
274
+ this.debug(`unable to extract id from state ${id}`);
262
275
  return;
263
276
  }
264
277
  const stateKey = arr[1];
@@ -272,15 +285,15 @@ class StatesController extends EventEmitter {
272
285
  if (this.debugActive) this.debug('State Change detected on deactivated Device - ignored');
273
286
  return;
274
287
  }
275
- if (model === 'group') {
276
- const match = deviceId.match(/group_(\d+)/gm);
277
- if (match) {
278
- deviceId = parseInt(match[1]);
279
- this.publishFromState(deviceId, model, stateKey, state, {});
288
+
289
+ if (model && model.id === 'device_query') {
290
+ if (this.query_device_block.indexOf(deviceId) > -1 && !state.source.includes('.admin.')) {
291
+ this.info(`Device query for '${deviceId}' blocked - device query timeout has not elapsed yet.`);
280
292
  return;
281
293
  }
282
-
294
+ this.emit('device_query', { deviceId, debugId });
283
295
  }
296
+
284
297
  this.collectOptions(id.split('.')[2], model, true, options =>
285
298
  this.publishFromState(deviceId, model, stateKey, state, options, debugId));
286
299
  }
@@ -295,7 +308,7 @@ class StatesController extends EventEmitter {
295
308
  callback(result);
296
309
  return;
297
310
  }
298
- // find model states for options and get it values. No options for groups !!!
311
+ // find model states for options and get it values.
299
312
  const devStates = await this.getDevStates('0x' + devId, model);
300
313
  if (devStates == null || devStates == undefined || devStates.states == null || devStates.states == undefined) {
301
314
  callback(result);
@@ -372,7 +385,7 @@ class StatesController extends EventEmitter {
372
385
  }
373
386
  }
374
387
 
375
- async triggerComposite(_deviceId, model, stateDesc, interactive) {
388
+ async triggerComposite(_deviceId, stateDesc, interactive) {
376
389
  const deviceId = (_deviceId.replace('0x', ''));
377
390
  const idParts = stateDesc.id.split('.').slice(-2);
378
391
  const key = `${deviceId}.${idParts[0]}`;
@@ -400,20 +413,26 @@ class StatesController extends EventEmitter {
400
413
  }, (stateDesc.compositeTimeout ? stateDesc.compositeTimeout : 100) * factor);
401
414
  }
402
415
 
403
- async publishFromState(deviceId, model, stateKey, state, options, debugId) {
416
+
417
+ handleLinkedFunctResult(lfArr, devId, state) {
418
+ if (this.handleOption(devId, state.stateDesc)) return;
419
+ lfArr.push(state);
420
+ }
421
+
422
+ async publishFromState(deviceId, model, stateKey, state, options, debugID) {
404
423
  if (this.debugActive) this.debug(`Change state '${stateKey}' at device ${deviceId} type '${model}'`);
405
- const elevated = this.checkDebugDevice(deviceId);
424
+ const has_elevated_debug = this.checkDebugDevice(typeof deviceId == 'number' ? `group_${deviceId}` : deviceId);
406
425
 
407
- if (elevated) {
426
+ if (has_elevated_debug) {
408
427
  const message = (`Change state '${stateKey}' at device ${deviceId} type '${model}'`);
409
- this.emit('device_debug', { ID:debugId, data: { ID: deviceId, model: model, flag:'02', IO:false }, message:message});
428
+ this.emit('device_debug', { ID:debugID, data: { ID: deviceId, model: model, flag:'02', IO:false }, message:message});
410
429
  }
411
430
 
412
431
  const devStates = await this.getDevStates(deviceId, model);
413
432
  if (!devStates) {
414
- if (elevated) {
433
+ if (has_elevated_debug) {
415
434
  const message = (`no device states for device ${deviceId} type '${model}'`);
416
- this.emit('device_debug', { ID:debugId, data: { error: 'NOSTATES' , IO:false }, message:message});
435
+ this.emit('device_debug', { ID:debugID, data: { error: 'NOSTATES' , IO:false }, message:message});
417
436
  }
418
437
  return;
419
438
  }
@@ -422,33 +441,80 @@ class StatesController extends EventEmitter {
422
441
  const stateModel = devStates.stateModel;
423
442
  if (!stateDesc) {
424
443
  const message = (`No state available for '${model}' with key '${stateKey}'`);
425
- if (elevated) this.emit('device_debug', { ID:debugId, data: { states:[{id:state.ID, value:'unknown', payload:'unknown'}], error: 'NOSTKEY' , IO:false }, message:message});
444
+ if (has_elevated_debug) this.emit('device_debug', { ID:debugID, data: { states:[{id:state.ID, value:'unknown', payload:'unknown'}], error: 'NOSTKEY' , IO:false }, message:message});
426
445
  return;
427
446
  }
428
447
 
429
448
  const value = state.val;
430
449
  if (value === undefined || value === '') {
431
- if (elevated) {
450
+ if (has_elevated_debug) {
432
451
  const message = (`no value for device ${deviceId} type '${model}'`);
433
- this.emit('device_debug', { ID:debugId, data: { states:[{id:state.ID, value:'--', payload:'error', ep:stateDesc.epname}],error: 'NOVAL' , IO:false }, message:message});
452
+ this.emit('device_debug', { ID:debugID, data: { states:[{id:state.ID, value:'--', payload:'error', ep:stateDesc.epname}],error: 'NOVAL' , IO:false }, message:message});
453
+ }
454
+ return;
455
+ }
456
+
457
+ // send_payload can never be a linked state !;
458
+ if (stateDesc.id === 'send_payload') {
459
+ try {
460
+ const json_value = JSON.parse(value);
461
+ const payload = {device: deviceId.replace('0x', ''), payload: json_value, model:model, stateModel:stateModel};
462
+ 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 }});
463
+
464
+ this.emit('send_payload', payload, debugID, has_elevated_debug);
465
+ } catch (error) {
466
+ const message = `send_payload: ${value} does not parse as JSON Object : ${error.message}`;
467
+ 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});
468
+ else this.error(message);
469
+ return;
470
+ }
471
+ return;
472
+ }
473
+
474
+ if (stateDesc.id === 'device_query') {
475
+ const interactive = (state.from.includes('.admin'));
476
+ if (!interactive && this.deviceQueryBlock.includes(deviceId)) {
477
+ this.warn(`device_query blocked due to excessive triggering - retrigger > 10 seconds after previous trigger has completed.`);
478
+ return;
434
479
  }
480
+ this.deviceQueryBlock.push[deviceId];
481
+ const id = deviceId;
482
+ this.emit('device_query', deviceId, debugID, has_elevated_debug, (devId) =>{
483
+ setTimeout(() => { const idx = this.deviceQueryBlock.indexOf(id);
484
+ if (idx > -1) this.deviceQueryBlock.splice(idx);
485
+ }, 10000)
486
+
487
+ } )
488
+ return;
489
+ }
490
+
491
+ // composite states can never be linked states;
492
+ if (stateDesc.compositeState && stateDesc.compositeTimeout) {
493
+ this.triggerComposite(deviceId, stateDesc, state.from.includes('.admin.'));
435
494
  return;
436
495
  }
437
- let stateList = [{stateDesc: stateDesc, value: value, index: 0, timeout: 0, source:state.from}];
496
+
497
+ const stateList = [{stateDesc: stateDesc, value: value, index: 0, timeout: 0, source:state.from}];
498
+
438
499
  if (stateModel && stateModel.linkedStates) {
439
500
  stateModel.linkedStates.forEach(linkedFunct => {
440
501
  try {
441
502
  if (typeof linkedFunct === 'function') {
442
503
  const res = linkedFunct(stateDesc, value, options, this.adapter.config.disableQueue);
443
504
  if (res) {
444
- stateList = stateList.concat(res);
505
+ if (res.hasOwnProperty('stateDesc')) { // we got a single state back
506
+ if (! res.stateDesc.isOption) stateList.push(res);
507
+ }
508
+ else {
509
+ res.forEach((ls) => { if (!ls.stateDesc.isOption) stateList.push(res)} );
510
+ }
445
511
  }
446
512
  } else {
447
513
  this.warn(`publish from State - LinkedState is not a function ${JSON.stringify(linkedFunct)}`);
448
514
  }
449
515
  } catch (e) {
450
516
  this.sendError(e);
451
- if (elevated) this.emit('device_debug', { ID:debugId, data: { states:[{id:state.ID, value:state.val, payload:'unknown'}], error: 'EXLINK' , IO:false }});
517
+ if (has_elevated_debug) this.emit('device_debug', { ID:debugID, data: { states:[{id:state.ID, value:state.val, payload:'unknown'}], error: 'EXLINK' , IO:false }});
452
518
  this.error('Exception caught in publishfromstate: ' + (e && e.message ? e.message : 'no error message given'));
453
519
  }
454
520
 
@@ -464,7 +530,8 @@ class StatesController extends EventEmitter {
464
530
  readAfterWriteStates = readAfterWriteStates.concat(readAfterWriteStateDesc.id));
465
531
  }
466
532
 
467
- this.emit('changed', deviceId, model, stateModel, stateList, options, debugId);
533
+ if (stateList.length > 0)
534
+ this.emit('changed', deviceId, model, stateModel, stateList, options, debugID, has_elevated_debug );
468
535
  }
469
536
 
470
537
  async renameDevice(id, newName) {
@@ -481,13 +548,12 @@ class StatesController extends EventEmitter {
481
548
  }
482
549
 
483
550
  verifyDeviceName(id, model ,name) {
484
- const savedId = id.replace(`${this.adapter.namespace}.`, '');
485
551
  return this.localConfig.NameForId(id, model, name);
486
552
  }
487
553
 
488
554
 
489
- setDeviceActivated(id, active) {
490
- this.adapter.extendObject(id, {common: {deactivated: active}});
555
+ setDeviceActivated(id, inActive) {
556
+ this.adapter.extendObject(id, {common: {deactivated: inActive, color:inActive ? '#888888' : null, statusStates: inActive ? null : {onlineId:`${id}.available`} }});
491
557
  }
492
558
 
493
559
  storeDeviceName(id, name) {
@@ -503,7 +569,7 @@ class StatesController extends EventEmitter {
503
569
  }
504
570
 
505
571
  } catch (error) {
506
- this.adapter.log.info(`Cannot delete Object ${devId}: ${error && error.message ? error.message : 'without error message'}`);
572
+ this.adapter.log.warn(`Cannot delete Object ${devId}: ${error && error.message ? error.message : 'without error message'}`);
507
573
  }
508
574
  }
509
575
 
@@ -576,35 +642,8 @@ class StatesController extends EventEmitter {
576
642
  const stateId = devId + '.' + name;
577
643
  const new_name = obj.common.name;
578
644
  if (common) {
579
- if (common.name !== undefined) {
580
- new_common.name = common.name;
581
- }
582
- if (common.type !== undefined) {
583
- new_common.type = common.type;
584
- }
585
- if (common.unit !== undefined) {
586
- new_common.unit = common.unit;
587
- }
588
- if (common.states !== undefined) {
589
- new_common.states = common.states;
590
- }
591
- if (common.read !== undefined) {
592
- new_common.read = common.read;
593
- }
594
- if (common.write !== undefined) {
595
- new_common.write = common.write;
596
- }
597
- if (common.role !== undefined) {
598
- new_common.role = common.role;
599
- }
600
- if (common.min !== undefined) {
601
- new_common.min = common.min;
602
- }
603
- if (common.max !== undefined) {
604
- new_common.max = common.max;
605
- }
606
- if (common.icon !== undefined) {
607
- new_common.icon = common.icon;
645
+ for (const key in common) {
646
+ if (common[key] !== undefined) new_common[key] = common[key];
608
647
  }
609
648
  }
610
649
  // check if state exist
@@ -697,7 +736,7 @@ class StatesController extends EventEmitter {
697
736
  if (this.debugActive) this.debug(`UpdateState: Device is deactivated ${devId} ${JSON.stringify(obj)}`);
698
737
  }
699
738
  } else {
700
- if (this.debugActive) this.debug(`UpdateState: missing device ${devId} ${JSON.stringify(obj)}`);
739
+ if (this.debugActive) this.debug(`UpdateState: missing device ${devId}`);
701
740
  }
702
741
  }
703
742
 
@@ -746,13 +785,10 @@ class StatesController extends EventEmitter {
746
785
  async applyLegacyDevices() {
747
786
  const legacyModels = await this.localConfig.getLegacyModels();
748
787
  const modelarr1 = [];
749
- //this.warn('devices are' + modelarr1.join(','));
750
788
  statesMapping.devices.forEach(item => modelarr1.push(item.models));
751
- //this.warn('legacy models are ' + JSON.stringify(legacyModels));
752
789
  statesMapping.setLegacyDevices(legacyModels);
753
790
  const modelarr2 = [];
754
791
  statesMapping.devices.forEach(item => modelarr2.push(item.models));
755
- //this.warn('devices are' + modelarr2.join(','));
756
792
  }
757
793
 
758
794
  async addLegacyDevice(model) {
@@ -760,15 +796,25 @@ class StatesController extends EventEmitter {
760
796
  statesMapping.getByModel();
761
797
  }
762
798
 
763
- async getDefaultGroupIcon(id) {
764
- const regexResult = id.match(new RegExp(/group_(\d+)/));
765
- if (!regexResult) return '';
766
- const groupID = Number(regexResult[1]);
767
- const group = await this.adapter.zbController.getGroupMembersFromController(groupID)
768
- if (typeof group == 'object')
769
- return `img/group_${Math.max(Math.min(group.length, 7), 0)}.png`
770
- else
771
- return 'img/group_x.png'
799
+
800
+ async getDefaultGroupIcon(id, members) {
801
+ let groupID = 0;
802
+ if (typeof id == 'string') {
803
+ const regexResult = id.match(new RegExp(/group_(\d+)/));
804
+ if (!regexResult) return '';
805
+ groupID = Number(regexResult[1]);
806
+ } else if (typeof id == 'number') {
807
+ groupID = id;
808
+ }
809
+ if (groupID <= 0) return;
810
+ if (typeof members != 'number') {
811
+ const group = await this.adapter.zbController.getGroupMembersFromController(groupID)
812
+ if (typeof group == 'object')
813
+ return `img/group_${Math.max(Math.min(group.length, 7), 0)}.png`
814
+ else
815
+ return 'img/group_x.png'
816
+ }
817
+ return `img/group_${Math.max(Math.min(members, 7), 0)}.png`
772
818
  }
773
819
 
774
820
  async updateDev(dev_id, dev_name, model, callback) {
@@ -777,7 +823,9 @@ class StatesController extends EventEmitter {
777
823
  if (this.debugActive) this.debug(`UpdateDev called with ${dev_id}, ${dev_name}, ${model}, ${__dev_name}`);
778
824
  const id = '' + dev_id;
779
825
  const modelDesc = statesMapping.findModel(model);
780
- const modelIcon = (model == 'group' ? await this.getDefaultGroupIcon(dev_id) : modelDesc && modelDesc.icon ? modelDesc.icon : 'img/unknown.png');
826
+ const modelIcon = (model == 'group' ?
827
+ await this.getDefaultGroupIcon(dev_id) :
828
+ modelDesc && modelDesc.icon ? modelDesc.icon : 'img/unknown.png');
781
829
  let icon = this.localConfig.IconForId(dev_id, model, modelIcon);
782
830
 
783
831
  // download icon if it external and not undefined
@@ -787,6 +835,7 @@ class StatesController extends EventEmitter {
787
835
  const model_modif = model.replace(/\//g, '-');
788
836
  const pathToAdminIcon = `img/${model_modif}.png`;
789
837
 
838
+
790
839
  if (icon.startsWith('http')) {
791
840
  try {
792
841
  this.downloadIconToAdmin(icon, pathToAdminIcon)
@@ -797,66 +846,141 @@ class StatesController extends EventEmitter {
797
846
  }
798
847
  }
799
848
 
800
- this.adapter.setObjectNotExists(id, {
801
- type: 'device',
802
- // actually this is an error, so device.common has no attribute type. It must be in native part
803
- common: {
804
- name: __dev_name,
805
- type: model,
806
- icon,
807
- modelIcon: modelIcon,
808
- color: null,
809
- statusStates: {onlineId: `${this.adapter.namespace}.${dev_id}.available`}
810
- },
811
- native: {id: dev_id}
812
- }, () => {
813
- // update type and icon
849
+ const obj = await this.adapter.getObjectAsync(id);
850
+
851
+ const myCommon = {
852
+ name: __dev_name,
853
+ type: model,
854
+ icon,
855
+ modelIcon: modelIcon,
856
+ color: (obj && obj.common && obj.common.deactivated) ? `#888888` : null,
857
+ statusStates: (obj && obj.common && obj.common.deactivated) ? null : {onlineId: `${this.adapter.namespace}.${dev_id}.available`}
858
+ }
859
+ if (obj) {
814
860
  this.adapter.extendObject(id, {
815
- common: {
816
- name: __dev_name,
817
- type: model,
818
- icon,
819
- modelIcon: modelIcon,
820
- color: null,
821
- statusStates: {onlineId: `${this.adapter.namespace}.${dev_id}.available`}
822
- }
861
+ common: myCommon
823
862
  }, callback);
824
- });
863
+ } else {
864
+ this.adapter.setObjectNotExists(id, {
865
+ type: 'device',
866
+ // actually this is an error, so device.common has no attribute type. It must be in native part
867
+ common: myCommon,
868
+ native: {id: dev_id}
869
+ }, callback);
870
+ }
825
871
  }
826
872
 
827
- async downloadIcon(url, image_path) {
873
+ async streamToBufferFetch(readableStream) {
874
+ const reader = readableStream.getReader();
875
+ const chunks = [];
876
+ let done, value;
877
+ try {
878
+ while (true) {
879
+ const result = await reader.read();
880
+ done = result.done;
881
+ value = result.value;
882
+ if (done) break;
883
+ if (value) chunks.push(Buffer.from(value));
884
+ }
885
+ return Buffer.concat(chunks);
886
+ } catch (err) {
887
+ this.error(`error getting buffer from stream: ${err && err.message ? err.message : 'no reason given'}`);
888
+ throw err;
889
+ }
890
+ }
891
+
892
+ async fetchIcon(url, image_path) {
893
+ const namespace = `${this.adapter.name}.admin`;
828
894
  try {
829
- if (!fs.existsSync(image_path)) {
830
- this.ImagesToDownload.push(url);
831
- return new Promise((resolve, reject) => {
832
- this.info(`downloading ${url} to ${image_path}`);
833
- axios({
834
- method: 'get',
835
- url: url,
836
- responseType: 'stream' // Dies ist wichtig, um den Stream direkt zu erhalten
837
- }).then(response => {
838
- const writer = fs.createWriteStream(image_path);
839
- response.data.pipe(writer);
840
- writer.on('finish', resolve);
841
- writer.on('error', reject);
842
- }).catch(err => {
843
- // reject(err);
844
- this.warn(`ERROR : icon path not found ${image_path}`);
845
- }).finally(() => {
895
+ return new Promise((resolve, reject) => {
896
+ fetch(url)
897
+ .then(async response => {
898
+ if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
899
+ const data = await this.streamToBufferFetch(response.body);
900
+ this.adapter.writeFile(namespace, image_path, data, (err) => {
901
+ if (err) {
902
+ this.error(`error writing ${image_path} to admin: ${err.message ? err.message : 'no message given'}`);
903
+ reject(err);
904
+ return;
905
+ }
906
+ this.info(`downloaded ${url} to ${image_path}.`);
907
+ resolve();
908
+ });
909
+ })
910
+ .catch(err => {
911
+ this.warn(`error downloading icon ${err && err.message ? err.message : 'no message given'}`);
912
+ reject(err);
913
+ })
914
+ .finally(() => {
846
915
  const idx = this.ImagesToDownload.indexOf(url);
847
916
  if (idx > -1) {
848
917
  this.ImagesToDownload.splice(idx, 1);
849
918
  }
919
+ });
920
+ });
921
+ }
922
+ catch (error) {
923
+ this.warn(`error fetching ${url} : ${error && error.message ? error.message : 'no reason given'}`)
924
+ }
925
+ }
850
926
 
927
+ async streamToBuffer(readableStream) {
928
+ return new Promise((resolve, reject) => {
929
+ const chunks = [];
930
+ readableStream.on('data', data => {
931
+ if (typeof data === 'string') {
932
+ // Convert string to Buffer assuming UTF-8 encoding
933
+ chunks.push(Buffer.from(data, 'utf-8'));
934
+ } else if (data instanceof Buffer) {
935
+ chunks.push(data);
936
+ } else {
937
+ // Convert other data types to JSON and then to a Buffer
938
+ const jsonData = JSON.stringify(data);
939
+ chunks.push(Buffer.from(jsonData, 'utf-8'));
940
+ }
941
+ });
942
+ readableStream.on('end', () => {
943
+ resolve(Buffer.concat(chunks));
944
+ });
945
+ readableStream.on('error', (err) => {
946
+ this.error(`error getting buffer from stream: ${err && err.message ? err.message : 'no reason given'}`);
947
+ reject;
948
+ });
949
+ });
950
+ }
951
+
952
+ async downloadIcon(url, image_path) {
953
+ try {
954
+ const namespace = `${this.adapter.name}.admin`;
955
+ this.ImagesToDownload.push(url);
956
+ return new Promise((resolve, reject) => {
957
+ this.info(`downloading ${url} to ${image_path}`);
958
+ axios({
959
+ method: 'get',
960
+ url: url,
961
+ responseType: 'stream' // Dies ist wichtig, um den Stream direkt zu erhalten
962
+ }).then(async response => {
963
+ const data = await this.streamToBuffer(response.data);
964
+ this.adapter.writeFile(namespace, image_path, data, (err) => {
965
+ if (err) {
966
+ this.error(`error writing ${image_path} to admin: ${err.message ? err.message : 'no message given'}`);
967
+ reject;
968
+ }
969
+ this.info(`downloaded ${url} to ${image_path}.`)
970
+ resolve;
851
971
  });
972
+ }).catch(err => {
973
+ this.warn(`error downloading icon ${err && err.message ? err.message : 'no message given'}`);
974
+ }).finally(() => {
975
+ const idx = this.ImagesToDownload.indexOf(url);
976
+ if (idx > -1) {
977
+ this.ImagesToDownload.splice(idx, 1);
978
+ }
852
979
  });
853
- }
854
- else {
855
- this.info(`not downloading ${image_path} - file exists`)
856
- }
980
+ });
857
981
  }
858
982
  catch (error) {
859
- this.error('downloadIcon ', error);
983
+ this.error('error in downloadIcon: ', error && error.message ? error.message : 'no message given');
860
984
  }
861
985
  }
862
986
 
@@ -864,37 +988,11 @@ class StatesController extends EventEmitter {
864
988
  const namespace = `${this.adapter.name}.admin`;
865
989
  this.adapter.fileExists(namespace, target, async (err,result) => {
866
990
  if (result) return;
867
- const src = `${tmpdir()}/${path.basename(target)}`;
868
- //const msg = `downloading ${url} to ${src}`;
869
991
  if (this.ImagesToDownload.indexOf(url) ==-1) {
870
- await this.downloadIcon(url, src)
871
- try {
872
- fs.readFile(src, (err, data) => {
873
- if (err) {
874
- this.error('unable to read ' + src + ' : '+ (err && err.message? err.message:' no message given'))
875
- return;
876
- }
877
- if (data) {
878
- this.adapter.writeFile(namespace, target, data, (err) => {
879
- if (err) {
880
- this.error('error writing file ' + target + JSON.stringify(err))
881
- return;
882
- }
883
- this.info(`copied ${src} to ${target}.`)
884
- fs.rm(src, (err) => {
885
- if (err) this.warn(`error removing ${src} : ${JSON.stringify(err)}`);
886
- });
887
- })
888
- }
889
- })
890
- }
891
- catch (error) {
892
- this.error('fs.readfile error : ', error);
893
- }
992
+ await this.downloadIcon(url, target);
894
993
  }
895
994
  });
896
995
  }
897
-
898
996
  CleanupRequired(set) {
899
997
  try {
900
998
  if (typeof set === 'boolean') this.cleanupRequired = set;
@@ -983,8 +1081,10 @@ class StatesController extends EventEmitter {
983
1081
  const has_elevated_debug = (this.checkDebugDevice(devId) && !payload.hasOwnProperty('msg_from_zigbee'));
984
1082
 
985
1083
  const message = `message received '${JSON.stringify(payload)}' from device ${devId} type '${model}'`;
986
- if (has_elevated_debug) this.emit('device_debug', { ID:debugId, data: { deviceID: devId, flag:'01', IO:true }, message:message});
987
- else if (this.debugActive) this.debug(message);
1084
+ if (has_elevated_debug)
1085
+ this.emit('device_debug', { ID:debugId, data: { deviceID: devId, flag:'03', IO:true }, message:message});
1086
+ else
1087
+ if (this.debugActive) this.debug(message);
988
1088
  if (!devStates) {
989
1089
  const message = `no device states for device ${devId} type '${model}'`;
990
1090
  if (has_elevated_debug)this.emit('device_debug', { ID:debugId, data: { error:'NOSTATE',states:[{ id:'--', value:'--', payload:payload}], IO:true }, message:message});
@@ -1015,7 +1115,7 @@ class StatesController extends EventEmitter {
1015
1115
  let stateID = statedesc.id;
1016
1116
 
1017
1117
  const message = `value generated '${JSON.stringify(value)}' from device ${devId} for '${statedesc.name}'`;
1018
- if (has_elevated_debug) this.emit('device_debug', { ID:debugId, data: { states:[{id:stateID, value:value, payload:payload }],flag:'02', IO:true }, message});
1118
+ if (has_elevated_debug) this.emit('device_debug', { ID:debugId, data: { states:[{id:stateID, value:value, payload:payload }],flag:'04', IO:true }, message});
1019
1119
  else if (this.debugActive) this.debug(message);
1020
1120
 
1021
1121
  const common = {
@@ -1065,8 +1165,10 @@ class StatesController extends EventEmitter {
1065
1165
  else if (this.debugActive) this.debug(message);
1066
1166
  }
1067
1167
  const message = `No value published for device ${devId}`;
1068
- if (!has_published && has_elevated_debug) this.emit('device_debug', { ID:debugId, data:{ error:'NOVAL', IO:true }, message:message});
1069
- else if (this.debugActive) this.debug(message);
1168
+ if (!has_published) {
1169
+ if (has_elevated_debug) this.emit('device_debug', { ID:debugId, data:{ error:'NOVAL', IO:true }, message:message});
1170
+ else if (this.debugActive) this.debug(message);
1171
+ }
1070
1172
  }
1071
1173
  else {
1072
1174
  const message = `ELEVATED IE05 - NOSTATE: No states matching the payload ${JSON.stringify(payload)} for device ${devId}`;
@@ -1079,31 +1181,78 @@ class StatesController extends EventEmitter {
1079
1181
  }
1080
1182
  }
1081
1183
 
1082
- async processConverters(converters, devId, model, mappedModel, message, meta, debugId) {
1083
- for (const converter of converters) {
1084
- const publish = (payload, dID) => {
1085
- if (typeof payload === 'object') {
1086
- this.publishToState(devId, model, payload,dID);
1184
+ postProcessConvertedFromZigbeeMessage(definition, payload, options, device) {
1185
+ // Apply calibration/precision options
1186
+ for (const [key, value] of Object.entries(payload)) {
1187
+ const definitionExposes = Array.isArray(definition.exposes) ? definition.exposes : definition.exposes(device, {});
1188
+ const expose = definitionExposes.find((e) => e.property === key);
1189
+
1190
+ if (!expose) return;
1191
+
1192
+ if (expose &&
1193
+ expose.name in zigbeeHerdsmanConvertersUtils.calibrateAndPrecisionRoundOptionsDefaultPrecision &&
1194
+ value !== '' &&
1195
+ typeof value === 'number') {
1196
+ try {
1197
+ payload[key] = zigbeeHerdsmanConvertersUtils.calibrateAndPrecisionRoundOptions(value, options, expose.name);
1198
+ } catch (error) {
1199
+ this.warn(`Failed to apply calibration to '${expose.name}': ${error && error.message ? error.message: 'no reason given'}`);
1087
1200
  }
1088
- };
1201
+ }
1202
+ }
1203
+ }
1089
1204
 
1090
- const options = await new Promise((resolve, reject) => {
1091
- this.collectOptions(devId, model, false, (options) => {
1092
- resolve(options);
1093
- });
1205
+ async processConverters(converters, devId, model, mappedModel, message, meta, debugId, has_elevated_debug) {
1206
+ let cnt = 0;
1207
+ const publish = (payload, dID) => {
1208
+ if (typeof payload === 'object' && Object.keys(payload).length > 0) {
1209
+ this.publishToState(devId, model, payload,dID);
1210
+ }
1211
+ else if (has_elevated_debug)
1212
+ this.emit('device_debug', {ID:debugId,data: { error:`NOVAL`, IO:true }, message:` payload ${JSON.stringify(payload)} is empty`})
1213
+ };
1214
+ const options = await new Promise((resolve, reject) => {
1215
+ this.collectOptions(devId, model, false, (options) => {
1216
+ resolve(options);
1094
1217
  });
1218
+ });
1095
1219
 
1096
- const payload = await new Promise((resolve, reject) => {
1220
+ const chain = [];
1221
+ for (const converter of converters) {
1222
+ const idx = cnt++;
1223
+ chain.push(new Promise((resolve) => {
1224
+ if (has_elevated_debug) this.emit('device_debug', {ID:debugId,data: { flag:`02.${cnt}a`, IO:true }, message:`converter ${cnt} : Cluster ${converter.cluster}`})
1097
1225
  const payloadConv = converter.convert(mappedModel, message, publish, options, meta);
1226
+ const metapost = meta ? {
1227
+ deviceIEEE: meta.device ? meta.device.ieeeAddr : 'no device',
1228
+ deviceModelId: meta.device ? meta.device.ModelId : 'no device',
1229
+ logger: meta.logger ? (meta.logger.constructor ? meta.logger.constructor.name : 'not a class') : 'undefined',
1230
+ state : meta.state
1231
+ } : 'undefined';
1232
+ if (has_elevated_debug) this.emit('device_debug', {ID:debugId,data: { flag:`02.${idx}b`, IO:true }, message:` data: ${safeJsonStringify(message.data)} options: ${safeJsonStringify(options)} meta:${safeJsonStringify(metapost)} result:${safeJsonStringify(payloadConv)}`})
1098
1233
  if (typeof payloadConv === 'object') {
1099
1234
  resolve(payloadConv);
1100
1235
  }
1101
- });
1236
+ else resolve({});
1237
+ }));
1238
+ }
1239
+ const candidates = await Promise.all(chain);
1240
+ const payload = {};
1102
1241
 
1103
- publish(payload, debugId);
1242
+ for (const candidate of candidates) {
1243
+ for (const key in candidate)
1244
+ payload[key] = candidate[key];
1104
1245
  }
1105
- }
1106
1246
 
1247
+ if (Object.keys(payload).length > 0 && Object.keys(options).length > 0) {
1248
+ const premsg = `candidates: ${JSON.stringify(candidates)} => payload ${JSON.stringify(payload)}`
1249
+ this.postProcessConvertedFromZigbeeMessage(mappedModel, payload, options, null);
1250
+ if (has_elevated_debug) this.emit('device_debug', {ID:debugId,data: { flag:`02.${cnt}d`, IO:true }, message:`${premsg} => processed payload : ${JSON.stringify(payload)}`})
1251
+ }
1252
+ else if (has_elevated_debug) this.emit('device_debug', {ID:debugId,data: { flag:`02.${cnt}c`, IO:true }, message:`candidates: ${JSON.stringify(candidates)} => payload ${JSON.stringify(payload)}`})
1253
+
1254
+ publish(payload, debugId);
1255
+ }
1107
1256
 
1108
1257
  async onZigbeeEvent(type, entity, message) {
1109
1258
  if (this.debugActive) this.debug(`Type ${type} device ${safeJsonStringify(entity)} incoming event: ${safeJsonStringify(message)}`);
@@ -1117,6 +1266,10 @@ class StatesController extends EventEmitter {
1117
1266
 
1118
1267
  const has_elevated_debug = this.checkDebugDevice(devId);
1119
1268
  const debugId = Date.now();
1269
+ if (entity.device.interviewing) {
1270
+ this.warn(`zigbee event for ${device.ieeeAddr} received during interview!`);
1271
+ return;
1272
+ }
1120
1273
 
1121
1274
  // raw message data for logging and msg_from_zigbee
1122
1275
  const msgForState = Object.assign({}, message);
@@ -1185,16 +1338,13 @@ class StatesController extends EventEmitter {
1185
1338
  }
1186
1339
  }
1187
1340
 
1188
- // publish raw event to "from_zigbee"
1189
- // some cleanup
1190
-
1191
1341
  this.publishToState(devId, model, {msg_from_zigbee: safeJsonStringify(msgForState)}, -1);
1192
1342
 
1193
1343
  if (!entity.mapped) {
1194
1344
  return;
1195
1345
  }
1196
1346
 
1197
- let converters = mappedModel.fromZigbee.filter(c => c && c.cluster === cluster && (
1347
+ let converters = [...mappedModel.fromZigbee,...mappedModel.toZigbee].filter(c => c && c.cluster === cluster && (
1198
1348
  Array.isArray(c.type) ? c.type.includes(type) : c.type === type));
1199
1349
 
1200
1350
 
@@ -1203,8 +1353,13 @@ class StatesController extends EventEmitter {
1203
1353
  Array.isArray(c.type) ? c.type.includes('attributeReport') : c.type === 'attributeReport'));
1204
1354
  }
1205
1355
 
1356
+ if (has_elevated_debug) {
1357
+ const message = `${converters.length} converter${converters.length > 1 ? 's' : ''} available for '${mappedModel.model}' '${devId}' with cluster '${cluster}' and type '${type}'`
1358
+ this.emit('device_debug', { ID:debugId, data: { flag:'02', IO:true }, message:message})
1359
+ }
1360
+
1206
1361
  if (!converters.length) {
1207
- if (type !== 'readResponse') {
1362
+ if (type !== 'readResponse' && type !== 'commandQueryNextImageRequest') {
1208
1363
  const message = `No converter available for '${mappedModel.model}' '${devId}' with cluster '${cluster}' and type '${type}'`;
1209
1364
  if (has_elevated_debug) this.emit('device_debug', { ID:debugId, data: { error:'NOCONV', IO:true }, message:message});
1210
1365
  else if (this.debugActive) this.debug(message);
@@ -1214,7 +1369,7 @@ class StatesController extends EventEmitter {
1214
1369
 
1215
1370
  meta.state = { state: '' }; // for tuya
1216
1371
 
1217
- this.processConverters(converters, devId, model, mappedModel, message, meta, debugId)
1372
+ this.processConverters(converters, devId, model, mappedModel, message, meta, debugId, has_elevated_debug)
1218
1373
  .catch((error) => {
1219
1374
  // 'Error: Expected one of: 0, 1, got: 'undefined''
1220
1375
  if (cluster !== '64529') {
@@ -1228,9 +1383,6 @@ class StatesController extends EventEmitter {
1228
1383
  async stop() {
1229
1384
  this.localConfig.retainData();
1230
1385
  }
1231
-
1232
-
1233
-
1234
1386
  }
1235
1387
 
1236
- module.exports = StatesController;
1388
+ module.exports = StatesController;