iobroker.zigbee 3.0.3 → 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.
@@ -35,6 +35,7 @@ class StatesController extends EventEmitter {
35
35
  this.stashedUnknownModels = [];
36
36
  this.debugMessages = { nodevice:{ in:[], out: []} };
37
37
  this.debugActive = true;
38
+ this.deviceQueryBlock = [];
38
39
  }
39
40
 
40
41
  info(message, data) {
@@ -81,6 +82,8 @@ class StatesController extends EventEmitter {
81
82
  }
82
83
 
83
84
  async AddModelFromHerdsman(device, model) {
85
+ const namespace = `${this.adapter.name}.admin`;
86
+
84
87
  // this.warn('addModelFromHerdsman ' + JSON.stringify(model) + ' ' + JSON.stringify(this.localConfig.getOverrideWithKey(model, 'legacy', true)));
85
88
  if (this.localConfig.getOverrideWithTargetAndKey(model, 'legacy', true)) {
86
89
  this.debug('Applying legacy definition for ' + model);
@@ -97,52 +100,56 @@ class StatesController extends EventEmitter {
97
100
  const srcIcon = (modelDesc ? modelDesc.icon : '');
98
101
  const model_modif = model.replace(/\//g, '-');
99
102
  const pathToAdminIcon = `img/${model_modif}.png`;
100
-
101
- const namespace = `${this.adapter.name}.admin`;
103
+ // source is a web address
102
104
  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
- });
105
+ try {
106
+ //if (modelDesc) modelDesc.icon = pathToAdminIcon;
107
+ this.downloadIconToAdmin(srcIcon, pathToAdminIcon)
108
+ } catch (err) {
109
+ this.warn(`ERROR : unable to download ${srcIcon}: ${err && err.message ? err.message : 'no reason given'}`);
110
+ }
116
111
  return;
117
112
  }
113
+ // source is inline basee64
118
114
  const base64Match = srcIcon.match(/data:image\/(.+);base64,/);
119
115
  if (base64Match) {
120
116
  this.warn(`base 64 Icon matched, trying to save it to disk as ${pathToAdminIcon}`);
121
- modelDesc.icon = pathToAdminIcon;
117
+ if (modelDesc) modelDesc.icon = pathToAdminIcon;
122
118
  this.adapter.fileExists(namespace, pathToAdminIcon, async (err,result) => {
123
119
  if (result) {
124
120
  this.warn(`no need to save icon to ${pathToAdminIcon}`);
125
121
  return;
126
122
  }
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.');
123
+ const msg = `Saving base64 Data to ${pathToAdminIcon}`
124
+ try {
125
+ const buffer = Buffer.from(srcIcon.replace(base64Match[0],''), 'base64');
126
+ this.adapter.writeFile(namespace, pathToAdminIcon, buffer, (err) => {
127
+ if (err) {
128
+ this.warn(`${msg} -- failed: ${err && err.message ? err.message : 'no reason given'}`);
129
+ return;
130
+ }
131
+ this.info(`${msg} -- success`);
132
+ });
133
+ }
134
+ catch (err) {
135
+ this.warn(`${msg} -- failed: ${err && err.message ? err.message : 'no reason given'}`)
136
+ }
131
137
  });
132
138
  return;
133
139
  }
140
+ // path is absolute
134
141
  if (modelDesc) modelDesc.icon = pathToAdminIcon;
135
142
  this.adapter.fileExists(namespace, pathToAdminIcon, async(err, result) => {
136
143
  if (result) {
137
- this.debug(`icon ${modelDesc.icon} found - no copy needed`);
144
+ this.debug(`icon ${modelDesc ? modelDesc.icon : 'unknown icon'} found - no copy needed`);
138
145
  return;
139
146
  }
140
147
  // try 3 options for source file
141
148
  let src = srcIcon; // as given
142
149
  const locations=[];
143
150
  if (!fs.existsSync(src)) {
144
- locations.push(src);
145
151
  src = path.normalize(this.adapter.expandFileName(src));
152
+ locations.push(src);
146
153
  } // assumed relative to data folder
147
154
  if (!fs.existsSync(src)) {
148
155
  locations.push(src);
@@ -158,36 +165,46 @@ class StatesController extends EventEmitter {
158
165
  }
159
166
  fs.readFile(src, (err, data) => {
160
167
  if (err) {
161
- this.error('unable to read ' + src + ' : '+ (err.message? err.message:' no message given'))
168
+ this.warn(`unable to read ${src}: ${(err.message? err.message:' no message given')}`);
162
169
  return;
163
170
  }
164
171
  if (data) {
165
172
  this.adapter.writeFile(namespace, pathToAdminIcon, data, (err) => {
166
173
  if (err) {
167
- this.error('error writing file ' + path + JSON.stringify(err))
174
+ this.error(`error writing file ${path}: ${err.message ? err.message : 'no reason given'}`);
168
175
  return;
169
176
  }
170
177
  this.info('Updated image file ' + pathToAdminIcon);
171
178
  });
179
+ return;
172
180
  }
181
+ this.error(`fs.readFile failed - neither error nor data is returned!`);
173
182
  });
174
183
  });
175
184
  }
176
185
  }
177
186
  }
