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/README.md +26 -0
- package/admin/admin.js +153 -86
- package/admin/i18n/de/translations.json +16 -16
- package/admin/index_m.html +59 -90
- package/admin/tab_m.html +7 -5
- package/docs/de/readme.md +4 -1
- package/docs/en/readme.md +3 -1
- package/io-package.json +110 -54
- package/lib/binding.js +1 -1
- package/lib/commands.js +129 -97
- package/lib/developer.js +1 -1
- package/lib/devices.js +11 -7
- package/lib/exposes.js +14 -3
- package/lib/groups.js +400 -63
- package/lib/localConfig.js +16 -5
- package/lib/states.js +32 -2
- package/lib/statescontroller.js +254 -146
- package/lib/utils.js +7 -5
- package/lib/zbDeviceAvailability.js +78 -21
- package/lib/zbDeviceEvent.js +1 -1
- package/lib/zigbeecontroller.js +485 -56
- package/main.js +139 -469
- package/package.json +7 -7
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,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
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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
|
-
|
|
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
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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
|
-
|
|
315
|
-
this.debug(`
|
|
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
|
-
|
|
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
|
-
};
|
|
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(
|
|
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
|
-
|
|
359
|
-
|
|
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
|
});
|
package/lib/localConfig.js
CHANGED
|
@@ -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
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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
|
}
|