iobroker.zigbee 3.3.1-alpha.0 → 3.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/models.js CHANGED
@@ -7,7 +7,7 @@ const { findByDevice } = require('zigbee-herdsman-converters');
7
7
  const herdsmanModelInfo = new Map();
8
8
  const UUIDbyDevice = new Map();
9
9
 
10
- const { ParseColor } = require('./colors');
10
+ const { ParseColor, complexColor, CC_DEFS } = require('./colors');
11
11
  const { rgb_to_cie, cie_to_rgb } = require('./rgb');
12
12
  const { decimalToHex, adapterLevelToBulbLevel, bulbLevelToAdapterLevel , toMired } = require('./utils');
13
13
 
@@ -245,6 +245,8 @@ const states = {
245
245
  setter: (value) => {
246
246
 
247
247
  // convert RGB to XY for set
248
+ const objColor = complexColor(value, true);
249
+ if (objColor) return objColor;
248
250
  let xy = [0, 0];
249
251
  const rgbcolor = ParseColor(value);
250
252
  xy = rgb_to_cie(rgbcolor.r, rgbcolor.g, rgbcolor.b);
@@ -259,14 +261,100 @@ const states = {
259
261
  return {...options, transition: transitionTime};
260
262
  },
261
263
  getter: payload => {
262
- if (payload.color && payload.color.hasOwnProperty('x') && payload.color.hasOwnProperty('y')) {
263
- const colorval = cie_to_rgb(payload.color.x, payload.color.y);
264
- return '#' + decimalToHex(colorval[0],2) + decimalToHex(colorval[1],2) + decimalToHex(colorval[2],2);
265
- } else {
266
- return undefined;
264
+ const value = payload.color;
265
+ if (value && typeof value === 'object') {
266
+ return complexColor(value, false);
267
267
  }
268
+ if (typeof value === 'string') return value;
269
+ return undefined;
268
270
  },
269
271
  },
272
+ color_hue: {
273
+ id: `color_hs.hue`,
274
+ prop:'color',
275
+ name: `Hue`,
276
+ icon: undefined,
277
+ role: 'level.color.hue',
278
+ write: true,
279
+ read: true,
280
+ type: 'number',
281
+ min: 0,
282
+ max: 360,
283
+ compositeKey: 'color_hs',
284
+ compositeTimeout: 500,
285
+ compositeState: 'color',
286
+ getter: (payload) => {
287
+ if (typeof payload.color == 'object') {
288
+ if (payload.color?.hue) {
289
+ return payload.color.hue;
290
+ }
291
+ }
292
+ }
293
+ },
294
+ color_saturation: {
295
+ id: `color_hs.saturation`,
296
+ prop:'color',
297
+ name: `Saturation`,
298
+ icon: undefined,
299
+ role: 'level.color.saturation',
300
+ write: true,
301
+ read: true,
302
+ type: 'number',
303
+ min: 0,
304
+ max: 100,
305
+ compositeKey: 'color_hs',
306
+ compositeTimeout: 500,
307
+ compositeState: 'color',
308
+ getter: (payload) => {
309
+ if (typeof payload.color == 'object') {
310
+ if (payload.color?.saturation) {
311
+ return payload.color.saturation;
312
+ }
313
+ }
314
+ }
315
+ },
316
+ color_red: {
317
+ id: `color_rgb.r`,
318
+ name: `Red`,
319
+ icon: undefined,
320
+ role: 'level.color.red',
321
+ write: true,
322
+ read: true,
323
+ type: 'number',
324
+ min: 0,
325
+ max: 255,
326
+ compositeKey: 'color_rgb',
327
+ compositeTimeout: 500,
328
+ compositeState: 'color',
329
+ },
330
+ color_green: {
331
+ id: `color_rgb.g`,
332
+ name: `Green`,
333
+ icon: undefined,
334
+ role: 'level.color.green',
335
+ write: true,
336
+ read: true,
337
+ type: 'number',
338
+ min: 0,
339
+ max: 255,
340
+ compositeKey: 'color_rgb',
341
+ compositeTimeout: 500,
342
+ compositeState: 'color',
343
+ },
344
+ color_blue: {
345
+ id: `color_rgb.b`,
346
+ name: `Blue`,
347
+ icon: undefined,
348
+ role: 'level.color.blue',
349
+ write: true,
350
+ read: true,
351
+ type: 'number',
352
+ min: 0,
353
+ max: 255,
354
+ compositeKey: 'color_rgb',
355
+ compositeTimeout: 500,
356
+ compositeState: 'color'
357
+ },
270
358
  hex_color: {
271
359
  id:`hex_color`,
272
360
  name: `Hex Color`,
@@ -489,14 +577,14 @@ const states = {
489
577
 
490
578
 
491
579
  const lightStates = [states.state, states.brightness, states.brightness_move, states.transition_time, states.brightness_step],
492
- lightStatesWithColor=[...lightStates, states.color, states.hex_color, states.colortemp, states.colortemp_move],
580
+ lightStatesWithColor=[...lightStates, states.color, states.hex_color, states.colortemp, states.colortemp_move,states.color_red, states.color_green, states.color_blue],
493
581
  onOffStates=[states.state],
494
582
  lightStatesWithColortemp = [...lightStates, states.colortemp, states.colortemp_move],
495
- lightStatesWithColor_hue= [...lightStatesWithColor, states.hue_move, states.transition_time, states.effect_type_hue],
496
- lightStatesWithColorNoTemp= [...lightStates, states.color],
583
+ lightStatesWithColor_hue= [...lightStatesWithColor, states.hue_move, states.transition_time, /*states.effect_type_hue,*/ states.color_hue, states.color_saturation],
584
+ lightStatesWithColorNoTemp= [...lightStates, states.color, states.color_hue, states.color_saturation],
497
585
  commonStates=Object.values(states).filter((candidate) => candidate.isCommonState),
498
586
  commonGroupStates=[states.groupstateupdate, states.groupmemberupdate],
499
- groupStates=[states.groupstateupdate, states.groupmemberupdate, ...lightStatesWithColor, ...onOffStates];
587
+ groupStates=[states.groupstateupdate, states.groupmemberupdate, ...lightStatesWithColor_hue, ...onOffStates];
500
588
 
501
589
  /**
502
590
  * Returns a model description, if one exists
@@ -557,7 +645,7 @@ async function addExposeToDevices(device, model, logger) {
557
645
  adapterModelDefinition.states = commonStates;
558
646
  const adapterModel = await findModel(model, device.ieeeAddr, false);
559
647
  if (adapterModel) return adapterModel;
560
- const { newModel, message, error } = await applyHerdsmanModel(adapterModelDefinition);
648
+ const { newModel, message, error } = await applyHerdsmanModel(adapterModelDefinition, { newCompositeMethod: true });
561
649
  if (error) {
562
650
  if (logger) logger.warn(`${message} : ${error?.message}`);
563
651
  }
@@ -580,8 +668,9 @@ async function clearModelDefinitions() {
580
668
 
581
669
  function getStateDefinition(name, type, prop) {
582
670
  if (typeof name != 'string') return getStateDefinition('ilstate', type, prop);
671
+ if (states.hasOwnProperty(name)) return states[name];
583
672
  if (legacyStates.hasOwnProperty(name)) return legacyStates[name];
584
- return states[name] || {
673
+ return {
585
674
  id: name,
586
675
  prop: prop || name,
587
676
  name: name.replace('_',' '),
@@ -99,87 +99,92 @@ class StatesController extends EventEmitter {
99
99
  const srcIcon = (modelDesc && modelDesc.icon ? modelDesc.icon : '');
100
100
  const model_modif = model.replace(/\//g, '-');
101
101
  const pathToAdminIcon = `img/${model_modif}.png`;
102
- // source is a web address
103
- if (srcIcon.startsWith('http')) {
104
- try {
105
- //if (modelDesc) modelDesc.icon = pathToAdminIcon;
106
- this.downloadIconToAdmin(srcIcon, pathToAdminIcon)
107
- } catch (err) {
108
- this.warn(`ERROR : unable to download ${srcIcon}: ${err && err.message ? err.message : 'no reason given'}`);
109
- }
110
- return;
111
- }
112
- // source is inline basee64F
113
- const base64Match = srcIcon.match(/data:image\/(.+);base64,/);
114
- if (base64Match) {
115
- this.warn(`base 64 Icon matched, trying to save it to disk as ${pathToAdminIcon}`);
116
- if (modelDesc) modelDesc.icon = pathToAdminIcon;
117
- this.adapter.fileExists(namespace, pathToAdminIcon, async (err,result) => {
118
- if (result) {
119
- this.info(`no need to save icon to ${pathToAdminIcon}`);
120
- return;
121
- }
122
- const msg = `Saving base64 Data to ${pathToAdminIcon}`
102
+
103
+ if (srcIcon != '') {
104
+ // source is a web address
105
+ if (srcIcon.startsWith('http')) {
123
106
  try {
124
- const buffer = Buffer.from(srcIcon.replace(base64Match[0],''), 'base64');
125
- this.adapter.writeFile(namespace, pathToAdminIcon, buffer, (err) => {
126
- if (err) {
127
- this.warn(`${msg} -- failed: ${err?.message || 'no reason given'}`);
128
- return;
129
- }
130
- this.debug(`${msg} -- success`);
131
- });
107
+ //if (modelDesc) modelDesc.icon = pathToAdminIcon;
108
+ this.downloadIconToAdmin(srcIcon, pathToAdminIcon);
109
+ } catch (err) {
110
+ this.warn(`ERROR : unable to download ${srcIcon}: ${err && err.message ? err.message : 'no reason given'}`);
132
111
  }
133
- catch (err) {
134
- this.warn(`${msg} -- failed: ${err?.message || 'no reason given'}`)
135
- }
136
- });
137
- return;
138
- }
139
- // path is absolute
140
- if (modelDesc) modelDesc.icon = pathToAdminIcon;
141
- this.adapter.fileExists(namespace, pathToAdminIcon, async(err, result) => {
142
- if (result) {
143
- this.debug(`icon ${modelDesc?.icon || 'unknown icon'} found - no copy needed`);
144
112
  return;
145
113
  }
146
- // try 3 options for source file
147
- let src = srcIcon; // as given
148
- const locations=[];
149
- if (!fs.existsSync(src)) {
150
- src = path.normalize(this.adapter.expandFileName(src));
151
- locations.push(src);
152
- } // assumed relative to data folder
153
- if (!fs.existsSync(src)) {
154
- locations.push(src);
155
- src = path.normalize(this.adapter.expandFileName(path.basename(src)));
156
- } // filename relative to data folder
157
- if (!fs.existsSync(src)) {
158
- locations.push(src);
159
- src = path.normalize(this.adapter.expandFileName(path.join('img',path.basename(src))));
160
- } // img/<filename> relative to data folder
161
- if (!fs.existsSync(src)) {
162
- this.warn(`Unable to copy icon from device definition, looked at ${locations.join(', ')} and ${src}`);
114
+ // source is inline basee64F
115
+ const base64Match = srcIcon.match(/data:image\/(.+);base64,/);
116
+ if (base64Match) {
117
+ this.warn(`base 64 Icon matched, trying to save it to disk as ${pathToAdminIcon}`);
118
+ if (modelDesc) modelDesc.icon = pathToAdminIcon;
119
+ this.adapter.fileExists(namespace, pathToAdminIcon, async (err,result) => {
120
+ if (result) {
121
+ this.info(`no need to save icon to ${pathToAdminIcon}`);
122
+ return;
123
+ }
124
+ const msg = `Saving base64 Data to ${pathToAdminIcon}`
125
+ try {
126
+ const buffer = Buffer.from(srcIcon.replace(base64Match[0],''), 'base64');
127
+ this.adapter.writeFile(namespace, pathToAdminIcon, buffer, (err) => {
128
+ if (err) {
129
+ this.warn(`${msg} -- failed: ${err?.message || 'no reason given'}`);
130
+ return;
131
+ }
132
+ this.debug(`${msg} -- success`);
133
+ });
134
+ }
135
+ catch (err) {
136
+ this.warn(`${msg} -- failed: ${err?.message || 'no reason given'}`)
137
+ }
138
+ });
163
139
  return;
164
140
  }
165
- fs.readFile(src, (err, data) => {
166
- if (err) {
167
- this.warn(`unable to read ${src}: ${(err.message? err.message:' no message given')}`);
141
+ // path is absolute
142
+ if (modelDesc) modelDesc.icon = pathToAdminIcon;
143
+ this.adapter.fileExists(namespace, pathToAdminIcon, async(err, result) => {
144
+ if (result) {
145
+ this.debug(`icon ${modelDesc?.icon || 'unknown icon'} found - no copy needed`);
168
146
  return;
169
147
  }
170
- if (data) {
171
- this.adapter.writeFile(namespace, pathToAdminIcon, data, (err) => {
172
- if (err) {
173
- this.error(`error writing file ${path}: ${err.message ? err.message : 'no reason given'}`);
174
- return;
175
- }
176
- this.debug('Updated image file ' + pathToAdminIcon);
177
- });
148
+ // try 3 options for source file
149
+ let src = srcIcon; // as given
150
+ const locations=[];
151
+ if (!fs.existsSync(src)) {
152
+ src = path.normalize(this.adapter.expandFileName(src));
153
+ locations.push(src);
154
+ } // assumed relative to data folder
155
+ if (!fs.existsSync(src)) {
156
+ locations.push(src);
157
+ src = path.normalize(this.adapter.expandFileName(path.basename(src)));
158
+ } // filename relative to data folder
159
+ if (!fs.existsSync(src)) {
160
+ locations.push(src);
161
+ src = path.normalize(this.adapter.expandFileName(path.join('img',path.basename(src))));
162
+ } // img/<filename> relative to data folder
163
+ if (!fs.existsSync(src)) {
164
+ this.warn(`Unable to copy icon from device definition, looked at ${locations.join(', ')} and ${src}`);
178
165
  return;
179
166
  }
180
- this.error(`fs.readFile failed - neither error nor data is returned!`);
167
+ fs.readFile(src, (err, data) => {
168
+ if (err) {
169
+ this.warn(`unable to read icon from ${src}: ${(err.message? err.message:' no message given')}`);
170
+ return;
171
+ }
172
+ if (data) {
173
+ this.adapter.writeFile(namespace, pathToAdminIcon, data, (err) => {
174
+ if (err) {
175
+ this.error(`error writing file ${path}: ${err.message ? err.message : 'no reason given'}`);
176
+ return;
177
+ }
178
+ this.debug('Updated image file ' + pathToAdminIcon);
179
+ });
180
+ return;
181
+ }
182
+ this.error(`fs.readFile failed - neither error nor data is returned!`);
183
+ });
181
184
  });
182
- });
185
+
186
+ }
187
+
183
188
  return modelDesc;
184
189
 
185
190
  }
@@ -430,13 +435,19 @@ class StatesController extends EventEmitter {
430
435
  }
431
436
 
432
437
  async triggerComposite(_deviceId, stateDesc, interactive) {
433
- const deviceId = (_deviceId.replace('0x', ''));
438
+ const deviceId = typeof _deviceId === 'string' ? _deviceId.replace('0x', '') : `group_${_deviceId}`;
434
439
  const idParts = stateDesc.id.split('.').slice(-2);
435
440
  const key = `${deviceId}.${idParts[0]}`;
436
- const handle = this.compositeData[key]
441
+ if (this.compositeData[key] == undefined)
442
+ this.compositeData[key] = {triggers: [stateDesc.id]};
443
+ else this.compositeData[key].triggers.push(stateDesc.id);
444
+ const handle = this.compositeData[key].handle;
437
445
  const factor = (interactive ? 10 : 1);
438
446
  if (handle) this.adapter.clearTimeout(handle);
439
- this.compositeData[key] = this.adapter.setTimeout(async () => {
447
+ this.compositeData[key].handle = this.adapter.setTimeout(async () => {
448
+ for (const trigger of this.compositeData[key].triggers) {
449
+ this.emit('acknowledge_state', deviceId, undefined, { id:trigger} , undefined );
450
+ }
440
451
  delete this.compositeData[key];
441
452
  const parts = stateDesc.id.split('.');
442
453
  parts.pop();
@@ -699,7 +710,7 @@ class StatesController extends EventEmitter {
699
710
  statename = arr[1];
700
711
  }
701
712
  if (commonStates.find(statedesc => statename === statedesc.id) === undefined &&
702
- devStates.states.find(statedesc => statename === statedesc.id) === undefined
713
+ devStates.states.find(statedesc => statename === statedesc?.id) === undefined
703
714
  ) {
704
715
  this.cleanupRequired |= markOnly;
705
716
  if (state.common.hasOwnProperty('custom') && !force && !markOnly) {
@@ -740,43 +751,70 @@ class StatesController extends EventEmitter {
740
751
  setTimeout(() => this.updateState(dev_id, name, outValue, common), timeout);
741
752
  }
742
753
 
743
- async updateState(devId, name, value, common, force) {
754
+ deepCompare(item1, item2) {
755
+ if (typeof item1 != typeof item2) return false; // different object, definitely different.
756
+ switch (typeof item1) {
757
+ case 'boolean':
758
+ case 'number':
759
+ case 'string': return item1 === item2;
760
+ case 'object':
761
+ if (!item1 || !item2) {
762
+ if (!item1 && !item2) return true;
763
+ return false;
764
+ }
765
+ for (const key of Object.keys(item1)) {
766
+ if (!this.deepCompare(item1[key],item2[key])) return false;
767
+ }
768
+ return true;
769
+ case 'function':
770
+ return true; // functions are always considere equal
771
+ }
772
+ }
773
+
774
+ async updateState(devId, name, value, common) {
744
775
  const new_common = {name: name, color:null};
745
776
  const stateId = devId + '.' + name;
746
- if (common) {
777
+ if (common && typeof common === 'object') {
747
778
  for (const key in common) {
748
779
  if (common[key] !== undefined) new_common[key] = common[key];
749
780
  }
750
781
  }
751
- // check if state exist
782
+
752
783
  const stobj = await this.adapter.getObjectAsync(stateId);
753
- let hasChanges = Boolean(force); // if force is set we always update the state.
754
- if (stobj) {
755
- // update state - not change name and role (user can it changed)
756
- if (!force) {
757
- if (stobj.common.name) {
758
- delete new_common.name;
759
- }
784
+ const type = (stobj?.common?.type) ? stobj.common.type : new_common.type;
785
+ // first check value
786
+ if (value !== undefined && type === 'number') {
787
+ const minval = parseFloat(stobj?.common ? stobj.common.min : new_common.min);
788
+ const maxval = parseFloat(stobj?.common ? stobj.common.max : new_common.max);
789
+ let nval = (typeof value == 'number' ? value : parseFloat(value));
790
+ if (isNaN(nval)) {
791
+ if (minval !== undefined && typeof minval === 'number')
792
+ nval = minval;
793
+ else
794
+ nval = 0;
760
795
  }
761
- // always force allow change of level.color roles.
762
- if ((name.includes('color') || force) && (stobj.common.role != new_common.role)) {
763
- console.info(`${force ? 'forc' : 'allow'}ing role change for ${name}`)
764
- } else delete new_common.role;
765
-
766
- // check whether any common property is different
767
- if (!force) {
768
- if (stobj.common) {
769
- for (const property in new_common) {
770
- if (stobj.common.hasOwnProperty(property)) {
771
- if (stobj.common[property] === new_common[property]) {
772
- delete new_common[property];
773
- } else {
774
- hasChanges = true;
775
- }
796
+ if (nval < minval) {
797
+ new_common.color = '#FF0000'
798
+ value = minval
799
+ this.stashErrors(`${stateId}.min`,`State value for ${stateId} has value "${nval}" less than min "${minval}".`, false );
800
+ } else if (nval > maxval) {
801
+ new_common.color = '#FF0000'
802
+ value = maxval;
803
+ this.stashErrors(`${stateId}.max`,`State value for ${stateId} has value "${nval}" greater than max "${maxval}".`, false );
804
+ }
805
+ else new_common.color = new_common.color ? new_common.color : null;
806
+ }
807
+
808
+ if (stobj) {
809
+ // always update common if common is provided
810
+ if (stobj.common) {
811
+ for (const property in new_common) {
812
+ if (stobj.common[property] != undefined) {
813
+ if (this.deepCompare(stobj.common[property],new_common[property])) {
814
+ delete new_common[property];
776
815
  }
777
816
  }
778
817
  }
779
- else hasChanges = true; // we dont have common => we need common
780
818
  }
781
819
  } else {
782
820
  const matches = stateId.match((/\./g));
@@ -790,42 +828,14 @@ class StatesController extends EventEmitter {
790
828
  await this.adapter.extendObjectAsync(id, {type: 'channel', common: { name:channel}, native:{}})
791
829
  }
792
830
  }
793
- hasChanges = true;
794
831
  }
795
832
 
796
- // first check value
797
- if (value !== undefined) {
798
- const type = stobj ? stobj.common.type : new_common.type;
799
- if (type === 'number') {
800
- const minval = (stobj ? stobj.common.min : new_common.min);
801
- const maxval = (stobj ? stobj.common.max : new_common.max);
802
- let nval = (typeof value == 'number' ? value : parseFloat(value));
803
- if (isNaN(nval)) {
804
- if (minval !== undefined && typeof minval === 'number')
805
- nval = minval;
806
- else
807
- nval = 0;
808
- }
809
- if (typeof minval == 'number' && nval < minval) {
810
- hasChanges = true;
811
- new_common.color = '#FF0000'
812
- value = minval
813
- this.stashErrors(`${stateId}.min`,`State value for ${stateId} has value "${nval}" less than min "${minval}".`, false );
814
- }
815
- if (typeof maxval == 'number' && nval > maxval) {
816
- hasChanges = true;
817
- new_common.color = '#FF0000'
818
- value = maxval;
819
- this.stashErrors(`${stateId}.max`,`State value for ${stateId} has value "${nval}" greater than max "${maxval}".`, false );
820
- }
821
- }
822
- }
823
- //
833
+ if (new_common.color=== null && (stobj?.common?.color === undefined || stobj?.common.color === null)) delete new_common.color;
824
834
  // only change object when any common property has changed
825
- if (hasChanges) {
835
+ if (Object.keys(new_common).length) {
826
836
  this.adapter.extendObject(stateId, {type: 'state', common: new_common, native: {}}, () =>
827
837
  value !== undefined && this.setState_typed(stateId, value, true, stobj ? stobj.common.type : new_common.type));
828
- } else if (value !== undefined) {
838
+ } else if (value !== undefined && stobj) {
829
839
  this.setState_typed(stateId, value, true, stobj.common.type);
830
840
  }
831
841
  }
@@ -1088,7 +1098,7 @@ class StatesController extends EventEmitter {
1088
1098
  }
1089
1099
  }
1090
1100
 
1091
- async syncDevStates(dev, model, force) {
1101
+ async syncDevStates(dev, model) {
1092
1102
  if (this.debugActive) this.debug('synchronizing device states for ' + dev.ieeeAddr + ' (' + model + ')');
1093
1103
  const devId = utils.zbIdorIeeetoAdId(this.adapter, dev.ieeeAddr, false);
1094
1104
  // devId - iobroker device id
@@ -1131,8 +1141,9 @@ class StatesController extends EventEmitter {
1131
1141
  min: statedesc.min,
1132
1142
  max: statedesc.max,
1133
1143
  states: statedesc.states,
1144
+ color: null,
1134
1145
  };
1135
- this.updateState(devId, statedesc.id, undefined, common, force);
1146
+ this.updateState(devId, statedesc.id, undefined, common);
1136
1147
  }
1137
1148
  this.deleteOrphanedDeviceStates(dev.ieeeAddr, model, false, undefined, true);
1138
1149
  }
@@ -1185,6 +1196,7 @@ class StatesController extends EventEmitter {
1185
1196
  role: statedesc.role,
1186
1197
  min: statedesc.min,
1187
1198
  max: statedesc.max,
1199
+ color: null,
1188
1200
  };
1189
1201
 
1190
1202
  if (typeof value === 'object' && value.hasOwnProperty('stateid')) {
@@ -1210,7 +1222,7 @@ class StatesController extends EventEmitter {
1210
1222
  );
1211
1223
  if (has_elevated_debug) this.emit('device_debug', { ID:debugId, data: { flag:'SUCCESS', IO:true }});
1212
1224
  } else {
1213
- this.updateState(devId, stateID, value, common, debugId);
1225
+ this.updateState(devId, stateID, value, common, false);
1214
1226
  if (has_elevated_debug) this.emit('device_debug', { ID:debugId, data: { flag:'SUCCESS', IO:true }});
1215
1227
  }
1216
1228
  }
@@ -1226,6 +1238,7 @@ class StatesController extends EventEmitter {
1226
1238
  const has_elevated_debug = (this.checkDebugDevice(devId) && !payload.hasOwnProperty('msg_from_zigbee'));
1227
1239
 
1228
1240
  const message = `message received '${JSON.stringify(payload)}' from device ${devId} type '${model}'`;
1241
+
1229
1242
  if (has_elevated_debug)
1230
1243
  this.emit('device_debug', { ID:debugId, data: { deviceID: devId, flag:'03', IO:true }, message:message});
1231
1244
  else