178
187
 
188
+ async updateDebugDevices(debugDevices) {
189
+ if (debugDevices != undefined)
190
+ this.debugDevices = debugDevices;
191
+ this.adapter.zbController.callExtensionMethod('setLocalVariable', ['debugDevices', this.debugDevices]);
192
+
193
+ }
194
+
179
195
  async getDebugDevices(callback) {
180
196
  if (this.debugDevices === undefined) {
181
197
  this.debugDevices = [];
182
198
  const state = await this.adapter.getStateAsync(`${this.adapter.namespace}.info.debugmessages`);
183
199
  if (state) {
184
200
  if (typeof state.val === 'string' && state.val.length > 2) {
185
- this.debugDevices = state.val.split(';');
201
+ this.updateDebugDevices(state.val.split(';'));
186
202
  }
187
203
  this.info(`debug devices set to ${JSON.stringify(this.debugDevices)}`);
188
204
  if (callback) callback(this.debugDevices);
189
205
  }
190
206
  } else {
207
+ this.updateDebugDevices();
191
208
  // this.info(`debug devices was already set to ${JSON.stringify(this.debugDevices)}`);
192
209
  callback(this.debugDevices)
193
210
  }
@@ -195,15 +212,20 @@ class StatesController extends EventEmitter {
195
212
 
196
213
  async toggleDeviceDebug(id) {
197
214
  const arr = /zigbee.[0-9].([^.]+)/gm.exec(id);
215
+ if (!arr) {
216
+ this.warn(`unable to toggle debug for device ${id}: there was no mat (${JSON.stringify(arr)}) `);
217
+ return this.debugDevices;
218
+ }
198
219
  if (arr[1] === undefined) {
199
220
  this.warn(`unable to extract id from state ${id}`);
200
- return [];
221
+ return this.debugDevices;
201
222
  }
202
223
  const stateKey = arr[1];
203
224
  if (this.debugDevices === undefined) this.debugDevices = await this.getDebugDevices()
204
225
  const idx = this.debugDevices.indexOf(stateKey);
205
226
  if (idx < 0) this.debugDevices.push(stateKey);
206
227
  else this.debugDevices.splice(idx, 1);
228
+ this.updateDebugDevices()
207
229
  await this.adapter.setStateAsync(`${this.adapter.namespace}.info.debugmessages`, this.debugDevices.join(';'), true);
208
230
  this.info('debug devices set to ' + JSON.stringify(this.debugDevices));
209
231
  return this.debugDevices;
@@ -243,12 +265,13 @@ class StatesController extends EventEmitter {
243
265
  } else {
244
266
  this.debugDevices = [];
245
267
  }
268
+ this.updateDebugDevices();
246
269
  this.info('debug devices set to ' + JSON.stringify(this.debugDevices));
247
270
  return;
248
271
  }
249
272
 
250
273
  const devId = getAdId(this.adapter, id); // iobroker device id
251
- let deviceId = getZbId(id); // zigbee device id
274
+ const deviceId = getZbId(id); // zigbee device id
252
275
 
253
276
  if (this.checkDebugDevice(id)) {
254
277
  const message = `User state change of state ${id} with value ${state.val} (ack: ${state.ack}) from ${state.from}`;
@@ -272,15 +295,24 @@ class StatesController extends EventEmitter {
272
295
  if (this.debugActive) this.debug('State Change detected on deactivated Device - ignored');
273
296
  return;
274
297
  }
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, {});
298
+ // check for group (model is group, deviceId is numerical, and not Nan or 0)
299
+ /*
300
+ if (model === 'group' && typeof deviceId == 'number' && Boolean(deviceId)) {
301
+ const options = this.localConfig.getOptions(`group_${deviceId}`);
302
+ options.isActive == (obj.common !== null);
303
+ this.publishFromState(deviceId, model, stateKey, state, options, debugId);
304
+ return;
305
+ }
306
+ */
307
+ // handle send_payload here
308
+ if (model && model.id === 'device_query') {
309
+ if (this.query_device_block.indexOf(deviceId) > -1 && !state.source.includes('.admin.')) {
310
+ this.info(`Device query for '${deviceId}' blocked - device query timeout has not elapsed yet.`);
280
311
  return;
281
312
  }
282
-
313
+ this.emit('device_query', { deviceId, debugId });
283
314
  }
315
+
284
316
  this.collectOptions(id.split('.')[2], model, true, options =>
285
317
  this.publishFromState(deviceId, model, stateKey, state, options, debugId));
286
318
  }
@@ -295,7 +327,7 @@ class StatesController extends EventEmitter {
295
327
  callback(result);
296
328
  return;
297
329
  }
298
- // find model states for options and get it values. No options for groups !!!
330
+ // find model states for options and get it values.
299
331
  const devStates = await this.getDevStates('0x' + devId, model);
300
332
  if (devStates == null || devStates == undefined || devStates.states == null || devStates.states == undefined) {
301
333
  callback(result);
@@ -372,7 +404,7 @@ class StatesController extends EventEmitter {
372
404
  }
373
405
  }
374
406
 
375
- async triggerComposite(_deviceId, model, stateDesc, interactive) {
407
+ async triggerComposite(_deviceId, stateDesc, interactive) {
376
408
  const deviceId = (_deviceId.replace('0x', ''));
377
409
  const idParts = stateDesc.id.split('.').slice(-2);
378
410
  const key = `${deviceId}.${idParts[0]}`;
@@ -400,20 +432,26 @@ class StatesController extends EventEmitter {
400
432
  }, (stateDesc.compositeTimeout ? stateDesc.compositeTimeout : 100) * factor);
401
433
  }
402
434
 
403
- async publishFromState(deviceId, model, stateKey, state, options, debugId) {
435
+
436
+ handleLinkedFunctResult(lfArr, devId, state) {
437
+ if (this.handleOption(devId, state.stateDesc)) return;
438
+ lfArr.push(state);
439
+ }
440
+
441
+ async publishFromState(deviceId, model, stateKey, state, options, debugID) {
404
442
  if (this.debugActive) this.debug(`Change state '${stateKey}' at device ${deviceId} type '${model}'`);
405
- const elevated = this.checkDebugDevice(deviceId);
443
+ const has_elevated_debug = this.checkDebugDevice(typeof deviceId == 'number' ? `group_${deviceId}` : deviceId);
406
444
 
407
- if (elevated) {
445
+ if (has_elevated_debug) {
408
446
  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});
447
+ this.emit('device_debug', { ID:debugID, data: { ID: deviceId, model: model, flag:'02', IO:false }, message:message});
410
448
  }
411
449
 
412
450
  const devStates = await this.getDevStates(deviceId, model);
413
451
  if (!devStates) {
414
- if (elevated) {
452
+ if (has_elevated_debug) {
415
453
  const message = (`no device states for device ${deviceId} type '${model}'`);
416
- this.emit('device_debug', { ID:debugId, data: { error: 'NOSTATES' , IO:false }, message:message});
454
+ this.emit('device_debug', { ID:debugID, data: { error: 'NOSTATES' , IO:false }, message:message});
417
455
  }
418
456
  return;
419
457
  }
@@ -422,33 +460,80 @@ class StatesController extends EventEmitter {
422
460
  const stateModel = devStates.stateModel;
423
461
  if (!stateDesc) {
424
462
  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});
463
+ 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
464
  return;
427
465
  }
428
466
 
429
467
  const value = state.val;
430
468
  if (value === undefined || value === '') {
431
- if (elevated) {
469
+ if (has_elevated_debug) {
432
470
  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});
471
+ this.emit('device_debug', { ID:debugID, data: { states:[{id:state.ID, value:'--', payload:'error', ep:stateDesc.epname}],error: 'NOVAL' , IO:false }, message:message});
472
+ }
473
+ return;
474
+ }
475
+
476
+ // send_payload can never be a linked state !;
477
+ if (stateDesc.id === 'send_payload') {
478
+ try {
479
+ const json_value = JSON.parse(value);
480
+ const payload = {device: deviceId.replace('0x', ''), payload: json_value, model:model, stateModel:stateModel};
481
+ 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 }});
482
+
483
+ this.emit('send_payload', payload, debugID);
484
+ } catch (error) {
485
+ const message = `send_payload: ${value} does not parse as JSON Object : ${error.message}`;
486
+ 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});
487
+ else this.error(message);
488
+ return;
489
+ }
490
+ return;
491
+ }
492
+
493
+ if (stateDesc.id === 'device_query') {
494
+ const interactive = (state.from.includes('.admin'));
495
+ if (!interactive && this.deviceQueryBlock.includes(deviceId)) {
496
+ this.warn(`device_query blocked due to excessive triggering - retrigger > 10 seconds after previous trigger has completed.`);
497
+ return;
434
498
  }
499
+ this.deviceQueryBlock.push[deviceId];
500
+ const id = deviceId;
501
+ this.emit('device_query', deviceId, debugID, has_elevated_debug, (devId) =>{
502
+ setTimeout(() => { const idx = this.deviceQueryBlock.indexOf(id);
503
+ if (idx > -1) this.deviceQueryBlock.splice(idx);
504
+ }, 10000)
505
+
506
+ } )
507
+ return;
508
+ }
509
+
510
+ // composite states can never be linked states;
511
+ if (stateDesc.compositeState && stateDesc.compositeTimeout) {
512
+ this.triggerComposite(deviceId, stateDesc, state.from.includes('.admin.'));
435
513
  return;
436
514
  }
