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.
package/lib/groups.js CHANGED
@@ -1,9 +1,9 @@
1
1
  'use strict';
2
2
 
3
- const json = require('iobroker.zigbee/lib/json');
3
+ const json = require('./json');
4
4
  const statesMapping = require('./devices');
5
5
  const idRegExp = new RegExp(/group_(\d+)/);
6
-
6
+ const { getZbId , getAdId } = require('./utils');
7
7
 
8
8
 
9
9
  class Groups {
@@ -18,12 +18,17 @@ class Groups {
18
18
  switch (typeof id) {
19
19
  case 'number': return id;
20
20
  case 'string': {
21
- const regexResult = id.match(idRegExp);
22
- if (regexResult) return Number(regexResult[1]);
21
+ if (!id.includes('x')) {
22
+ const numericresult = Number(id);
23
+ if (numericresult) return numericresult;
24
+ const regexResult = id.match(idRegExp);
25
+ if (regexResult) return Number(regexResult[1]);
26
+ }
23
27
  break;
24
28
  }
25
29
  default: return -1;
26
30
  }
31
+ return -1;
27
32
  }
28
33
 
29
34
  static generateGroupID(gnum) {
@@ -33,12 +38,39 @@ class Groups {
33
38
  start(zbController, stController) {
34
39
  this.zbController = zbController;
35
40
  this.stController = stController;
41
+ this.GroupData = { states:{} }; // field to store group members
42
+ /*
43
+ {
44
+ groupid: { info:
45
+ [
46
+ ieee:
47
+ ep:
48
+ epname:
49
+ ], capabilties: [], stateupdate:true, memberupdate:'off'
50
+ }
51
+ groupid: members
52
+ }
53
+ states { 0xabcdef01234567890/1 : {
54
+ id:
55
+ epid:
56
+ groups:[]
57
+ states:[]
58
+ }, states: [state, brightness, ...];
59
+ ]
60
+ }
61
+ */
62
+ this.anyGroupStateUpdate = false;
63
+ this.GroupUpdateIntervalHandle = null;
64
+ this.GroupUpdateQueue = []
65
+ this.zbController.on('published', this.onGroupStatePublished.bind(this));
66
+ this.stController.on('changed', this.onDeviceStateChanged.bind(this));
36
67
  this.syncGroups();
37
68
  }
38
69
 
39
70
  stop() {
40
71
  delete this.zbController;
41
72
  delete this.stController;
73
+ delete this.GroupData;
42
74
  }
43
75
 
44
76
  info(msg) {
@@ -80,6 +112,197 @@ class Groups {
80
112
  }
81
113
  }
82
114
 
115
+ setMaxVal(target, values) {
116
+ this.adapter.setState(target,values.sort()[values.length-1], true);
117
+ }
118
+ setMinVal(target, values) {
119
+ this.adapter.setState(target, values.sort()[0], true);
120
+ }
121
+
122
+ setAvgVal(target, values) {
123
+ let sum = 0;
124
+ let cnt = 0;
125
+ let hasBooleanSrc = false;
126
+ for (const v of values) {
127
+ if (typeof v === 'boolean') {
128
+ sum += v ? 1 : 0;
129
+ hasBooleanSrc = true;
130
+ }
131
+ else if (typeof v === 'number') sum += v;
132
+ else cnt--;
133
+ cnt++;
134
+ }
135
+ if (cnt > 0) {
136
+ if (hasBooleanSrc) this.adapter.setState(target, sum/cnt > 0.49999999, true);
137
+ else this.adapter.setState(target, sum/cnt, true)
138
+ }
139
+ }
140
+
141
+ async onGroupStatePublished(deviceId, model, stateModel, stateList, options, debugId) {
142
+ if (Groups.extractGroupID(deviceId) >= 0) { // the states are group states
143
+ for (const state of stateList) {
144
+ const GroupDataKey =`group_${deviceId}`
145
+ if (state.stateDesc.id === 'memberupdate')
146
+ {
147
+ this.GroupData[GroupDataKey][state.stateDesc.id] = state.value;
148
+ }
149
+ if ( state.stateDesc.id === 'stateupdate') {
150
+ this.GroupData[GroupDataKey][state.stateDesc.id] = state.value;
151
+ this.anyGroupStateUpdate = false;
152
+ for (const item in this.GroupData) {
153
+ if (this.GroupData[item] && this.GroupData[item].hasOwnProperty('stateupdate')) {
154
+ if (this.GroupData[item].stateupdate && this.GroupData[item].stateupdate == 'off') continue;
155
+ this.anyGroupStateUpdate = true;
156
+ break;
157
+ }
158
+ }
159
+ }
160
+ if (state.stateDesc.isCommonState) continue;
161
+ if (this.GroupData[GroupDataKey].memberupdate) this.readGroupMemberStatus(this, { id: deviceId, state:state.stateDesc.id} );
162
+ }
163
+ }
164
+ }
165
+
166
+ async onDeviceStateChanged(deviceId, model, stateModel, stateList, options, debugId) {
167
+ if (Groups.extractGroupID(deviceId) < 0) { // the states are device states
168
+ for (const state of stateList) {
169
+ const GroupDataKey =`group_${deviceId}`
170
+ const sd = state.stateDesc;
171
+ const id = deviceId;
172
+ if (sd.isCommonState || !this.anyGroupStateUpdate) continue // common states are never valid for groups
173
+ const sid = `${this.adapter.namespace}.${deviceId.split('x')[1]}.${state.stateDesc.id}`;
174
+ if (this.GroupData.states[sid]) this.GroupData.states[sid].val = state.value;
175
+
176
+ const affectedStates = this.GroupData.states[sid];
177
+ const targetsByGroup = []
178
+ if (typeof affectedStates == 'object' && affectedStates.targets.length > 0) {
179
+ // find who feeds into these states
180
+ for (const s of affectedStates.targets)
181
+ {
182
+ if (!targetsByGroup.includes(s)) {
183
+ targetsByGroup.push(s);
184
+ }
185
+ }
186
+ for (const target of targetsByGroup) {
187
+ const gid = Groups.generateGroupID(getZbId(target));
188
+ const gData = this.GroupData[gid]
189
+ if (typeof gData == 'object' && gData.hasOwnProperty('stateupdate') && typeof this.GroupData.groupStates == 'object') {
190
+ const method = gData.stateupdate;
191
+ const sources = this.GroupData.groupStates[target]
192
+ const values = [];
193
+ if (typeof sources != 'object' || method === 'off') continue;
194
+ for (const s of sources) {
195
+ const v = await this.getGroupMemberValue(s);
196
+ if (v != undefined) {
197
+ values.push(v);
198
+ }
199
+ }
200
+ if (values.length < 2) {
201
+ if (values.length > 0) this.adapter.setState(target, values[0], true)
202
+ continue;
203
+ }
204
+ switch (method) {
205
+ case 'min':
206
+ this.setMinVal(target, values);
207
+ break;
208
+ case 'max':
209
+ this.setMaxVal(target, values);
210
+ break;
211
+ case 'avg':
212
+ this.setAvgVal(target, values);
213
+ break;
214
+ case 'mat': if (values.filter(c => c == values[0]).length == values.length)
215
+ this.adapter.setState(target, values[0], true);
216
+ break;
217
+ }
218
+ }
219
+
220
+ }
221
+ }
222
+ }
223
+ }
224
+ else {
225
+ for (const state of stateList) {
226
+ const GroupDataKey =`group_${deviceId}`
227
+ if (state.stateDesc.id === 'memberupdate')
228
+ {
229
+ this.GroupData[GroupDataKey][state.stateDesc.id] = state.value;
230
+ }
231
+ if ( state.stateDesc.id === 'stateupdate') {
232
+ this.GroupData[GroupDataKey][state.stateDesc.id] = state.value;
233
+ this.anyGroupStateUpdate = false;
234
+ for (const item in this.GroupData) {
235
+ if (this.GroupData[item] && this.GroupData[item].hasOwnProperty('stateupdate')) {
236
+ if (this.GroupData[item].stateupdate && this.GroupData[item].stateupdate == 'off') continue;
237
+ this.anyGroupStateUpdate = true;
238
+ break;
239
+ }
240
+ }
241
+ }
242
+ }
243
+
244
+ }
245
+ }
246
+
247
+
248
+
249
+ static readables = {
250
+ state: { cluster:6, attributes:['onOff']},
251
+ brightness: { cluster:8, attributes:['currentLevel']},
252
+ colortemp: { cluster:768, attributes:['colorTemperature', 'colorMode']},
253
+ color_temp: { cluster:768, attributes:['colorTemperature', 'colorMode']},
254
+ }
255
+
256
+ async readMemberStatus(member, item, toRead) {
257
+ const entity = await this.zbController.resolveEntity(member.ieee, member.epid);
258
+ if (!entity) return;
259
+ const device = entity.device;
260
+ const endpoint = entity.endpoint;
261
+ const mappedModel = entity.mapped;
262
+ if (!mappedModel) return;
263
+ const converter = mappedModel.toZigbee.find(c => c && (c.key.includes(item.state)));
264
+ const canReadViaConverter = converter && converter.hasOwnProperty('convertGet');
265
+ if (canReadViaConverter) {
266
+ try {
267
+ await converter.convertGet(endpoint, item.state, {device:entity.device});
268
+ } catch (error) {
269
+ this.warn(`reading ${item.state} from ${member.id}${member.ieee ? '/' + member.epid : ''} via convertGet failed with ${error && error.message ? error.message : 'no reason given'}`);
270
+ return {unread:member.device};
271
+ }
272
+ this.warn(`reading ${item.state} from ${member.ieee}${member.ep ? '/' + member.epid : ''} via convertGet succeeded` )
273
+ return {read:member.device};
274
+ }
275
+ if (toRead.cluster) {
276
+ if (device && endpoint) {
277
+ if (entity.endpoint.inputClusters.includes(toRead.cluster)) {
278
+ try {
279
+ const result = await endpoint.read(toRead.cluster, toRead.attributes,{disableDefaultResponse : true });
280
+ this.warn(`readGroupMemberStatus for ${item.state} from ${member.ieee}${member.ep ? '/' + member.epid : ''} resulted in ${JSON.stringify(result)}`);
281
+ }
282
+ catch (error) {
283
+ this.warn(`reading ${item.state} from ${member.ieee}${member.ep ? '/' + member.epid : ''} via endpoint.read with ${toRead.cluster}, ${JSON.stringify(toRead.attributes)} resulted in ${error && error.message ? error.message : 'an unspecified error'}`);
284
+ }
285
+ this.warn(`reading ${item.state} from ${member.ieee}${member.ep ? '/' + member.epid : ''} via endpoint.read with ${toRead.cluster}, ${JSON.stringify(toRead.attributes)} succeeded`);
286
+ }
287
+ else this.warn(`omitting cluster ${toRead.cluster} - not supported`);
288
+ }
289
+ else {
290
+ this.warn(`unable to read ${item.state} from ${member.id}${member.ep ? '/' + member.epid : ''} - unable to resolve the entity`);
291
+ }
292
+ return;
293
+ }
294
+ }
295
+
296
+ async readGroupMemberStatus(obj, item) {
297
+ obj.warn(`rgms with ${JSON.stringify(item)}`);
298
+
299
+ const toRead = Groups.readables[item.state]
300
+ if (toRead) {
301
+ const members = await obj.zbController.getGroupMembersFromController(item.id);
302
+ Promise.all(members.map((m) => this.readMemberStatus(m, item, toRead)))
303
+ }
304
+ }
305
+
83
306
 
84
307
  buildGroupID(id, withInstance) {
85
308
  const parts = [];
@@ -157,16 +380,13 @@ class Groups {
157
380
  this.adapter.sendTo(from, command, {error: 'No device specified'}, callback);
158
381
  }
159
382
  const sysid = devId.replace(this.adapter.namespace + '.', '0x');
160
- // Keeping this for reference. State update or state removal needs to be decided upon
161
- //const id = `${devId}.groups`;
162
- // this.adapter.setState(id, JSON.stringify(groups), true);
163
-
164
- //const current = await this.zbController.getGroupMembersFromController(sysid);
165
383
  const errors = [];
384
+ const GroupsToSync = [];
166
385
  for (const epid in groups) {
167
386
  for (const gpid of groups[epid]) {
168
387
  const gpidn = parseInt(gpid);
169
388
  if (gpidn < 0) {
389
+ GroupsToSync.push(-gpidn);
170
390
  this.debug(`calling removeDevFromGroup with ${sysid}, ${-gpidn}, ${epid}` );
171
391
  const response = await this.zbController.removeDevFromGroup(sysid, (-gpidn), epid);
172
392
  if (response && response.error) {
@@ -175,6 +395,7 @@ class Groups {
175
395
  }
176
396
  const icon = this.stController.getDefaultGroupIcon(-gpidn)
177
397
  } else if (gpidn > 0) {
398
+ GroupsToSync.push(gpidn);
178
399
  this.debug(`calling addDevToGroup with ${sysid}, ${gpidn}, ${epid}` );
179
400
  const response = await this.zbController.addDevToGroup(sysid, (gpidn), epid);
180
401
  if (response && response.error) {
@@ -186,13 +407,13 @@ class Groups {
186
407
  }
187
408
  }
188
409
  }
410
+ this.syncGroups(GroupsToSync);
189
411
  } catch (e) {
190
412
  this.warn('caught error ' + JSON.stringify(e) + ' in updateGroupMembership');
191
413
  this.adapter.sendTo(from, command, {error: e}, callback);
192
414
  return;
193
415
  }
194
416
  //await this.renameGroup(from, command, { name: undefined, id: message.id});
195
- this.syncGroups();
196
417
  this.adapter.sendTo(from, command, {}, callback);
197
418
  }
198
419
 
@@ -239,6 +460,7 @@ class Groups {
239
460
  async deleteGroup(from, command, message) {
240
461
  await this.zbController.removeGroupById(message);
241
462
  await this.stController.deleteObj(`group_${parseInt(message)}`);
463
+ await this.removeGroupMemberStateList(parseInt(message));
242
464
  }
243
465
 
244
466
  async renameGroup(from, command, message) {
@@ -266,58 +488,181 @@ class Groups {
266
488
  }
267
489
  }
268
490
  this.debug(`rename group name ${name}, id ${id}, icon ${icon} remove ${JSON.stringify(message.removeMembers)}`);
269
- const group = await this.adapter.getObjectAsync(id);
270
- if (!group) {
271
- this.debug('group object doesnt exist ')
272
- // assume we have to create the group
273
- this.adapter.setObjectNotExists(id, {
274
- type: 'device',
275
- common: {name: (name ? name : `Group ${message.id}` ), type: 'group', icon: icon},
276
- native: {id}
277
- }, () => {
278
- this.adapter.extendObject(id, {common: {name, type: 'group', icon: icon}});
279
- // create writable states for groups from their devices
280
- for (const stateInd in statesMapping.groupStates) {
281
- if (!statesMapping.groupStates.hasOwnProperty(stateInd)) {
282
- continue;
491
+ this.syncGroups([parseInt(message.id)]);
492
+ }
493
+
494
+ addMissingState(arr, states) {
495
+ if (typeof arr != 'object') arr = [];
496
+ for (const state of [...states]) {
497
+ if (arr.find((candidate) => candidate == state.id)=== undefined) arr.push(state.id)
498
+ }
499
+ return arr;
500
+ }
501
+
502
+ async getGroupMemberCapabilities(members) {
503
+ // const rv = [];
504
+ const rv = this.addMissingState([],statesMapping.commonGroupStates);
505
+ for (const member of members) {
506
+ const entity = await this.zbController.resolveEntity(member.ieee, member.epid);
507
+ if (!entity) continue;
508
+ if (entity.endpoint.inputClusters.includes(6)) { // genOnOff
509
+ this.addMissingState(rv,statesMapping.onOffStates);
510
+ }
511
+ if (entity.endpoint.inputClusters.includes(768)) { //genLightingColorCtrl
512
+ this.addMissingState(rv, statesMapping.lightStatesWithColor);
513
+ } else if (entity.endpoint.inputClusters.includes(8)) { // genLvlControl
514
+ this.addMissingState(rv,statesMapping.lightStates);
515
+ }
516
+ }
517
+ return rv;
518
+ }
519
+
520
+ async removeGroupMemberStateList(numericGroupID, allowedDevices) {
521
+ const groupID = `group_${numericGroupID}`;
522
+ const gd = this.GroupData;
523
+
524
+ const devices = allowedDevices ? allowedDevices : undefined;
525
+
526
+ const trackedStates = gd && gd.states ? gd.states : {};
527
+ const t = this;
528
+ if (!devices && this.GroupData.hasOwnProperty(groupID)) delete this.GroupData[groupID];
529
+
530
+ // remove the ones which are no longer part of the group
531
+ const keys = Object.keys(trackedStates);
532
+
533
+ for (const key of keys) {
534
+ const devId = key.split('.')[2]
535
+ if (devices && devices.includes(getZbId(key))) continue;
536
+ if (trackedStates[key] && typeof trackedStates[key].targets == 'object')
537
+ trackedStates[key].targets = trackedStates[key].targets.filter((c) => !c.includes(groupID));
538
+ else trackedStates[key].targets = [];
539
+ if (trackedStates[key].targets.length < 1) delete trackedStates[key]
540
+ };
541
+
542
+ }
543
+
544
+ async rebuildGroupMemberStateList(numericGroupID, memberInfo) {
545
+ const groupID = `group_${numericGroupID}`;
546
+ const gd = this.GroupData[groupID];
547
+ const t = this;
548
+
549
+ // remove the ones which are no longer part of the group
550
+ const keys = Object.keys(this.GroupData.states);
551
+ this.removeGroupMemberStateList(numericGroupID, memberInfo.members.map((m) => m.ieee));
552
+
553
+ const UpdatableStates = [];
554
+ for (const member of memberInfo.members) {
555
+ const entity = await t.zbController.resolveEntity(member.ieee, member.epid);
556
+ if (!entity) return;
557
+ const device = entity.device;
558
+ const endpoint = entity.endpoint;
559
+ const mappedModel = entity.mapped;
560
+ if (!mappedModel) return;
561
+ const ieeeParts = member.ieee.split('x');
562
+ const devId = ieeeParts.length > 1 ? ieeeParts[1]:ieeeParts[0]
563
+ const devStates = await t.stController.getDevStates(devId, mappedModel.model);
564
+ devStates.states.forEach(state => {
565
+ const key = state.setattr || state.prop || state.id;
566
+ if (key) {
567
+ if (memberInfo.capabilities && memberInfo.capabilities.find(candidate => candidate == key)) {
568
+ if (member.epname && member.epname == state.epname) return;
569
+ UpdatableStates.push({id: `${t.adapter.namespace}.${devId}.${state.id}`, grpid: `${t.adapter.namespace}.${groupID}.${key}`})
283
570
  }
284
- const statedesc = statesMapping.groupStates[stateInd];
285
- const common = {
286
- name: statedesc.name,
287
- type: statedesc.type,
288
- unit: statedesc.unit,
289
- read: statedesc.read,
290
- write: statedesc.write,
291
- icon: statedesc.icon,
292
- role: statedesc.role,
293
- min: statedesc.min,
294
- max: statedesc.max,
295
- };
296
- this.stController.updateState(id, statedesc.id, undefined, common);
297
571
  }
298
- this.stController.storeDeviceName(id, name);
299
- });
572
+ })
573
+
574
+ };
575
+ UpdatableStates.forEach(entry => {
576
+ const ged = this.GroupData.states[entry.id]
577
+ if (ged) {
578
+ if (!ged.targets.includes(entry.grpid))
579
+ ged.targets.push(entry.grpid);
580
+ }
581
+ else
582
+ {
583
+ this.GroupData.states[entry.id] = {val:undefined, targets:[entry.grpid]};
584
+ }
585
+ })
586
+ const gsm = {};
587
+ for (const s in this.GroupData.states) {
588
+ const gds = this.GroupData.states[s]
589
+ if (gds && typeof gds.targets && gds.targets.length > 0) {
590
+ for (const t of gds.targets) {
591
+ if (!gsm.hasOwnProperty(t))
592
+ gsm[t] = [];
593
+ if (!gsm[t].includes(s)) gsm[t].push(s)
594
+ }
595
+ }
300
596
  }
301
- else {
302
- this.debug('group object exists');
303
- this.adapter.extendObject(id, {common: {name, type: 'group', icon: icon}});
597
+ this.GroupData.groupStates = gsm;
598
+ }
599
+
600
+ async getGroupMemberValue(id) {
601
+ if (!this.GroupData.states[id]) return undefined;
602
+ const val = this.GroupData.states[id].val;
603
+ if (val == null || val == undefined) {
604
+ const obj = await this.adapter.getStateAsync(id);
605
+ if (obj) {
606
+ this.GroupData.states[id].val = obj.val;
607
+ return obj.val;
608
+ }
609
+ return undefined;
304
610
  }
611
+ return val;
305
612
  }
306
613
 
307
- async syncGroups() {
614
+ async syncGroups(group_id) {
615
+ const numericGroupIdArray = [];
616
+ if (group_id) group_id.forEach(gid => numericGroupIdArray.push(Groups.extractGroupID(gid)));
617
+ // get all group id's from the database and the respective names from the local overrides (if present)
308
618
  const groups = await this.getGroups();
309
- this.debug('sync Groups called: groups is '+ JSON.stringify(groups))
310
619
  const chain = [];
311
- const usedGroupsIds = [];
312
- let GroupCount = 0;
620
+ const usedGroupsIds = Object.keys(groups);
313
621
  for (const j in groups) {
314
- GroupCount++;
315
- this.debug(`group ${GroupCount} is ${JSON.stringify(j)}`);
622
+ if (numericGroupIdArray.length > 0 && !numericGroupIdArray.includes(Number(j))) continue; // skip groups we didnt ask for
623
+ this.debug(`Analysing group_${JSON.stringify(j)}`);
316
624
  if (groups.hasOwnProperty(j)) {
317
625
  const id = `group_${j}`;
626
+ const members = await this.zbController.getGroupMembersFromController(j);
627
+ const memberInfo = { capabilities: [], members: [] };
628
+ const storedGroupInfo = this.GroupData.hasOwnProperty(id) ? this.GroupData[id] : { capabilities: [], members: [] };
629
+ let GroupMembersChanged = false;
630
+ if (members) for (const member of members) {
631
+ const entity = await this.zbController.resolveEntity(member.ieee, member.epid);
632
+ let epname = undefined;
633
+ if (entity && entity.mapped && entity.mapped.endpoint) {
634
+ const epnames = entity.mapped.endpoint();
635
+ for (const key in epnames) {
636
+ if (epnames[key] == member.epid) {
637
+ epname = key;
638
+ break;
639
+ }
640
+ }
641
+ }
642
+ GroupMembersChanged |= (storedGroupInfo.members.find((obj) => obj.ieee === member.ieee && obj.epid === member.epid) === undefined)
643
+ memberInfo.members.push({ ieee:member.ieee, epid:member.epid, epname: epname });
644
+ const key = `${member.ieee}/${member.epid}`;
645
+ }
646
+ GroupMembersChanged |= (memberInfo.members.length != storedGroupInfo.length);
647
+ this.GroupData[id] = memberInfo;
648
+
649
+ const mu = await this.adapter.getStateAsync(`${this.adapter.namespace}.group_${j}.memberupdate`);
650
+ if (mu) this.GroupData[id].memberupdate = mu.val;
651
+ else this.GroupData[id].memberupdate = false;
652
+ const su = await this.adapter.getStateAsync(`${this.adapter.namespace}.group_${j}.stateupdate`);
653
+ if (su) this.GroupData[id].stateupdate = (typeof su.val == 'string' ? su.val : 'off') ;
654
+ else this.GroupData[id].stateupdate = 'off';
655
+ if (this.GroupData[id].stateupdate != 'off') this.anyGroupStateUpdate = true;
656
+
657
+ if (GroupMembersChanged) {
658
+ memberInfo.capabilities = await this.getGroupMemberCapabilities(memberInfo.members);
659
+ this.rebuildGroupMemberStateList(j, memberInfo);
660
+ }
661
+
318
662
  const name = groups[j];
319
663
  const icon = this.stController.localConfig.IconForId(id, 'group', await this.stController.getDefaultGroupIcon(id));
320
664
  chain.push(new Promise(resolve => {
665
+ const isActive = false;
321
666
  this.adapter.setObjectNotExists(id, {
322
667
  type: 'device',
323
668
  common: {name: name, type: 'group', icon: icon },
@@ -330,23 +675,16 @@ class Groups {
330
675
  continue;
331
676
  }
332
677
  const statedesc = statesMapping.groupStates[stateInd];
333
- const common = {
334
- name: statedesc.name,
335
- type: statedesc.type,
336
- unit: statedesc.unit,
337
- read: statedesc.read,
338
- write: statedesc.write,
339
- icon: statedesc.icon,
340
- role: statedesc.role,
341
- min: statedesc.min,
342
- max: statedesc.max,
343
- };
678
+ const common = {};
679
+ for (const prop in statedesc) common[prop] = statedesc[prop];
680
+ common.color= memberInfo.capabilities.find((candidate) => candidate == statedesc.id) ? null: '#888888';
681
+
344
682
  this.stController.updateState(id, statedesc.id, undefined, common);
345
683
  }
346
684
  resolve();
347
685
  });
348
686
  }));
349
- usedGroupsIds.push(parseInt(j));
687
+ usedGroupsIds.push(j);
350
688
  }
351
689
  }
352
690
  chain.push(new Promise(resolve =>
@@ -355,9 +693,8 @@ class Groups {
355
693
  if (!err) {
356
694
  devices.forEach((dev) => {
357
695
  if (dev.common.type === 'group') {
358
- const groupid = parseInt(dev.native.id);
359
- if (!usedGroupsIds.includes(groupid)) {
360
- this.stController.deleteObj(`group_${groupid}`);
696
+ if ( dev.native.id && !usedGroupsIds.includes(dev.native.id)) {
697
+ this.stController.deleteObj(`group_${dev.native.id}`);
361
698
  }
362
699
  }
363
700
  });
@@ -60,7 +60,7 @@ class localConfig extends EventEmitter {
60
60
  }
61
61
  if (typeof name != 'string' || name.trim().length < 1)
62
62
  {
63
- if (this.localData.hasOwnProperty(id))
63
+ if (this.localData.by_id.hasOwnProperty(id))
64
64
  delete this.localData.by_id[id].name;
65
65
  }
66
66
  else {
@@ -103,6 +103,7 @@ class localConfig extends EventEmitter {
103
103
  else this.localData.by_id[target] = base;
104
104
  }
105
105
  this.info(`Local Data for ${target} is ${JSON.stringify(base)} after update`);
106
+ this.retainData();
106
107
  return true;
107
108
  }
108
109
 
@@ -311,11 +312,21 @@ class localConfig extends EventEmitter {
311
312
  return rv;
312
313
  }
313
314
 
314
- getOptions(dev_id) {
315
+ getOptions(dev_id, model_id) {
316
+ function extractOptions(target, options) {
317
+ if (typeof target != 'object') target = {};
318
+ if (typeof options != 'object') return target;
319
+ Object.keys(options).forEach((option) => target[option] = options[option]);
320
+ return target;
321
+ }
322
+
315
323
  const ld = this.localData.by_id[dev_id];
316
- if (ld === undefined || ld.options === undefined) return {};
317
- this.debug(`getOptions for ${dev_id} : ${JSON.stringify(ld.options)}`);
318
- return ld.options;
324
+ const gd = this.localData.by_model[model_id];
325
+ const rv = {};
326
+ if (gd) extractOptions(rv, gd.options);
327
+ if (ld) extractOptions(rv, ld.options);
328
+ this.debug(`getOptions for ${dev_id} : ${JSON.stringify(rv)}`);
329
+ return rv;
319
330
  }
320
331
 
321
332
  }