iobroker.zigbee 3.2.5 → 3.3.1-alpha.0

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.
@@ -2,8 +2,7 @@
2
2
 
3
3
  const safeJsonStringify = require('./json');
4
4
  const { EventEmitter } = require('events');
5
- const statesMapping = require('./devices');
6
- const { getAdId, getZbId, zbIdorIeeetoAdId } = require('./utils');
5
+ const modelDefinitions = require('./models');
7
6
  const fs = require('fs');
8
7
  const localConfig = require('./localConfig');
9
8
  const path = require('path');
@@ -27,7 +26,12 @@ class StatesController extends EventEmitter {
27
26
  this.debugMessages = { nodevice:{ in:[], out: []} };
28
27
  this.debugActive = true;
29
28
  this.deviceQueryBlock = [];
29
+ this.stashedErrors = { knownUnknownModels : new Set() }
30
30
  this.clearStashedErrors();
31
+ this.commonStates = {};
32
+ for (const s of modelDefinitions.commonStates) {
33
+ this.commonStates[s.prop || s.id] = s;
34
+ }
31
35
  }
32
36
 
33
37
  info(message, data) {
@@ -55,119 +59,129 @@ class StatesController extends EventEmitter {
55
59
  }
56
60
 
57
61
  clearStashedErrors() {
58
- this.stashedErrors = {
59
- errors:{},
60
- unknownModels: {},
61
- hasData:false
62
- };
62
+ this.stashedErrors.errors = {};
63
+ this.stashedErrors.unknownModels = {},
64
+ this.stashedErrors.knownUnknownModels.clear();
65
+ this.stashedErrors.hasData = false;
63
66
  return this.stashedErrors;
64
67
  }
65
68
 
69
+ resetKnownUnknownModels() {
70
+ this.stashedErrors.unknownModels = {};
71
+ this.stashedErrors.knownUnknownModels.clear();
72
+ }
73
+
66
74
  debugMessagesById() {
67
75
  return this.debugMessages;
68
76
  }
69
77
 
78
+ async clearModelDefinitions() {
79
+ await modelDefinitions.clearModelDefinitions();
80
+ }
81
+
70
82
  async AddModelFromHerdsman(device, model) {
71
83
  const namespace = `${this.adapter.name}.admin`;
84
+ const isLegacy = Boolean(this.localConfig.getModelOption(model, 'legacy'));
85
+ if (model === undefined) {
86
+ const dev_name = this.verifyDeviceName(utils.zbIdorIeeetoAdId(this.adapter, device.ieeeAddr, false), model, device.modelID);
87
+ this.warn(`icon ${dev_name} for undefined Device not available. Check your devices.`);
88
+ return undefined;
89
+ }
72
90
 
73
- if (this.localConfig.getOverrideWithTargetAndKey(model, 'options.legacy', true)) {
74
- this.debug('Applying legacy definition for ' + model);
75
- await this.addLegacyDevice(model);
91
+ const existingModelDesc = modelDefinitions.findModel(model, device.ieeeAddr, isLegacy);
92
+ const modelDesc = existingModelDesc ? existingModelDesc : await modelDefinitions.addExposeToDevices(device, model, this);
93
+ if (isLegacy) {
94
+ this.warn(`Trying to applying legacy definition for ${model} - ${modelDesc ? 'success.' : 'failed - no matching legacy model found'}`);
95
+ return;
76
96
  }
77
- else {
78
- this.debug('Generating states from exposes for ' + model);
79
- // download icon if it external and not undefined
80
- if (model === undefined) {
81
- const dev_name = this.verifyDeviceName(utils.zbIdorIeeetoAdId(this.adapter, device.ieeeAddr, false), model, device.modelID);
82
- this.warn(`icon ${dev_name} for undefined Device not available. Check your devices.`);
83
- } else {
84
- const modelDesc = await statesMapping.addExposeToDevices(device, this, model);
85
- const srcIcon = (modelDesc ? modelDesc.icon : '');
86
- const model_modif = model.replace(/\//g, '-');
87
- const pathToAdminIcon = `img/${model_modif}.png`;
88
- // source is a web address
89
- if (srcIcon.startsWith('http')) {
90
- try {
91
- //if (modelDesc) modelDesc.icon = pathToAdminIcon;
92
- this.downloadIconToAdmin(srcIcon, pathToAdminIcon)
93
- } catch (err) {
94
- this.warn(`ERROR : unable to download ${srcIcon}: ${err && err.message ? err.message : 'no reason given'}`);
95
- }
97
+ if (!modelDesc || modelDesc == {}) return;
98
+
99
+ const srcIcon = (modelDesc && modelDesc.icon ? modelDesc.icon : '');
100
+ const model_modif = model.replace(/\//g, '-');
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}`);
96
120
  return;
97
121
  }
98
- // source is inline basee64F
99
- const base64Match = srcIcon.match(/data:image\/(.+);base64,/);
100
- if (base64Match) {
101
- this.warn(`base 64 Icon matched, trying to save it to disk as ${pathToAdminIcon}`);
102
- if (modelDesc) modelDesc.icon = pathToAdminIcon;
103
- this.adapter.fileExists(namespace, pathToAdminIcon, async (err,result) => {
104
- if (result) {
105
- this.warn(`no need to save icon to ${pathToAdminIcon}`);
122
+ const msg = `Saving base64 Data to ${pathToAdminIcon}`
123
+ 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'}`);
106
128
  return;
107
129
  }
108
- const msg = `Saving base64 Data to ${pathToAdminIcon}`
109
- try {
110
- const buffer = Buffer.from(srcIcon.replace(base64Match[0],''), 'base64');
111
- this.adapter.writeFile(namespace, pathToAdminIcon, buffer, (err) => {
112
- if (err) {
113
- this.warn(`${msg} -- failed: ${err && err.message ? err.message : 'no reason given'}`);
114
- return;
115
- }
116
- this.debug(`${msg} -- success`);
117
- });
118
- }
119
- catch (err) {
120
- this.warn(`${msg} -- failed: ${err && err.message ? err.message : 'no reason given'}`)
121
- }
130
+ this.debug(`${msg} -- success`);
122
131
  });
132
+ }
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
+ return;
145
+ }
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}`);
163
+ return;
164
+ }
165
+ fs.readFile(src, (err, data) => {
166
+ if (err) {
167
+ this.warn(`unable to read ${src}: ${(err.message? err.message:' no message given')}`);
123
168
  return;
124
169
  }
125
- // path is absolute
126
- if (modelDesc) modelDesc.icon = pathToAdminIcon;
127
- this.adapter.fileExists(namespace, pathToAdminIcon, async(err, result) => {
128
- if (result) {
129
- this.debug(`icon ${modelDesc ? modelDesc.icon : 'unknown icon'} found - no copy needed`);
130
- return;
131
- }
132
- // try 3 options for source file
133
- let src = srcIcon; // as given
134
- const locations=[];
135
- if (!fs.existsSync(src)) {
136
- src = path.normalize(this.adapter.expandFileName(src));
137
- locations.push(src);
138
- } // assumed relative to data folder
139
- if (!fs.existsSync(src)) {
140
- locations.push(src);
141
- src = path.normalize(this.adapter.expandFileName(path.basename(src)));
142
- } // filename relative to data folder
143
- if (!fs.existsSync(src)) {
144
- locations.push(src);
145
- src = path.normalize(this.adapter.expandFileName(path.join('img',path.basename(src))));
146
- } // img/<filename> relative to data folder
147
- if (!fs.existsSync(src)) {
148
- this.warn(`Unable to copy icon from device definition, looked at ${locations.join(', ')} and ${src}`)
149
- return;
150
- }
151
- fs.readFile(src, (err, data) => {
170
+ if (data) {
171
+ this.adapter.writeFile(namespace, pathToAdminIcon, data, (err) => {
152
172
  if (err) {
153
- this.warn(`unable to read ${src}: ${(err.message? err.message:' no message given')}`);
173
+ this.error(`error writing file ${path}: ${err.message ? err.message : 'no reason given'}`);
154
174
  return;
155
175
  }
156
- if (data) {
157
- this.adapter.writeFile(namespace, pathToAdminIcon, data, (err) => {
158
- if (err) {
159
- this.error(`error writing file ${path}: ${err.message ? err.message : 'no reason given'}`);
160
- return;
161
- }
162
- this.debug('Updated image file ' + pathToAdminIcon);
163
- });
164
- return;
165
- }
166
- this.error(`fs.readFile failed - neither error nor data is returned!`);
176
+ this.debug('Updated image file ' + pathToAdminIcon);
167
177
  });
168
- });
169
- }
170
- }
178
+ return;
179
+ }
180
+ this.error(`fs.readFile failed - neither error nor data is returned!`);
181
+ });
182
+ });
183
+ return modelDesc;
184
+
171
185
  }
172
186
 
173
187
  async updateDebugDevices(debugDevices) {
@@ -255,8 +269,8 @@ class StatesController extends EventEmitter {
255
269
  return;
256
270
  }
257
271
 
258
- const devId = getAdId(this.adapter, id); // iobroker device id
259
- const deviceId = getZbId(id); // zigbee device id
272
+ const devId = utils.getAdId(this.adapter, id); // iobroker device id
273
+ const deviceId = utils.getZbId(id); // zigbee device id
260
274
 
261
275
  if (this.checkDebugDevice(id)) {
262
276
  const message = `User state change of state ${id} with value ${state.val} (ack: ${state.ack}) from ${state.from}`;
@@ -347,18 +361,20 @@ class StatesController extends EventEmitter {
347
361
 
348
362
  async getDevStates(deviceId, model) {
349
363
  try {
350
- let states;
364
+ let states = [];
351
365
  let stateModel;
366
+ // if i know the model is unknown, dont keep searching.
367
+ if (this.stashedErrors.knownUnknownModels.has(model)) return { states: modelDefinitions.commonStates };
352
368
  if (model === 'group') {
353
- states = statesMapping.groupStates;
369
+ states = modelDefinitions.groupStates;
354
370
  } else {
355
- stateModel = statesMapping.findModel(model);
371
+ stateModel = modelDefinitions.findModel(model, utils.adIdtoZbIdorIeee(this.adapter,deviceId), this.localConfig.getModelOption(model, 'legacy', false));
356
372
  if (!stateModel) {
357
373
  // try to get the model from the exposes
358
374
  this.stashUnknownModel(`getDevStates:${deviceId}`,`Model "${model}" not found for Device ${deviceId}`);
359
- states = statesMapping.commonStates;
375
+ states = modelDefinitions.commonStates;
360
376
  } else {
361
- states = stateModel.states;
377
+ states = modelDefinitions.commonStates.concat([...stateModel.states]);
362
378
  }
363
379
  if (typeof states === 'function' && !states.prototype) {
364
380
  const entity = await this.adapter.zbController.resolveEntity(deviceId);
@@ -379,9 +395,15 @@ class StatesController extends EventEmitter {
379
395
  if (!this.stashedErrors.errors.hasOwnProperty(key))
380
396
  {
381
397
  if (error) this.error(msg); else this.warn(msg);
382
- this.stashedErrors.errors[key] = { message:msg, ts:[Date.now()], error:error };
398
+ this.stashedErrors.errors[key] = { message:msg, ts:[Date.now()], count: 1, error:error };
399
+ }
400
+ else {
401
+ if (this.stashedErrors.errors[key].ts.length < 2)
402
+ this.stashedErrors.errors[key].ts.push(Date.now());
403
+ else
404
+ this.stashedErrors.errors[key].ts[1]=Date.now();
405
+ this.stashedErrors.errors[key].count++;
383
406
  }
384
- else this.stashedErrors.errors[key].ts.push(Date.now());
385
407
  this.stashedErrors.hasData = true;
386
408
  this.adapter.setState('info.lasterror', JSON.stringify({ error: key, data:this.stashedErrors.errors[key]}), true)
387
409
  }
@@ -391,14 +413,20 @@ class StatesController extends EventEmitter {
391
413
  stashUnknownModel(model, msg) {
392
414
  try {
393
415
  if (!this.stashedErrors.unknownModels.hasOwnProperty(model)) {
394
- this.stashedErrors.unknownModels[model] = { message:msg, ts:[Date.now()], error:true };
416
+ this.stashedErrors.unknownModels[model] = { message:msg, ts:[Date.now()], count: 1, error:true };
395
417
  this.error(`Unknown ${model}: ${msg}`)
396
418
  }
397
- else this.stashedErrors.unknownModels[model].ts.push(Date.now());
419
+ else {
420
+ if (this.stashedErrors.unknownModels[model].count == 1) this.stashedErrors.unknownModels[model].ts.push(Date.now());
421
+ else
422
+ this.stashedErrors.unknownModels[model].ts[1] = Date.now();
423
+ this.stashedErrors.unknownModels[model].count++;
424
+ }
398
425
  this.stashedErrors.hasData = true;
399
426
  this.adapter.setState('info.lasterror', JSON.stringify({ model: model, data:this.stashedErrors.unknownModels[model]}), true)
427
+ return this.stashedErrors.unknownModels[model].count;
400
428
  }
401
- catch { /* */ }
429
+ catch { return -1 }
402
430
  }
403
431
 
404
432
  async triggerComposite(_deviceId, stateDesc, interactive) {
@@ -468,11 +496,11 @@ class StatesController extends EventEmitter {
468
496
  }
469
497
 
470
498
  async publishFromStates(deviceId, model, stateIDs, options, debugID) {
471
- const adId = zbIdorIeeetoAdId(this.adapter, deviceId, true);
499
+ const adId = utils.zbIdorIeeetoAdId(this.adapter, deviceId, true);
472
500
  for (const stateID of Object.keys(stateIDs).sort((a,b) => {if (a=='device_query' || a=='state') return (b=='device_query' ? -1 : 1); else return a.localeCompare(b)})) {
473
501
  const state = await this.adapter.getStateAsync(`${adId}.${stateID}`);
474
- if (stateIDs[stateID]!= null) state.val = stateIDs[stateID];
475
502
  if (state && state.hasOwnProperty('val')) {
503
+ if (stateIDs[stateID]!= null) state.val = stateIDs[stateID];
476
504
  await this.publishFromState(deviceId, model, stateID, state, options, debugID)
477
505
  }
478
506
  }
@@ -496,8 +524,7 @@ class StatesController extends EventEmitter {
496
524
  }
497
525
  return;
498
526
  }
499
- const commonStates = statesMapping.commonStates.find(statedesc => stateKey === statedesc.id);
500
- const stateDesc = (commonStates === undefined ? devStates.states.find(statedesc => stateKey === statedesc.id) : commonStates);
527
+ const stateDesc = devStates.states.find(statedesc => stateKey === statedesc.id);
501
528
  const stateModel = devStates.stateModel;
502
529
  if (!stateDesc) {
503
530
  const message = (`No state available for '${model}' with key '${stateKey}'`);
@@ -616,6 +643,12 @@ class StatesController extends EventEmitter {
616
643
  this.adapter.extendObject(id, {common: {deactivated: inActive, color:inActive ? '#888888' : null, statusStates: inActive ? null : {onlineId:`${id}.available`} }});
617
644
  }
618
645
 
646
+ async getDeviceActivated(id) {
647
+
648
+ const obj = await this.adapter.getObject(id);
649
+ return (obj?.common?.deactivated === false);
650
+ }
651
+
619
652
  storeDeviceName(id, name) {
620
653
  return this.localConfig.updateDeviceName(id, name);
621
654
  }
@@ -625,9 +658,10 @@ class StatesController extends EventEmitter {
625
658
  const options = { recursive:true };
626
659
  try {
627
660
  await this.adapter.delObjectAsync(devId,options);
661
+ return {status:true};
628
662
  } catch (error) {
629
663
  this.adapter.log.warn(`Cannot delete Object ${devId}: ${error && error.message ? error.message : 'without error message'}`);
630
- return `Cannot delete Object ${devId}: ${error && error.message ? error.message : 'without error message'}`;
664
+ return { status:false, message: `Cannot delete Object ${devId}: ${error && error.message ? error.message : 'without error message'}`};
631
665
  }
632
666
  }
633
667
 
@@ -647,7 +681,7 @@ class StatesController extends EventEmitter {
647
681
 
648
682
  async deleteOrphanedDeviceStates(ieeeAddr, model, force, callback, markOnly) {
649
683
  const devStates = await this.getDevStates(ieeeAddr, model);
650
- const commonStates = statesMapping.commonStates;
684
+ const commonStates = modelDefinitions.commonStates || [];
651
685
  const devId = utils.zbIdorIeeetoAdId(this.adapter, ieeeAddr, false);
652
686
  const messages = [];
653
687
  this.adapter.getStatesOf(devId, (err, states) => {
@@ -706,101 +740,93 @@ class StatesController extends EventEmitter {
706
740
  setTimeout(() => this.updateState(dev_id, name, outValue, common), timeout);
707
741
  }
708
742
 
709
- async updateState(devId, name, value, common) {
710
- const obj = await this.adapter.getObjectAsync(devId)
711
- if (obj) {
712
- if (!obj.common.deactivated) {
713
- const new_common = {name: name, color:null};
714
- const stateId = devId + '.' + name;
715
- const new_name = obj.common.name;
716
- if (common) {
717
- for (const key in common) {
718
- if (common[key] !== undefined) new_common[key] = common[key];
719
- }
743
+ async updateState(devId, name, value, common, force) {
744
+ const new_common = {name: name, color:null};
745
+ const stateId = devId + '.' + name;
746
+ if (common) {
747
+ for (const key in common) {
748
+ if (common[key] !== undefined) new_common[key] = common[key];
749
+ }
750
+ }
751
+ // check if state exist
752
+ 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;
720
759
  }
721
- // check if state exist
722
- const stobj = await this.adapter.getObjectAsync(stateId);
723
- let hasChanges = false;
724
- if (stobj) {
725
- // update state - not change name and role (user can it changed)
726
- if (stobj.common.name) {
727
- delete new_common.name;
728
- } else {
729
- new_common.name = `${new_name} ${new_common.name}`;
730
- }
731
- // force allow change of level.color roles.
732
- if (name.includes('color')) {
733
- console.warn(`allowing role change for ${name}`)
734
- } else delete new_common.role;
735
-
736
- // check whether any common property is different
737
- if (stobj.common) {
738
- for (const property in new_common) {
739
- if (stobj.common.hasOwnProperty(property)) {
740
- if (stobj.common[property] === new_common[property]) {
741
- delete new_common[property];
742
- } else {
743
- hasChanges = true;
744
- }
760
+ }
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;
745
775
  }
746
776
  }
747
777
  }
748
- } else {
749
- const matches = stateId.match((/\./g));
750
- if (matches && matches.length>1) {
751
- const channels = stateId.split('.');
752
- const SubChannels = [channels.shift()];
753
- channels.pop();
754
- for (const channel of channels) {
755
- SubChannels.push(channel);
756
- const id = SubChannels.join('.');
757
- await this.adapter.extendObjectAsync(id, {type: 'channel', common: { name:channel}, native:{}})
758
- }
759
- }
760
- hasChanges = true;
761
778
  }
779
+ else hasChanges = true; // we dont have common => we need common
780
+ }
781
+ } else {
782
+ const matches = stateId.match((/\./g));
783
+ if (matches && matches.length>1) {
784
+ const channels = stateId.split('.');
785
+ const SubChannels = [channels.shift()];
786
+ channels.pop();
787
+ for (const channel of channels) {
788
+ SubChannels.push(channel);
789
+ const id = SubChannels.join('.');
790
+ await this.adapter.extendObjectAsync(id, {type: 'channel', common: { name:channel}, native:{}})
791
+ }
792
+ }
793
+ hasChanges = true;
794
+ }
762
795
 
763
- // only change object when any common property has changed
764
- // first check value
765
- if (value !== undefined) {
766
- const type = stobj ? stobj.common.type : new_common.type;
767
- if (type === 'number') {
768
- const minval = (stobj ? stobj.common.min : new_common.min);
769
- const maxval = (stobj ? stobj.common.max : new_common.max);
770
- let nval = (typeof value == 'number' ? value : parseFloat(value));
771
- if (isNaN(nval)) {
772
- if (minval !== undefined && typeof minval === 'number')
773
- nval = minval;
774
- else
775
- nval = 0;
776
- }
777
- if (typeof minval == 'number' && nval < minval) {
778
- hasChanges = true;
779
- new_common.color = '#FF0000'
780
- value = minval
781
- this.stashErrors(`${stateId}.min`,`State value for ${stateId} has value "${nval}" less than min "${minval}".`, false );
782
- }
783
- if (typeof maxval == 'number' && nval > maxval) {
784
- hasChanges = true;
785
- hasChanges = true;
786
- new_common.color = '#FF0000'
787
- value = maxval;
788
- this.stashErrors(`${stateId}.max`,`State value for ${stateId} has value "${nval}" greater than max "${maxval}".`, false );
789
- }
790
- }
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;
791
808
  }
792
- //
793
- if (hasChanges) {
794
- this.adapter.extendObject(stateId, {type: 'state', common: new_common, native: {}}, () =>
795
- value !== undefined && this.setState_typed(stateId, value, true, stobj ? stobj.common.type : new_common.type));
796
- } else if (value !== undefined) {
797
- this.setState_typed(stateId, value, true, stobj.common.type);
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 );
798
820
  }
799
- } else {
800
- if (this.debugActive) this.debug(`UpdateState: Device is deactivated ${devId} ${JSON.stringify(obj)}`);
801
821
  }
802
- } else {
803
- if (this.debugActive) this.debug(`UpdateState: missing device ${devId}`);
822
+ }
823
+ //
824
+ // only change object when any common property has changed
825
+ if (hasChanges) {
826
+ this.adapter.extendObject(stateId, {type: 'state', common: new_common, native: {}}, () =>
827
+ value !== undefined && this.setState_typed(stateId, value, true, stobj ? stobj.common.type : new_common.type));
828
+ } else if (value !== undefined) {
829
+ this.setState_typed(stateId, value, true, stobj.common.type);
804
830
  }
805
831
  }
806
832
 
@@ -851,21 +877,10 @@ class StatesController extends EventEmitter {
851
877
  this.adapter.setState(id, value, ack, callback);
852
878
  }
853
879
 
854
- async applyLegacyDevices() {
855
- const legacyModels = await this.localConfig.getLegacyModels();
856
- const modelarr1 = [];
857
- statesMapping.devices.forEach(item => modelarr1.push(item.models));
858
- statesMapping.setLegacyDevices(legacyModels);
859
- const modelarr2 = [];
860
- statesMapping.devices.forEach(item => modelarr2.push(item.models));
861
- }
862
-
863
880
  async addLegacyDevice(model) {
864
- statesMapping.setLegacyDevices([model]);
865
- statesMapping.getByModel();
881
+ return await modelDefinitions.findModel(model, undefined, undefined, true);
866
882
  }
867
883
 
868
-
869
884
  async getDefaultGroupIcon(id, members) {
870
885
  let groupID = Number(id);
871
886
  if (typeof id == 'string' && isNaN(groupID)) {
@@ -891,7 +906,10 @@ class StatesController extends EventEmitter {
891
906
  const __dev_name = this.verifyDeviceName(dev_id, model, (dev_name ? dev_name : model));
892
907
  if (this.debugActive) this.debug(`UpdateDev called with ${dev_id}, ${dev_name}, ${model}, ${__dev_name}`);
893
908
  const id = '' + dev_id;
894
- const modelDesc = statesMapping.findModel(model);
909
+ const objId = model=='group' ? `group_${dev_id}` : dev_id;
910
+ const obj = await this.adapter.getObjectAsync( objId);
911
+ this.adapter.zbController.setDeviceEnable(dev_id, (obj?.common?.disabled == true));
912
+ const modelDesc = await modelDefinitions.findModel(model, `0x${dev_id}`, this.localConfig.getModelOption(model, 'legacy'));
895
913
  const modelIcon = (model == 'group' ?
896
914
  await this.getDefaultGroupIcon(dev_id) :
897
915
  modelDesc && modelDesc.icon ? modelDesc.icon : 'img/unknown.png');
@@ -914,9 +932,6 @@ class StatesController extends EventEmitter {
914
932
  }
915
933
  }
916
934
  }
917
- const objId = model=='group' ? `group_${dev_id}` : dev_id;
918
-
919
- const obj = await this.adapter.getObjectAsync( objId);
920
935
 
921
936
  const myCommon = {
922
937
  name: __dev_name,
@@ -1073,7 +1088,7 @@ class StatesController extends EventEmitter {
1073
1088
  }
1074
1089
  }
1075
1090
 
1076
- async syncDevStates(dev, model) {
1091
+ async syncDevStates(dev, model, force) {
1077
1092
  if (this.debugActive) this.debug('synchronizing device states for ' + dev.ieeeAddr + ' (' + model + ')');
1078
1093
  const devId = utils.zbIdorIeeetoAdId(this.adapter, dev.ieeeAddr, false);
1079
1094
  // devId - iobroker device id
@@ -1081,7 +1096,7 @@ class StatesController extends EventEmitter {
1081
1096
  if (!devStates) {
1082
1097
  return;
1083
1098
  }
1084
- const states = statesMapping.commonStates.concat(devStates.states);
1099
+ const states = devStates.states;
1085
1100
 
1086
1101
  for (const stateInd in states) {
1087
1102
  if (!states.hasOwnProperty(stateInd)) {
@@ -1094,10 +1109,11 @@ class StatesController extends EventEmitter {
1094
1109
  return;
1095
1110
  }
1096
1111
  // Filter out non routers or devices that are battery driven for the availability flag
1097
- if (statedesc.id === 'available') {
1098
- if (!(dev.type === 'Router') || dev.powerSource === 'Battery') {
1112
+ if (statedesc.id === 'available' && dev.type != 'Router' && dev.type != 'EndDevice') {
1113
+ /* if (!(dev.type === 'Router') || dev.powerSource === 'Battery') {
1099
1114
  continue;
1100
- }
1115
+ } */
1116
+ continue;
1101
1117
  }
1102
1118
  // lazy states
1103
1119
  if (statedesc.lazy) {
@@ -1116,22 +1132,11 @@ class StatesController extends EventEmitter {
1116
1132
  max: statedesc.max,
1117
1133
  states: statedesc.states,
1118
1134
  };
1119
- this.updateState(devId, statedesc.id, undefined, common);
1135
+ this.updateState(devId, statedesc.id, undefined, common, force);
1120
1136
  }
1121
1137
  this.deleteOrphanedDeviceStates(dev.ieeeAddr, model, false, undefined, true);
1122
1138
  }
1123
1139
 
1124
- async getExposes() {
1125
- await this.localConfig.init();
1126
- await this.applyLegacyDevices();
1127
- try {
1128
- statesMapping.fillStatesWithExposes(this);
1129
- }
1130
- catch (error) {
1131
- this.error(`Error applying exposes: ${error && error.message ? error.message : 'no error message'} ${error && error.stack ? error.stack : ''}`);
1132
- }
1133
- }
1134
-
1135
1140
  async elevatedMessage(device, message, isError) {
1136
1141
  if (isError) this.error(message); else this.warn(message);
1137
1142
  // emit data here for debug tab later
@@ -1143,6 +1148,76 @@ class StatesController extends EventEmitter {
1143
1148
  this.emit('debugmessage', {id: id, message:message});
1144
1149
  }
1145
1150
 
1151
+ async publishToDefinedState(devId, model, statedesc, payload, debugId) {
1152
+ const has_elevated_debug = (this.checkDebugDevice(devId) && !payload.hasOwnProperty('msg_from_zigbee'));
1153
+ let value = undefined;
1154
+ let mode = 'prop/id'
1155
+ if (statedesc.getter) {
1156
+ value = statedesc.getter(payload);
1157
+ mode = 'getter'
1158
+ } else {
1159
+ value = payload[statedesc.prop];
1160
+ if (value === undefined || value === null) value = payload[statedesc.id];
1161
+ }
1162
+
1163
+ let stateID = statedesc.id;
1164
+
1165
+ // checking value
1166
+ if (value === undefined || value === null) {
1167
+ const message = `value '${JSON.stringify(value)}' from device ${devId} via ${mode} for '${statedesc.name} (ID ${statedesc.id} ${statedesc.prop ? 'with property '+statedesc.prop : ''})`;
1168
+ if (has_elevated_debug) this.emit('device_debug', { ID:debugId, data: { states:[{id:stateID, value:value, payload:payload }],flag:'04', IO:true }, message});
1169
+ else if (this.debugActive) this.debug(message);
1170
+ return false;
1171
+ }
1172
+
1173
+
1174
+ const message = `value generated '${JSON.stringify(value)}' from device ${devId} for '${statedesc.name}'`;
1175
+ if (has_elevated_debug) this.emit('device_debug', { ID:debugId, data: { states:[{id:stateID, value:value, payload:payload }],flag:'04', IO:true }, message});
1176
+ else if (this.debugActive) this.debug(message);
1177
+
1178
+ const common = {
1179
+ name: statedesc.name,
1180
+ type: statedesc.type,
1181
+ unit: statedesc.unit,
1182
+ read: statedesc.read,
1183
+ write: statedesc.write,
1184
+ icon: statedesc.icon,
1185
+ role: statedesc.role,
1186
+ min: statedesc.min,
1187
+ max: statedesc.max,
1188
+ };
1189
+
1190
+ if (typeof value === 'object' && value.hasOwnProperty('stateid')) {
1191
+ stateID = `${stateID}.${value.stateid}`;
1192
+ if (value.hasOwnProperty('unit')) {
1193
+ common.unit = value.unit;
1194
+ }
1195
+ common.name = value.name ? value.name : value.stateid;
1196
+ common.role = value.role ? `value.${value.role}` : 'number';
1197
+ value = value.value;
1198
+ }
1199
+
1200
+ // if needs to return value to back after timeout
1201
+ if (statedesc.isEvent) {
1202
+ this.updateStateWithTimeout(devId, statedesc.id, value, common, 300, (typeof value == typeof (!value) ? !value : ''));
1203
+ if (has_elevated_debug) this.emit('device_debug', { ID:debugId, data: { flag:'SUCCESS', IO:true }});
1204
+
1205
+ } else {
1206
+ if (statedesc.prepublish) {
1207
+ this.collectOptions(devId, model, false, options =>
1208
+ statedesc.prepublish(devId, value, newvalue => {
1209
+ this.updateState(devId, stateID, newvalue, common) }, options)
1210
+ );
1211
+ if (has_elevated_debug) this.emit('device_debug', { ID:debugId, data: { flag:'SUCCESS', IO:true }});
1212
+ } else {
1213
+ this.updateState(devId, stateID, value, common, debugId);
1214
+ if (has_elevated_debug) this.emit('device_debug', { ID:debugId, data: { flag:'SUCCESS', IO:true }});
1215
+ }
1216
+ }
1217
+ return true;
1218
+
1219
+ }
1220
+
1146
1221
  async publishToState(devId, model, payload, debugId) {
1147
1222
  try {
1148
1223
  if (!debugId) debugId = Date.now();
@@ -1165,70 +1240,12 @@ class StatesController extends EventEmitter {
1165
1240
  let has_published = false;
1166
1241
  if (devStates.states !== undefined) {
1167
1242
  try {
1168
- const states = statesMapping.commonStates.concat(
1169
- devStates.states.filter(statedesc => payload.hasOwnProperty(statedesc.prop || statedesc.id))
1243
+ const states = devStates.states.filter(statedesc => payload.hasOwnProperty(statedesc.prop || statedesc.id)
1170
1244
  );
1171
1245
 
1172
1246
  for (const stateInd in states) {
1173
1247
  const statedesc = states[stateInd];
1174
- let value;
1175
- if (statedesc.getter) {
1176
- value = statedesc.getter(payload);
1177
- } else {
1178
- value = payload[statedesc.prop || statedesc.id];
1179
- }
1180
-
1181
- // checking value
1182
- if (value === undefined || value === null) {
1183
- continue;
1184
- }
1185
-
1186
- let stateID = statedesc.id;
1187
- const message = `value generated '${JSON.stringify(value)}' from device ${devId} for '${statedesc.name}'`;
1188
- if (has_elevated_debug) this.emit('device_debug', { ID:debugId, data: { states:[{id:stateID, value:value, payload:payload }],flag:'04', IO:true }, message});
1189
- else if (this.debugActive) this.debug(message);
1190
-
1191
-
1192
- const common = {
1193
- name: statedesc.name,
1194
- type: statedesc.type,
1195
- unit: statedesc.unit,
1196
- read: statedesc.read,
1197
- write: statedesc.write,
1198
- icon: statedesc.icon,
1199
- role: statedesc.role,
1200
- min: statedesc.min,
1201
- max: statedesc.max,
1202
- };
1203
-
1204
- if (typeof value === 'object' && value.hasOwnProperty('stateid')) {
1205
- stateID = `${stateID}.${value.stateid}`;
1206
- if (value.hasOwnProperty('unit')) {
1207
- common.unit = value.unit;
1208
- }
1209
- common.name = value.name ? value.name : value.stateid;
1210
- common.role = value.role ? `value.${value.role}` : 'number';
1211
- value = value.value;
1212
- }
1213
-
1214
- // if needs to return value to back after timeout
1215
- if (statedesc.isEvent) {
1216
- this.updateStateWithTimeout(devId, statedesc.id, value, common, 300, (typeof value == typeof (!value) ? !value : ''));
1217
- if (has_elevated_debug) this.emit('device_debug', { ID:debugId, data: { flag:'SUCCESS', IO:true }});
1218
-
1219
- } else {
1220
- if (statedesc.prepublish) {
1221
- this.collectOptions(devId, model, false, options =>
1222
- statedesc.prepublish(devId, value, newvalue => {
1223
- this.updateState(devId, stateID, newvalue, common) }, options)
1224
- );
1225
- if (has_elevated_debug) this.emit('device_debug', { ID:debugId, data: { flag:'SUCCESS', IO:true }});
1226
- } else {
1227
- this.updateState(devId, stateID, value, common, debugId);
1228
- if (has_elevated_debug) this.emit('device_debug', { ID:debugId, data: { flag:'SUCCESS', IO:true }});
1229
- }
1230
- }
1231
- has_published = true;
1248
+ has_published |= await this.publishToDefinedState(devId, model, statedesc, payload, debugId);
1232
1249
  }
1233
1250
  } catch (e) {
1234
1251
  const message = `unable to enumerate states of ${devId} for payload ${JSON.stringify(payload)}, ${(e ? e.name : 'undefined')} (${(e ? e.message : '')}).`;
@@ -1348,7 +1365,7 @@ class StatesController extends EventEmitter {
1348
1365
  const has_elevated_debug = this.checkDebugDevice(devId);
1349
1366
  const debugId = Date.now();
1350
1367
  if (entity.device.interviewing) {
1351
- this.warn(`zigbee event for ${device.ieeeAddr} received during interview!`);
1368
+ this.debug(`zigbee event for ${device.ieeeAddr} received during interview!`);
1352
1369
  return;
1353
1370
  }
1354
1371
 
@@ -1407,19 +1424,17 @@ class StatesController extends EventEmitter {
1407
1424
 
1408
1425
  // always publish link_quality and battery
1409
1426
  if (message.linkquality) { // send battery with
1410
- this.publishToState(devId, model, {linkquality: message.linkquality}, debugId);
1427
+ this.publishToDefinedState(devId, model, this.commonStates.linkquality, {linkquality: message.linkquality}, debugId);
1411
1428
  if (isBattKey) {
1412
- this.publishToState(devId, model, {voltage: _voltage}, debugId);
1413
1429
  const battProz = zigbeeHerdsmanConvertersUtils.batteryVoltageToPercentage(_voltage,entity.mapped.meta.battery.voltageToPercentage);
1414
- this.publishToState(devId, model, {battery: battProz}, debugId);
1430
+ this.publishToState(devId, model, {voltage: _voltage, battery: battProz}, debugId);
1415
1431
  }
1416
1432
  if (isMessure) {
1417
- this.publishToState(devId, model, {temperature: _temperature}, debugId);
1418
- this.publishToState(devId, model, {humidity: _humidity}), debugId;
1433
+ this.publishToState(devId, model, {temperature: _temperature, humidity: _humidity}, debugId);
1419
1434
  }
1420
1435
  }
1421
1436
 
1422
- this.publishToState(devId, model, {msg_from_zigbee: safeJsonStringify(msgForState)}, -1);
1437
+ this.publishToDefinedState(devId, model, this.commonStates.msg_from_zigbee, {msg_from_zigbee: safeJsonStringify(msgForState)}, -1);
1423
1438
 
1424
1439
  if (!entity.mapped) {
1425
1440
  return;