437
- let stateList = [{stateDesc: stateDesc, value: value, index: 0, timeout: 0, source:state.from}];
515
+
516
+ const stateList = [{stateDesc: stateDesc, value: value, index: 0, timeout: 0, source:state.from}];
517
+
438
518
  if (stateModel && stateModel.linkedStates) {
439
519
  stateModel.linkedStates.forEach(linkedFunct => {
440
520
  try {
441
521
  if (typeof linkedFunct === 'function') {
442
522
  const res = linkedFunct(stateDesc, value, options, this.adapter.config.disableQueue);
443
523
  if (res) {
444
- stateList = stateList.concat(res);
524
+ if (res.hasOwnProperty('stateDesc')) { // we got a single state back
525
+ if (! res.stateDesc.isOption) stateList.push(res);
526
+ }
527
+ else {
528
+ res.forEach((ls) => { if (!ls.stateDesc.isOption) stateList.push(res)} );
529
+ }
445
530
  }
446
531
  } else {
447
532
  this.warn(`publish from State - LinkedState is not a function ${JSON.stringify(linkedFunct)}`);
448
533
  }
449
534
  } catch (e) {
450
535
  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 }});
536
+ 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
537
  this.error('Exception caught in publishfromstate: ' + (e && e.message ? e.message : 'no error message given'));
453
538
  }
