iobroker.zigbee 3.0.5 → 3.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -0
- package/admin/admin.js +475 -230
- package/admin/i18n/de/translations.json +16 -16
- package/admin/index_m.html +84 -91
- package/admin/tab_m.html +38 -16
- package/docs/de/readme.md +1 -1
- package/docs/en/readme.md +4 -2
- package/io-package.json +35 -28
- package/lib/DeviceDebug.js +25 -2
- package/lib/binding.js +8 -8
- package/lib/commands.js +386 -326
- package/lib/developer.js +2 -2
- package/lib/devices.js +13 -9
- package/lib/exclude.js +1 -1
- package/lib/exposes.js +56 -24
- package/lib/groups.js +408 -73
- package/lib/localConfig.js +23 -12
- package/lib/networkmap.js +10 -2
- package/lib/states.js +32 -2
- package/lib/statescontroller.js +361 -209
- package/lib/utils.js +7 -5
- package/lib/zbDelayedAction.js +4 -4
- package/lib/zbDeviceAvailability.js +102 -46
- package/lib/zbDeviceConfigure.js +7 -0
- package/lib/zbDeviceEvent.js +40 -7
- package/lib/zigbeecontroller.js +552 -75
- package/main.js +168 -505
- package/package.json +8 -11
- package/lib/tools.js +0 -55
package/lib/groups.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const json = require('
|
|
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
|
-
|
|
22
|
-
|
|
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,196 @@ 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
|
+
else {
|
|
224
|
+
for (const state of stateList) {
|
|
225
|
+
const GroupDataKey =`group_${deviceId}`
|
|
226
|
+
if (state.stateDesc.id === 'memberupdate')
|
|
227
|
+
{
|
|
228
|
+
this.GroupData[GroupDataKey][state.stateDesc.id] = state.value;
|
|
229
|
+
}
|
|
230
|
+
if ( state.stateDesc.id === 'stateupdate') {
|
|
231
|
+
this.GroupData[GroupDataKey][state.stateDesc.id] = state.value;
|
|
232
|
+
this.anyGroupStateUpdate = false;
|
|
233
|
+
for (const item in this.GroupData) {
|
|
234
|
+
if (this.GroupData[item] && this.GroupData[item].hasOwnProperty('stateupdate')) {
|
|
235
|
+
if (this.GroupData[item].stateupdate && this.GroupData[item].stateupdate == 'off') continue;
|
|
236
|
+
this.anyGroupStateUpdate = true;
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
static readables = {
|
|
246
|
+
state: { cluster:6, attributes:['onOff']},
|
|
247
|
+
brightness: { cluster:8, attributes:['currentLevel']},
|
|
248
|
+
colortemp: { cluster:768, attributes:['colorTemperature', 'colorMode']},
|
|
249
|
+
color_temp: { cluster:768, attributes:['colorTemperature', 'colorMode']},
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async readMemberStatus(member, item, toRead) {
|
|
253
|
+
const entity = await this.zbController.resolveEntity(member.ieee, member.epid);
|
|
254
|
+
if (!entity) return;
|
|
255
|
+
const device = entity.device;
|
|
256
|
+
const endpoint = entity.endpoint;
|
|
257
|
+
const mappedModel = entity.mapped;
|
|
258
|
+
if (!mappedModel) return;
|
|
259
|
+
const obj = await this.adapter.getObjectAsync((member.ieee.includes('x') ? member.ieee.split('x')[1] : member.ieee));
|
|
260
|
+
if (obj && obj.common.deactivated) {
|
|
261
|
+
this.debug(`omitting reading member state for deactivated device ${member.ieee}`);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
const converter = mappedModel.toZigbee.find(c => c && (c.key.includes(item.state)));
|
|
265
|
+
const canReadViaConverter = converter && converter.hasOwnProperty('convertGet');
|
|
266
|
+
if (canReadViaConverter) {
|
|
267
|
+
try {
|
|
268
|
+
await converter.convertGet(endpoint, item.state, {device:entity.device});
|
|
269
|
+
} catch (error) {
|
|
270
|
+
this.debug(`reading ${item.state} from ${member.id}${member.ieee ? '/' + member.epid : ''} via convertGet failed with ${error && error.message ? error.message : 'no reason given'}`);
|
|
271
|
+
return {unread:member.device};
|
|
272
|
+
}
|
|
273
|
+
this.debug(`reading ${item.state} from ${member.ieee}${member.ep ? '/' + member.epid : ''} via convertGet succeeded` );
|
|
274
|
+
return {read:member.device};
|
|
275
|
+
}
|
|
276
|
+
if (toRead.cluster) {
|
|
277
|
+
if (device && endpoint) {
|
|
278
|
+
if (entity.endpoint.inputClusters.includes(toRead.cluster)) {
|
|
279
|
+
try {
|
|
280
|
+
const result = await endpoint.read(toRead.cluster, toRead.attributes,{disableDefaultResponse : true });
|
|
281
|
+
this.debug(`readGroupMemberStatus for ${item.state} from ${member.ieee}${member.ep ? '/' + member.epid : ''} resulted in ${JSON.stringify(result)}`);
|
|
282
|
+
}
|
|
283
|
+
catch (error) {
|
|
284
|
+
this.debug(`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'}`);
|
|
285
|
+
}
|
|
286
|
+
this.debug(`reading ${item.state} from ${member.ieee}${member.ep ? '/' + member.epid : ''} via endpoint.read with ${toRead.cluster}, ${JSON.stringify(toRead.attributes)} succeeded`);
|
|
287
|
+
}
|
|
288
|
+
else this.debug(`omitting cluster ${toRead.cluster} - not supported`);
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
this.debug(`unable to read ${item.state} from ${member.id}${member.ep ? '/' + member.epid : ''} - unable to resolve the entity`);
|
|
292
|
+
}
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
async readGroupMemberStatus(obj, item) {
|
|
298
|
+
const toRead = Groups.readables[item.state]
|
|
299
|
+
if (toRead) {
|
|
300
|
+
const members = await obj.zbController.getGroupMembersFromController(item.id);
|
|
301
|
+
Promise.all(members.map((m) => this.readMemberStatus(m, item, toRead)))
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
83
305
|
|
|
84
306
|
buildGroupID(id, withInstance) {
|
|
85
307
|
const parts = [];
|
|
@@ -157,16 +379,13 @@ class Groups {
|
|
|
157
379
|
this.adapter.sendTo(from, command, {error: 'No device specified'}, callback);
|
|
158
380
|
}
|
|
159
381
|
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
382
|
const errors = [];
|
|
383
|
+
const GroupsToSync = [];
|
|
166
384
|
for (const epid in groups) {
|
|
167
385
|
for (const gpid of groups[epid]) {
|
|
168
386
|
const gpidn = parseInt(gpid);
|
|
169
387
|
if (gpidn < 0) {
|
|
388
|
+
GroupsToSync.push(-gpidn);
|
|
170
389
|
this.debug(`calling removeDevFromGroup with ${sysid}, ${-gpidn}, ${epid}` );
|
|
171
390
|
const response = await this.zbController.removeDevFromGroup(sysid, (-gpidn), epid);
|
|
172
391
|
if (response && response.error) {
|
|
@@ -175,6 +394,7 @@ class Groups {
|
|
|
175
394
|
}
|
|
176
395
|
const icon = this.stController.getDefaultGroupIcon(-gpidn)
|
|
177
396
|
} else if (gpidn > 0) {
|
|
397
|
+
GroupsToSync.push(gpidn);
|
|
178
398
|
this.debug(`calling addDevToGroup with ${sysid}, ${gpidn}, ${epid}` );
|
|
179
399
|
const response = await this.zbController.addDevToGroup(sysid, (gpidn), epid);
|
|
180
400
|
if (response && response.error) {
|
|
@@ -186,13 +406,13 @@ class Groups {
|
|
|
186
406
|
}
|
|
187
407
|
}
|
|
188
408
|
}
|
|
409
|
+
this.syncGroups(GroupsToSync);
|
|
189
410
|
} catch (e) {
|
|
190
411
|
this.warn('caught error ' + JSON.stringify(e) + ' in updateGroupMembership');
|
|
191
412
|
this.adapter.sendTo(from, command, {error: e}, callback);
|
|
192
413
|
return;
|
|
193
414
|
}
|
|
194
415
|
//await this.renameGroup(from, command, { name: undefined, id: message.id});
|
|
195
|
-
this.syncGroups();
|
|
196
416
|
this.adapter.sendTo(from, command, {}, callback);
|
|
197
417
|
}
|
|
198
418
|
|
|
@@ -239,15 +459,15 @@ class Groups {
|
|
|
239
459
|
async deleteGroup(from, command, message) {
|
|
240
460
|
await this.zbController.removeGroupById(message);
|
|
241
461
|
await this.stController.deleteObj(`group_${parseInt(message)}`);
|
|
462
|
+
await this.removeGroupMemberStateList(parseInt(message));
|
|
242
463
|
}
|
|
243
464
|
|
|
244
465
|
async renameGroup(from, command, message) {
|
|
245
466
|
this.debug(`rename group called with ${from}, ${command}, ${JSON.stringify(message)}`);
|
|
246
|
-
// const groupsEntry = await this.adapter.getStateAsync('info.groups');
|
|
247
|
-
// const objGroups = (groupsEntry && groupsEntry.val ? JSON.parse(groupsEntry.val) : {});
|
|
248
|
-
const name = message.name;
|
|
249
467
|
const id = `group_${message.id}`;
|
|
250
|
-
|
|
468
|
+
|
|
469
|
+
this.stController.localConfig.updateDeviceName(id, message.name);
|
|
470
|
+
|
|
251
471
|
try {
|
|
252
472
|
const group = await this.zbController.verifyGroupExists(message.id);
|
|
253
473
|
if (message.remove) {
|
|
@@ -256,97 +476,213 @@ class Groups {
|
|
|
256
476
|
this.debug('trying to remove ' + member.id + (member.ep ? '.'+member.ep : '') + ' ' + ' from group ' + message.id + ' response is '+JSON.stringify(response));
|
|
257
477
|
}
|
|
258
478
|
}
|
|
259
|
-
if (icon.match(/img\/group_\d+\.png/g)) {
|
|
260
|
-
icon = await this.zbController.rebuildGroupIcon(group);
|
|
261
|
-
}
|
|
262
479
|
} catch (e) {
|
|
263
480
|
this.warn('renameGroup caught error ' + (e && e.message ? e.message : 'no message'));
|
|
264
481
|
if (e && e.hasOwnProperty('code')) {
|
|
265
482
|
this.warn(`renameGroup caught error ${JSON.stringify(e.code)}`);
|
|
266
483
|
}
|
|
267
484
|
}
|
|
268
|
-
this.debug(`rename group name ${name}, id ${id},
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
485
|
+
this.debug(`rename group name ${message.name}, id ${id}, remove ${JSON.stringify(message.removeMembers)}`);
|
|
486
|
+
this.syncGroups([parseInt(message.id)]);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
addMissingState(arr, states) {
|
|
490
|
+
if (typeof arr != 'object') arr = [];
|
|
491
|
+
for (const state of [...states]) {
|
|
492
|
+
if (arr.find((candidate) => candidate == state.id)=== undefined) arr.push(state.id)
|
|
493
|
+
}
|
|
494
|
+
return arr;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
async getGroupMemberCapabilities(members) {
|
|
498
|
+
// const rv = [];
|
|
499
|
+
const rv = this.addMissingState([],statesMapping.commonGroupStates);
|
|
500
|
+
for (const member of members) {
|
|
501
|
+
const entity = await this.zbController.resolveEntity(member.ieee, member.epid);
|
|
502
|
+
if (!entity) continue;
|
|
503
|
+
if (entity.endpoint.inputClusters.includes(6)) { // genOnOff
|
|
504
|
+
this.addMissingState(rv,statesMapping.onOffStates);
|
|
505
|
+
}
|
|
506
|
+
if (entity.endpoint.inputClusters.includes(768)) { //genLightingColorCtrl
|
|
507
|
+
this.addMissingState(rv, statesMapping.lightStatesWithColor);
|
|
508
|
+
} else if (entity.endpoint.inputClusters.includes(8)) { // genLvlControl
|
|
509
|
+
this.addMissingState(rv,statesMapping.lightStates);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
return rv;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
async removeGroupMemberStateList(numericGroupID, allowedDevices) {
|
|
516
|
+
const groupID = `group_${numericGroupID}`;
|
|
517
|
+
const gd = this.GroupData;
|
|
518
|
+
|
|
519
|
+
const devices = allowedDevices ? allowedDevices : undefined;
|
|
520
|
+
|
|
521
|
+
const trackedStates = gd && gd.states ? gd.states : {};
|
|
522
|
+
const t = this;
|
|
523
|
+
if (!devices && this.GroupData.hasOwnProperty(groupID)) delete this.GroupData[groupID];
|
|
524
|
+
|
|
525
|
+
// remove the ones which are no longer part of the group
|
|
526
|
+
const keys = Object.keys(trackedStates);
|
|
527
|
+
|
|
528
|
+
for (const key of keys) {
|
|
529
|
+
const devId = key.split('.')[2]
|
|
530
|
+
if (devices && devices.includes(getZbId(key))) continue;
|
|
531
|
+
if (trackedStates[key] && typeof trackedStates[key].targets == 'object')
|
|
532
|
+
trackedStates[key].targets = trackedStates[key].targets.filter((c) => !c.includes(groupID));
|
|
533
|
+
else trackedStates[key].targets = [];
|
|
534
|
+
if (trackedStates[key].targets.length < 1) delete trackedStates[key]
|
|
535
|
+
};
|
|
536
|
+
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
async rebuildGroupMemberStateList(numericGroupID, memberInfo) {
|
|
540
|
+
const groupID = `group_${numericGroupID}`;
|
|
541
|
+
const gd = this.GroupData[groupID];
|
|
542
|
+
const t = this;
|
|
543
|
+
|
|
544
|
+
// remove the ones which are no longer part of the group
|
|
545
|
+
const keys = Object.keys(this.GroupData.states);
|
|
546
|
+
this.removeGroupMemberStateList(numericGroupID, memberInfo.members.map((m) => m.ieee));
|
|
547
|
+
|
|
548
|
+
const UpdatableStates = [];
|
|
549
|
+
for (const member of memberInfo.members) {
|
|
550
|
+
const entity = await t.zbController.resolveEntity(member.ieee, member.epid);
|
|
551
|
+
if (!entity) return;
|
|
552
|
+
const device = entity.device;
|
|
553
|
+
const endpoint = entity.endpoint;
|
|
554
|
+
const mappedModel = entity.mapped;
|
|
555
|
+
if (!mappedModel) return;
|
|
556
|
+
const ieeeParts = member.ieee.split('x');
|
|
557
|
+
const devId = ieeeParts.length > 1 ? ieeeParts[1]:ieeeParts[0]
|
|
558
|
+
const devStates = await t.stController.getDevStates(devId, mappedModel.model);
|
|
559
|
+
devStates.states.forEach(state => {
|
|
560
|
+
const key = state.setattr || state.prop || state.id;
|
|
561
|
+
if (key) {
|
|
562
|
+
if (memberInfo.capabilities && memberInfo.capabilities.find(candidate => candidate == key)) {
|
|
563
|
+
if (member.epname && member.epname == state.epname) return;
|
|
564
|
+
UpdatableStates.push({id: `${t.adapter.namespace}.${devId}.${state.id}`, grpid: `${t.adapter.namespace}.${groupID}.${key}`})
|
|
283
565
|
}
|
|
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
566
|
}
|
|
298
|
-
|
|
299
|
-
|
|
567
|
+
})
|
|
568
|
+
|
|
569
|
+
};
|
|
570
|
+
UpdatableStates.forEach(entry => {
|
|
571
|
+
const ged = this.GroupData.states[entry.id]
|
|
572
|
+
if (ged) {
|
|
573
|
+
if (!ged.targets.includes(entry.grpid))
|
|
574
|
+
ged.targets.push(entry.grpid);
|
|
575
|
+
}
|
|
576
|
+
else
|
|
577
|
+
{
|
|
578
|
+
this.GroupData.states[entry.id] = {val:undefined, targets:[entry.grpid]};
|
|
579
|
+
}
|
|
580
|
+
})
|
|
581
|
+
const gsm = {};
|
|
582
|
+
for (const s in this.GroupData.states) {
|
|
583
|
+
const gds = this.GroupData.states[s]
|
|
584
|
+
if (gds && typeof gds.targets && gds.targets.length > 0) {
|
|
585
|
+
for (const t of gds.targets) {
|
|
586
|
+
if (!gsm.hasOwnProperty(t))
|
|
587
|
+
gsm[t] = [];
|
|
588
|
+
if (!gsm[t].includes(s)) gsm[t].push(s)
|
|
589
|
+
}
|
|
590
|
+
}
|
|
300
591
|
}
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
592
|
+
this.GroupData.groupStates = gsm;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
async getGroupMemberValue(id) {
|
|
596
|
+
if (!this.GroupData.states[id]) return undefined;
|
|
597
|
+
const val = this.GroupData.states[id].val;
|
|
598
|
+
if (val == null || val == undefined) {
|
|
599
|
+
const ai = getAdId(this.adapter, id);
|
|
600
|
+
const obj = await this.adapter.getObjectAsync(ai);
|
|
601
|
+
if (obj && obj.common.deactivated) return undefined;
|
|
602
|
+
const state = await this.adapter.getStateAsync(id);
|
|
603
|
+
if (state) {
|
|
604
|
+
this.GroupData.states[id].val = state.val;
|
|
605
|
+
return state.val;
|
|
606
|
+
}
|
|
607
|
+
return undefined;
|
|
304
608
|
}
|
|
609
|
+
return val;
|
|
305
610
|
}
|
|
306
611
|
|
|
307
|
-
async syncGroups() {
|
|
612
|
+
async syncGroups(group_id) {
|
|
613
|
+
const numericGroupIdArray = [];
|
|
614
|
+
if (group_id) group_id.forEach(gid => numericGroupIdArray.push(Groups.extractGroupID(gid)));
|
|
615
|
+
// get all group id's from the database and the respective names from the local overrides (if present)
|
|
308
616
|
const groups = await this.getGroups();
|
|
309
|
-
this.debug('sync Groups called: groups is '+ JSON.stringify(groups))
|
|
310
617
|
const chain = [];
|
|
311
|
-
const usedGroupsIds = [];
|
|
312
|
-
let GroupCount = 0;
|
|
618
|
+
const usedGroupsIds = numericGroupIdArray.length > 0 ? Object.keys(groups) : [];
|
|
313
619
|
for (const j in groups) {
|
|
314
|
-
|
|
315
|
-
this.debug(`
|
|
620
|
+
if (numericGroupIdArray.length > 0 && !numericGroupIdArray.includes(Number(j))) continue; // skip groups we didnt ask for
|
|
621
|
+
this.debug(`Analysing group_${JSON.stringify(j)}`);
|
|
316
622
|
if (groups.hasOwnProperty(j)) {
|
|
317
623
|
const id = `group_${j}`;
|
|
318
|
-
const
|
|
624
|
+
const members = await this.zbController.getGroupMembersFromController(j);
|
|
625
|
+
const memberInfo = { capabilities: [], members: [] };
|
|
626
|
+
const storedGroupInfo = this.GroupData.hasOwnProperty(id) ? this.GroupData[id] : { capabilities: [], members: [] };
|
|
627
|
+
let GroupMembersChanged = false;
|
|
628
|
+
if (members) for (const member of members) {
|
|
629
|
+
const entity = await this.zbController.resolveEntity(member.ieee, member.epid);
|
|
630
|
+
let epname = undefined;
|
|
631
|
+
if (entity && entity.mapped && entity.mapped.endpoint) {
|
|
632
|
+
const epnames = entity.mapped.endpoint();
|
|
633
|
+
for (const key in epnames) {
|
|
634
|
+
if (epnames[key] == member.epid) {
|
|
635
|
+
epname = key;
|
|
636
|
+
break;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
GroupMembersChanged |= (storedGroupInfo.members.find((obj) => obj.ieee === member.ieee && obj.epid === member.epid) === undefined)
|
|
641
|
+
memberInfo.members.push({ ieee:member.ieee, epid:member.epid, epname: epname });
|
|
642
|
+
const key = `${member.ieee}/${member.epid}`;
|
|
643
|
+
}
|
|
644
|
+
GroupMembersChanged |= (memberInfo.members.length != storedGroupInfo.length);
|
|
645
|
+
this.GroupData[id] = memberInfo;
|
|
646
|
+
|
|
647
|
+
const mu = await this.adapter.getStateAsync(`${this.adapter.namespace}.group_${j}.memberupdate`);
|
|
648
|
+
if (mu) this.GroupData[id].memberupdate = mu.val;
|
|
649
|
+
else this.GroupData[id].memberupdate = false;
|
|
650
|
+
const su = await this.adapter.getStateAsync(`${this.adapter.namespace}.group_${j}.stateupdate`);
|
|
651
|
+
if (su) this.GroupData[id].stateupdate = (typeof su.val == 'string' ? su.val : 'off') ;
|
|
652
|
+
else this.GroupData[id].stateupdate = 'off';
|
|
653
|
+
if (this.GroupData[id].stateupdate != 'off') this.anyGroupStateUpdate = true;
|
|
654
|
+
|
|
655
|
+
if (GroupMembersChanged) {
|
|
656
|
+
memberInfo.capabilities = await this.getGroupMemberCapabilities(memberInfo.members);
|
|
657
|
+
this.rebuildGroupMemberStateList(j, memberInfo);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
const name = this.stController.localConfig.NameForId(id, 'group', groups[j]);
|
|
319
661
|
const icon = this.stController.localConfig.IconForId(id, 'group', await this.stController.getDefaultGroupIcon(id));
|
|
320
662
|
chain.push(new Promise(resolve => {
|
|
663
|
+
const isActive = false;
|
|
321
664
|
this.adapter.setObjectNotExists(id, {
|
|
322
665
|
type: 'device',
|
|
323
666
|
common: {name: name, type: 'group', icon: icon },
|
|
324
667
|
native: {id: j}
|
|
325
668
|
}, () => {
|
|
326
|
-
this.adapter.extendObject(id, {common: {type: 'group', icon: icon}});
|
|
669
|
+
this.adapter.extendObject(id, {common: {name: name, type: 'group', icon: icon}});
|
|
327
670
|
// create writable states for groups from their devices
|
|
328
671
|
for (const stateInd in statesMapping.groupStates) {
|
|
329
672
|
if (!statesMapping.groupStates.hasOwnProperty(stateInd)) {
|
|
330
673
|
continue;
|
|
331
674
|
}
|
|
332
675
|
const statedesc = statesMapping.groupStates[stateInd];
|
|
333
|
-
const common = {
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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
|
-
};
|
|
676
|
+
const common = {};
|
|
677
|
+
for (const prop in statedesc) common[prop] = statedesc[prop];
|
|
678
|
+
common.color= memberInfo.capabilities.find((candidate) => candidate == statedesc.id) ? null: '#888888';
|
|
679
|
+
|
|
344
680
|
this.stController.updateState(id, statedesc.id, undefined, common);
|
|
345
681
|
}
|
|
346
682
|
resolve();
|
|
347
683
|
});
|
|
348
684
|
}));
|
|
349
|
-
usedGroupsIds.push(
|
|
685
|
+
usedGroupsIds.push(j);
|
|
350
686
|
}
|
|
351
687
|
}
|
|
352
688
|
chain.push(new Promise(resolve =>
|
|
@@ -355,9 +691,8 @@ class Groups {
|
|
|
355
691
|
if (!err) {
|
|
356
692
|
devices.forEach((dev) => {
|
|
357
693
|
if (dev.common.type === 'group') {
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
this.stController.deleteObj(`group_${groupid}`);
|
|
694
|
+
if ( dev.native.id && !usedGroupsIds.includes(dev.native.id)) {
|
|
695
|
+
this.stController.deleteObj(`group_${dev.native.id}`);
|
|
361
696
|
}
|
|
362
697
|
}
|
|
363
698
|
});
|