454
539
 
@@ -464,7 +549,8 @@ class StatesController extends EventEmitter {
464
549
  readAfterWriteStates = readAfterWriteStates.concat(readAfterWriteStateDesc.id));
465
550
  }
466
551
 
467
- this.emit('changed', deviceId, model, stateModel, stateList, options, debugId);
552
+ if (stateList.length > 0)
553
+ this.emit('changed', deviceId, model, stateModel, stateList, options, debugID, has_elevated_debug );
468
554
  }
469
555
 
470
556
  async renameDevice(id, newName) {
@@ -481,7 +567,6 @@ class StatesController extends EventEmitter {
481
567
  }
482
568
 
483
569
  verifyDeviceName(id, model ,name) {
484
- const savedId = id.replace(`${this.adapter.namespace}.`, '');
485
570
  return this.localConfig.NameForId(id, model, name);
486
571
  }
487
572
 
@@ -503,7 +588,7 @@ class StatesController extends EventEmitter {
503
588
  }
504
589
 
505
590
  } catch (error) {
506
- this.adapter.log.info(`Cannot delete Object ${devId}: ${error && error.message ? error.message : 'without error message'}`);
591
+ this.adapter.log.warn(`Cannot delete Object ${devId}: ${error && error.message ? error.message : 'without error message'}`);
507
592
  }
508
593
  }
509
594
 
@@ -576,35 +661,8 @@ class StatesController extends EventEmitter {
576
661
  const stateId = devId + '.' + name;
577
662
  const new_name = obj.common.name;
578
663
  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;
664
+ for (const key in common) {
665
+ if (common[key] !== undefined) new_common[key] = common[key];
608
666
  }
609
667
  }
610
668
  // check if state exist
@@ -697,7 +755,7 @@ class StatesController extends EventEmitter {
697
755
  if (this.debugActive) this.debug(`UpdateState: Device is deactivated ${devId} ${JSON.stringify(obj)}`);
698
756
  }
699
757
  } else {
700
- if (this.debugActive) this.debug(`UpdateState: missing device ${devId} ${JSON.stringify(obj)}`);
758
+ if (this.debugActive) this.debug(`UpdateState: missing device ${devId}`);
701
759
  }
702
760
  }
703
761
 
@@ -760,15 +818,25 @@ class StatesController extends EventEmitter {
760
818
  statesMapping.getByModel();
761
819
  }
762
820
 
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'
821
+
822
+ async getDefaultGroupIcon(id, members) {
823
+ let groupID = 0;
824
+ if (typeof id == 'string') {
825
+ const regexResult = id.match(new RegExp(/group_(\d+)/));
826
+ if (!regexResult) return '';
827
+ groupID = Number(regexResult[1]);
828
+ } else if (typeof id == 'number') {
829
+ groupID = id;
830
+ }
831
+ if (groupID <= 0) return;
832
+ if (typeof members != 'number') {
833
+ const group = await this.adapter.zbController.getGroupMembersFromController(groupID)
834
+ if (typeof group == 'object')
835
+ return `img/group_${Math.max(Math.min(group.length, 7), 0)}.png`
836
+ else
837
+ return 'img/group_x.png'
838
+ }
839
+ return `img/group_${Math.max(Math.min(members, 7), 0)}.png`
772
840
  }
773
841
 
774
842
  async updateDev(dev_id, dev_name, model, callback) {
@@ -777,7 +845,9 @@ class StatesController extends EventEmitter {
777
845
  if (this.debugActive) this.debug(`UpdateDev called with ${dev_id}, ${dev_name}, ${model}, ${__dev_name}`);
778
846
  const id = '' + dev_id;
779
847
  const modelDesc = statesMapping.findModel(model);
780
- const modelIcon = (model == 'group' ? await this.getDefaultGroupIcon(dev_id) : modelDesc && modelDesc.icon ? modelDesc.icon : 'img/unknown.png');
848
+ const modelIcon = (model == 'group' ?
849
+ await this.getDefaultGroupIcon(dev_id) :
850
+ modelDesc && modelDesc.icon ? modelDesc.icon : 'img/unknown.png');
781
851
  let icon = this.localConfig.IconForId(dev_id, model, modelIcon);
782
852
 
783
853
  // download icon if it external and not undefined
@@ -786,6 +856,8 @@ class StatesController extends EventEmitter {
786
856
  } else {
787
857
  const model_modif = model.replace(/\//g, '-');
788
858
  const pathToAdminIcon = `img/${model_modif}.png`;
859
+ // await this.fetchIcon(icon, '')
860
+
789
861
 
790
862
  if (icon.startsWith('http')) {
791
863
  try {
@@ -824,39 +896,69 @@ class StatesController extends EventEmitter {
824
896
  });
825
897
  }
826
898
 
899
+ async streamToBuffer(readableStream) {
900
+ return new Promise((resolve, reject) => {
901
+ const chunks = [];
902
+ readableStream.on('data', data => {
903
+ if (typeof data === 'string') {
904
+ // Convert string to Buffer assuming UTF-8 encoding
905
+ chunks.push(Buffer.from(data, 'utf-8'));
906
+ } else if (data instanceof Buffer) {
907
+ chunks.push(data);
908
+ } else {
909
+ // Convert other data types to JSON and then to a Buffer
910
+ const jsonData = JSON.stringify(data);
911
+ chunks.push(Buffer.from(jsonData, 'utf-8'));
912
+ }
913
+ });
914
+ readableStream.on('end', () => {
915
+ resolve(Buffer.concat(chunks));
916
+ });
917
+ readableStream.on('error', (err) => {
918
+ this.error(`error getting buffer from stream: ${err && err.message ? err.message : 'no reason given'}`);
919
+ reject;
920
+ });
921
+ });
922
+ }
923
+
924
+ async fetchIcon(url, image_path) {
925
+ const response = await fetch(url);
926
+ const data = await this.streamToBuffer(response.body);
927
+ fs.writeFileSync('/opt/iobroker/iobroker-data/zigbee_0/test.png', response.data);
928
+ }
929
+
827
930
  async downloadIcon(url, image_path) {
828
931
  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(() => {
846
- const idx = this.ImagesToDownload.indexOf(url);
847
- if (idx > -1) {
848
- this.ImagesToDownload.splice(idx, 1);
932
+ const namespace = `${this.adapter.name}.admin`;
933
+ this.ImagesToDownload.push(url);
934
+ return new Promise((resolve, reject) => {
935
+ this.info(`downloading ${url} to ${image_path}`);
936
+ axios({
937
+ method: 'get',
938
+ url: url,
939
+ responseType: 'stream' // Dies ist wichtig, um den Stream direkt zu erhalten
940
+ }).then(async response => {
941
+ const data = await this.streamToBuffer(response.data);
942
+ this.adapter.writeFile(namespace, image_path, data, (err) => {
943
+ if (err) {
944
+ this.error(`error writing ${image_path} to admin: ${err.message ? err.message : 'no message given'}`);
945
+ reject;
849
946
  }
850
-
947
+ this.info(`downloaded ${url} to ${image_path}.`)
948
+ resolve;
851
949
  });
950
+ }).catch(err => {
951
+ this.warn(`error downloading icon ${err && err.message ? err.message : 'no message given'}`);
952
+ }).finally(() => {
953
+ const idx = this.ImagesToDownload.indexOf(url);
954
+ if (idx > -1) {
955
+ this.ImagesToDownload.splice(idx, 1);
956
+ }
852
957
  });
853
- }
854
- else {
855
- this.info(`not downloading ${image_path} - file exists`)
856
- }
958
+ });
857
959
  }
858
960
  catch (error) {
859
- this.error('downloadIcon ', error);
961
+ this.error('error in downloadIcon: ', error && error.message ? error.message : 'no message given');
860
962
  }
861
963
  }
862
964
 
@@ -864,33 +966,8 @@ class StatesController extends EventEmitter {
864
966
  const namespace = `${this.adapter.name}.admin`;
865
967
  this.adapter.fileExists(namespace, target, async (err,result) => {
866
968
  if (result) return;
867
- const src = `${tmpdir()}/${path.basename(target)}`;
868
- //const msg = `downloading ${url} to ${src}`;
869
969
  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
- }
970
+ await this.downloadIcon(url, target);
894
971
  }
895
972
  });
896
973
  }
@@ -1065,8 +1142,10 @@ class StatesController extends EventEmitter {
1065
1142
  else if (this.debugActive) this.debug(message);
1066
1143
  }
1067
1144
  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);
1145
+ if (!has_published) {
1146
+ if (has_elevated_debug) this.emit('device_debug', { ID:debugId, data:{ error:'NOVAL', IO:true }, message:message});
1147
+ else if (this.debugActive) this.debug(message);
1148
+ }
1070
1149
  }
1071
1150
  else {
1072
1151
  const message = `ELEVATED IE05 - NOSTATE: No states matching the payload ${JSON.stringify(payload)} for device ${devId}`;
@@ -1079,6 +1158,27 @@ class StatesController extends EventEmitter {
1079
1158
  }
1080
1159
  }
1081
1160
 
1161
+ postProcessConvertedFromZigbeeMessage(definition, payload, options, device) {
1162
+ // Apply calibration/precision options
1163
+ for (const [key, value] of Object.entries(payload)) {
1164
+ const definitionExposes = Array.isArray(definition.exposes) ? definition.exposes : definition.exposes(device, {});
1165
+ const expose = definitionExposes.find((e) => e.property === key);
1166
+
1167
+ if (!expose) return;
1168
+
1169
+ if (expose &&
1170
+ expose.name in zigbeeHerdsmanConvertersUtils.calibrateAndPrecisionRoundOptionsDefaultPrecision &&
1171
+ value !== '' &&
1172
+ typeof value === 'number') {
1173
+ try {
1174
+ payload[key] = zigbeeHerdsmanConvertersUtils.calibrateAndPrecisionRoundOptions(value, options, expose.name);
1175
+ } catch (error) {
1176
+ this.warn(`Failed to apply calibration to '${expose.name}': ${error && error.message ? error.message: 'no reason given'}`);
1177
+ }
1178
+ }
1179
+ }
1180
+ }
1181
+
1082
1182
  async processConverters(converters, devId, model, mappedModel, message, meta, debugId) {
1083
1183
  for (const converter of converters) {
1084
1184
  const publish = (payload, dID) => {
@@ -1100,6 +1200,10 @@ class StatesController extends EventEmitter {
1100
1200
  }
1101
1201
  });
1102
1202
 
1203
+ if (Object.keys(payload).length > 0 && Object.keys(options).length > 0) {
1204
+ this.postProcessConvertedFromZigbeeMessage(mappedModel, payload, options, null);
1205
+ }
1206
+
1103
1207
  publish(payload, debugId);
1104
1208
  }
1105
1209
  }
@@ -1117,6 +1221,10 @@ class StatesController extends EventEmitter {
1117
1221
 
1118
1222
  const has_elevated_debug = this.checkDebugDevice(devId);
1119
1223
  const debugId = Date.now();
1224
+ if (entity.device.interviewing) {
1225
+ this.warn(`zigbee event for ${device.ieeeAddr} received during interview!`);
1226
+ return;
1227
+ }
1120
1228
 
1121
1229
  // raw message data for logging and msg_from_zigbee
1122
1230
  const msgForState = Object.assign({}, message);
@@ -1194,7 +1302,7 @@ class StatesController extends EventEmitter {
1194
1302
  return;
1195
1303
  }
1196
1304
 
1197
- let converters = mappedModel.fromZigbee.filter(c => c && c.cluster === cluster && (
1305
+ let converters = [...mappedModel.fromZigbee,...mappedModel.toZigbee].filter(c => c && c.cluster === cluster && (
1198
1306
  Array.isArray(c.type) ? c.type.includes(type) : c.type === type));
1199
1307
 
1200
1308
 
@@ -1204,7 +1312,7 @@ class StatesController extends EventEmitter {
1204
1312
  }
1205
1313
 
1206
1314
  if (!converters.length) {
1207
- if (type !== 'readResponse') {
1315
+ if (type !== 'readResponse' && type !== 'commandQueryNextImageRequest') {
1208
1316
  const message = `No converter available for '${mappedModel.model}' '${devId}' with cluster '${cluster}' and type '${type}'`;
1209
1317
  if (has_elevated_debug) this.emit('device_debug', { ID:debugId, data: { error:'NOCONV', IO:true }, message:message});
1210
1318
  else if (this.debugActive) this.debug(message);