iobroker.zigbee 3.0.5 → 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 +21 -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 +1 -1
- package/docs/en/readme.md +4 -2
- package/io-package.json +45 -41
- package/lib/binding.js +1 -1
- package/lib/commands.js +112 -82
- package/lib/developer.js +1 -1
- package/lib/devices.js +11 -7
- package/lib/exposes.js +2 -0
- 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 +5 -5
package/lib/statescontroller.js
CHANGED
|
@@ -35,6 +35,7 @@ class StatesController extends EventEmitter {
|
|
|
35
35
|
this.stashedUnknownModels = [];
|
|
36
36
|
this.debugMessages = { nodevice:{ in:[], out: []} };
|
|
37
37
|
this.debugActive = true;
|
|
38
|
+
this.deviceQueryBlock = [];
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
info(message, data) {
|
|
@@ -81,6 +82,8 @@ class StatesController extends EventEmitter {
|
|
|
81
82
|
}
|
|
82
83
|
|
|
83
84
|
async AddModelFromHerdsman(device, model) {
|
|
85
|
+
const namespace = `${this.adapter.name}.admin`;
|
|
86
|
+
|
|
84
87
|
// this.warn('addModelFromHerdsman ' + JSON.stringify(model) + ' ' + JSON.stringify(this.localConfig.getOverrideWithKey(model, 'legacy', true)));
|
|
85
88
|
if (this.localConfig.getOverrideWithTargetAndKey(model, 'legacy', true)) {
|
|
86
89
|
this.debug('Applying legacy definition for ' + model);
|
|
@@ -97,52 +100,56 @@ class StatesController extends EventEmitter {
|
|
|
97
100
|
const srcIcon = (modelDesc ? modelDesc.icon : '');
|
|
98
101
|
const model_modif = model.replace(/\//g, '-');
|
|
99
102
|
const pathToAdminIcon = `img/${model_modif}.png`;
|
|
100
|
-
|
|
101
|
-
const namespace = `${this.adapter.name}.admin`;
|
|
103
|
+
// source is a web address
|
|
102
104
|
if (srcIcon.startsWith('http')) {
|
|
103
|
-
|
|
104
|
-
if (
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
this.downloadIconToAdmin(srcIcon, pathToAdminIcon)
|
|
110
|
-
modelDesc.icon = pathToAdminIcon;
|
|
111
|
-
} catch (e) {
|
|
112
|
-
this.warn(`ERROR : icon not found at ${srcIcon}`);
|
|
113
|
-
}
|
|
114
|
-
return;
|
|
115
|
-
});
|
|
105
|
+
try {
|
|
106
|
+
//if (modelDesc) modelDesc.icon = pathToAdminIcon;
|
|
107
|
+
this.downloadIconToAdmin(srcIcon, pathToAdminIcon)
|
|
108
|
+
} catch (err) {
|
|
109
|
+
this.warn(`ERROR : unable to download ${srcIcon}: ${err && err.message ? err.message : 'no reason given'}`);
|
|
110
|
+
}
|
|
116
111
|
return;
|
|
117
112
|
}
|
|
113
|
+
// source is inline basee64
|
|
118
114
|
const base64Match = srcIcon.match(/data:image\/(.+);base64,/);
|
|
119
115
|
if (base64Match) {
|
|
120
116
|
this.warn(`base 64 Icon matched, trying to save it to disk as ${pathToAdminIcon}`);
|
|
121
|
-
modelDesc.icon = pathToAdminIcon;
|
|
117
|
+
if (modelDesc) modelDesc.icon = pathToAdminIcon;
|
|
122
118
|
this.adapter.fileExists(namespace, pathToAdminIcon, async (err,result) => {
|
|
123
119
|
if (result) {
|
|
124
120
|
this.warn(`no need to save icon to ${pathToAdminIcon}`);
|
|
125
121
|
return;
|
|
126
122
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
123
|
+
const msg = `Saving base64 Data to ${pathToAdminIcon}`
|
|
124
|
+
try {
|
|
125
|
+
const buffer = Buffer.from(srcIcon.replace(base64Match[0],''), 'base64');
|
|
126
|
+
this.adapter.writeFile(namespace, pathToAdminIcon, buffer, (err) => {
|
|
127
|
+
if (err) {
|
|
128
|
+
this.warn(`${msg} -- failed: ${err && err.message ? err.message : 'no reason given'}`);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
this.info(`${msg} -- success`);
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
catch (err) {
|
|
135
|
+
this.warn(`${msg} -- failed: ${err && err.message ? err.message : 'no reason given'}`)
|
|
136
|
+
}
|
|
131
137
|
});
|
|
132
138
|
return;
|
|
133
139
|
}
|
|
140
|
+
// path is absolute
|
|
134
141
|
if (modelDesc) modelDesc.icon = pathToAdminIcon;
|
|
135
142
|
this.adapter.fileExists(namespace, pathToAdminIcon, async(err, result) => {
|
|
136
143
|
if (result) {
|
|
137
|
-
this.debug(`icon ${modelDesc.icon} found - no copy needed`);
|
|
144
|
+
this.debug(`icon ${modelDesc ? modelDesc.icon : 'unknown icon'} found - no copy needed`);
|
|
138
145
|
return;
|
|
139
146
|
}
|
|
140
147
|
// try 3 options for source file
|
|
141
148
|
let src = srcIcon; // as given
|
|
142
149
|
const locations=[];
|
|
143
150
|
if (!fs.existsSync(src)) {
|
|
144
|
-
locations.push(src);
|
|
145
151
|
src = path.normalize(this.adapter.expandFileName(src));
|
|
152
|
+
locations.push(src);
|
|
146
153
|
} // assumed relative to data folder
|
|
147
154
|
if (!fs.existsSync(src)) {
|
|
148
155
|
locations.push(src);
|
|
@@ -158,36 +165,46 @@ class StatesController extends EventEmitter {
|
|
|
158
165
|
}
|
|
159
166
|
fs.readFile(src, (err, data) => {
|
|
160
167
|
if (err) {
|
|
161
|
-
this.
|
|
168
|
+
this.warn(`unable to read ${src}: ${(err.message? err.message:' no message given')}`);
|
|
162
169
|
return;
|
|
163
170
|
}
|
|
164
171
|
if (data) {
|
|
165
172
|
this.adapter.writeFile(namespace, pathToAdminIcon, data, (err) => {
|
|
166
173
|
if (err) {
|
|
167
|
-
this.error(
|
|
174
|
+
this.error(`error writing file ${path}: ${err.message ? err.message : 'no reason given'}`);
|
|
168
175
|
return;
|
|
169
176
|
}
|
|
170
177
|
this.info('Updated image file ' + pathToAdminIcon);
|
|
171
178
|
});
|
|
179
|
+
return;
|
|
172
180
|
}
|
|
181
|
+
this.error(`fs.readFile failed - neither error nor data is returned!`);
|
|
173
182
|
});
|
|
174
183
|
});
|
|
175
184
|
}
|
|
176
185
|
}
|
|
177
186
|
}
|
|
178
187
|
|
|
188
|
+
async updateDebugDevices(debugDevices) {
|
|
189
|
+
if (debugDevices != undefined)
|
|
190
|
+
this.debugDevices = debugDevices;
|
|
191
|
+
this.adapter.zbController.callExtensionMethod('setLocalVariable', ['debugDevices', this.debugDevices]);
|
|
192
|
+
|
|
193
|
+
}
|
|
194
|
+
|
|
179
195
|
async getDebugDevices(callback) {
|
|
180
196
|
if (this.debugDevices === undefined) {
|
|
181
197
|
this.debugDevices = [];
|
|
182
198
|
const state = await this.adapter.getStateAsync(`${this.adapter.namespace}.info.debugmessages`);
|
|
183
199
|
if (state) {
|
|
184
200
|
if (typeof state.val === 'string' && state.val.length > 2) {
|
|
185
|
-
this.
|
|
201
|
+
this.updateDebugDevices(state.val.split(';'));
|
|
186
202
|
}
|
|
187
203
|
this.info(`debug devices set to ${JSON.stringify(this.debugDevices)}`);
|
|
188
204
|
if (callback) callback(this.debugDevices);
|
|
189
205
|
}
|
|
190
206
|
} else {
|
|
207
|
+
this.updateDebugDevices();
|
|
191
208
|
// this.info(`debug devices was already set to ${JSON.stringify(this.debugDevices)}`);
|
|
192
209
|
callback(this.debugDevices)
|
|
193
210
|
}
|
|
@@ -195,15 +212,20 @@ class StatesController extends EventEmitter {
|
|
|
195
212
|
|
|
196
213
|
async toggleDeviceDebug(id) {
|
|
197
214
|
const arr = /zigbee.[0-9].([^.]+)/gm.exec(id);
|
|
215
|
+
if (!arr) {
|
|
216
|
+
this.warn(`unable to toggle debug for device ${id}: there was no mat (${JSON.stringify(arr)}) `);
|
|
217
|
+
return this.debugDevices;
|
|
218
|
+
}
|
|
198
219
|
if (arr[1] === undefined) {
|
|
199
220
|
this.warn(`unable to extract id from state ${id}`);
|
|
200
|
-
return
|
|
221
|
+
return this.debugDevices;
|
|
201
222
|
}
|
|
202
223
|
const stateKey = arr[1];
|
|
203
224
|
if (this.debugDevices === undefined) this.debugDevices = await this.getDebugDevices()
|
|
204
225
|
const idx = this.debugDevices.indexOf(stateKey);
|
|
205
226
|
if (idx < 0) this.debugDevices.push(stateKey);
|
|
206
227
|
else this.debugDevices.splice(idx, 1);
|
|
228
|
+
this.updateDebugDevices()
|
|
207
229
|
await this.adapter.setStateAsync(`${this.adapter.namespace}.info.debugmessages`, this.debugDevices.join(';'), true);
|
|
208
230
|
this.info('debug devices set to ' + JSON.stringify(this.debugDevices));
|
|
209
231
|
return this.debugDevices;
|
|
@@ -243,12 +265,13 @@ class StatesController extends EventEmitter {
|
|
|
243
265
|
} else {
|
|
244
266
|
this.debugDevices = [];
|
|
245
267
|
}
|
|
268
|
+
this.updateDebugDevices();
|
|
246
269
|
this.info('debug devices set to ' + JSON.stringify(this.debugDevices));
|
|
247
270
|
return;
|
|
248
271
|
}
|
|
249
272
|
|
|
250
273
|
const devId = getAdId(this.adapter, id); // iobroker device id
|
|
251
|
-
|
|
274
|
+
const deviceId = getZbId(id); // zigbee device id
|
|
252
275
|
|
|
253
276
|
if (this.checkDebugDevice(id)) {
|
|
254
277
|
const message = `User state change of state ${id} with value ${state.val} (ack: ${state.ack}) from ${state.from}`;
|
|
@@ -272,15 +295,24 @@ class StatesController extends EventEmitter {
|
|
|
272
295
|
if (this.debugActive) this.debug('State Change detected on deactivated Device - ignored');
|
|
273
296
|
return;
|
|
274
297
|
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
298
|
+
// check for group (model is group, deviceId is numerical, and not Nan or 0)
|
|
299
|
+
/*
|
|
300
|
+
if (model === 'group' && typeof deviceId == 'number' && Boolean(deviceId)) {
|
|
301
|
+
const options = this.localConfig.getOptions(`group_${deviceId}`);
|
|
302
|
+
options.isActive == (obj.common !== null);
|
|
303
|
+
this.publishFromState(deviceId, model, stateKey, state, options, debugId);
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
*/
|
|
307
|
+
// handle send_payload here
|
|
308
|
+
if (model && model.id === 'device_query') {
|
|
309
|
+
if (this.query_device_block.indexOf(deviceId) > -1 && !state.source.includes('.admin.')) {
|
|
310
|
+
this.info(`Device query for '${deviceId}' blocked - device query timeout has not elapsed yet.`);
|
|
280
311
|
return;
|
|
281
312
|
}
|
|
282
|
-
|
|
313
|
+
this.emit('device_query', { deviceId, debugId });
|
|
283
314
|
}
|
|
315
|
+
|
|
284
316
|
this.collectOptions(id.split('.')[2], model, true, options =>
|
|
285
317
|
this.publishFromState(deviceId, model, stateKey, state, options, debugId));
|
|
286
318
|
}
|
|
@@ -295,7 +327,7 @@ class StatesController extends EventEmitter {
|
|
|
295
327
|
callback(result);
|
|
296
328
|
return;
|
|
297
329
|
}
|
|
298
|
-
// find model states for options and get it values.
|
|
330
|
+
// find model states for options and get it values.
|
|
299
331
|
const devStates = await this.getDevStates('0x' + devId, model);
|
|
300
332
|
if (devStates == null || devStates == undefined || devStates.states == null || devStates.states == undefined) {
|
|
301
333
|
callback(result);
|
|
@@ -372,7 +404,7 @@ class StatesController extends EventEmitter {
|
|
|
372
404
|
}
|
|
373
405
|
}
|
|
374
406
|
|
|
375
|
-
async triggerComposite(_deviceId,
|
|
407
|
+
async triggerComposite(_deviceId, stateDesc, interactive) {
|
|
376
408
|
const deviceId = (_deviceId.replace('0x', ''));
|
|
377
409
|
const idParts = stateDesc.id.split('.').slice(-2);
|
|
378
410
|
const key = `${deviceId}.${idParts[0]}`;
|
|
@@ -400,20 +432,26 @@ class StatesController extends EventEmitter {
|
|
|
400
432
|
}, (stateDesc.compositeTimeout ? stateDesc.compositeTimeout : 100) * factor);
|
|
401
433
|
}
|
|
402
434
|
|
|
403
|
-
|
|
435
|
+
|
|
436
|
+
handleLinkedFunctResult(lfArr, devId, state) {
|
|
437
|
+
if (this.handleOption(devId, state.stateDesc)) return;
|
|
438
|
+
lfArr.push(state);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
async publishFromState(deviceId, model, stateKey, state, options, debugID) {
|
|
404
442
|
if (this.debugActive) this.debug(`Change state '${stateKey}' at device ${deviceId} type '${model}'`);
|
|
405
|
-
const
|
|
443
|
+
const has_elevated_debug = this.checkDebugDevice(typeof deviceId == 'number' ? `group_${deviceId}` : deviceId);
|
|
406
444
|
|
|
407
|
-
if (
|
|
445
|
+
if (has_elevated_debug) {
|
|
408
446
|
const message = (`Change state '${stateKey}' at device ${deviceId} type '${model}'`);
|
|
409
|
-
this.emit('device_debug', { ID:
|
|
447
|
+
this.emit('device_debug', { ID:debugID, data: { ID: deviceId, model: model, flag:'02', IO:false }, message:message});
|
|
410
448
|
}
|
|
411
449
|
|
|
412
450
|
const devStates = await this.getDevStates(deviceId, model);
|
|
413
451
|
if (!devStates) {
|
|
414
|
-
if (
|
|
452
|
+
if (has_elevated_debug) {
|
|
415
453
|
const message = (`no device states for device ${deviceId} type '${model}'`);
|
|
416
|
-
this.emit('device_debug', { ID:
|
|
454
|
+
this.emit('device_debug', { ID:debugID, data: { error: 'NOSTATES' , IO:false }, message:message});
|
|
417
455
|
}
|
|
418
456
|
return;
|
|
419
457
|
}
|
|
@@ -422,33 +460,80 @@ class StatesController extends EventEmitter {
|
|
|
422
460
|
const stateModel = devStates.stateModel;
|
|
423
461
|
if (!stateDesc) {
|
|
424
462
|
const message = (`No state available for '${model}' with key '${stateKey}'`);
|
|
425
|
-
if (
|
|
463
|
+
if (has_elevated_debug) this.emit('device_debug', { ID:debugID, data: { states:[{id:state.ID, value:'unknown', payload:'unknown'}], error: 'NOSTKEY' , IO:false }, message:message});
|
|
426
464
|
return;
|
|
427
465
|
}
|
|
428
466
|
|
|
429
467
|
const value = state.val;
|
|
430
468
|
if (value === undefined || value === '') {
|
|
431
|
-
if (
|
|
469
|
+
if (has_elevated_debug) {
|
|
432
470
|
const message = (`no value for device ${deviceId} type '${model}'`);
|
|
433
|
-
this.emit('device_debug', { ID:
|
|
471
|
+
this.emit('device_debug', { ID:debugID, data: { states:[{id:state.ID, value:'--', payload:'error', ep:stateDesc.epname}],error: 'NOVAL' , IO:false }, message:message});
|
|
472
|
+
}
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// send_payload can never be a linked state !;
|
|
477
|
+
if (stateDesc.id === 'send_payload') {
|
|
478
|
+
try {
|
|
479
|
+
const json_value = JSON.parse(value);
|
|
480
|
+
const payload = {device: deviceId.replace('0x', ''), payload: json_value, model:model, stateModel:stateModel};
|
|
481
|
+
if (has_elevated_debug) this.emit('device_debug', { ID:debugID, data: { flag: '04' ,payload:value ,states:[{id:stateDesc.id, value:json_value, payload:'none'}], IO:false }});
|
|
482
|
+
|
|
483
|
+
this.emit('send_payload', payload, debugID);
|
|
484
|
+
} catch (error) {
|
|
485
|
+
const message = `send_payload: ${value} does not parse as JSON Object : ${error.message}`;
|
|
486
|
+
if (has_elevated_debug) this.emit('device_debug', { ID:debugID, data: { error: 'EXSEND' ,states:[{id:stateDesc.id, value:value, payload:error.message}], IO:false }, message:message});
|
|
487
|
+
else this.error(message);
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
if (stateDesc.id === 'device_query') {
|
|
494
|
+
const interactive = (state.from.includes('.admin'));
|
|
495
|
+
if (!interactive && this.deviceQueryBlock.includes(deviceId)) {
|
|
496
|
+
this.warn(`device_query blocked due to excessive triggering - retrigger > 10 seconds after previous trigger has completed.`);
|
|
497
|
+
return;
|
|
434
498
|
}
|
|
499
|
+
this.deviceQueryBlock.push[deviceId];
|
|
500
|
+
const id = deviceId;
|
|
501
|
+
this.emit('device_query', deviceId, debugID, has_elevated_debug, (devId) =>{
|
|
502
|
+
setTimeout(() => { const idx = this.deviceQueryBlock.indexOf(id);
|
|
503
|
+
if (idx > -1) this.deviceQueryBlock.splice(idx);
|
|
504
|
+
}, 10000)
|
|
505
|
+
|
|
506
|
+
} )
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// composite states can never be linked states;
|
|
511
|
+
if (stateDesc.compositeState && stateDesc.compositeTimeout) {
|
|
512
|
+
this.triggerComposite(deviceId, stateDesc, state.from.includes('.admin.'));
|
|
435
513
|
return;
|
|
436
514
|
}
|
|
437
|
-
|
|
515
|
+
|
|
516
|
+
const stateList = [{stateDesc: stateDesc, value: value, index: 0, timeout: 0, source:state.from}];
|
|
517
|
+
|
|
438
518
|
if (stateModel && stateModel.linkedStates) {
|
|
439
519
|
stateModel.linkedStates.forEach(linkedFunct => {
|
|
440
520
|
try {
|
|
441
521
|
if (typeof linkedFunct === 'function') {
|
|
442
522
|
const res = linkedFunct(stateDesc, value, options, this.adapter.config.disableQueue);
|
|
443
523
|
if (res) {
|
|
444
|
-
|
|
524
|
+
if (res.hasOwnProperty('stateDesc')) { // we got a single state back
|
|
525
|
+
if (! res.stateDesc.isOption) stateList.push(res);
|
|
526
|
+
}
|
|
527
|
+
else {
|
|
528
|
+
res.forEach((ls) => { if (!ls.stateDesc.isOption) stateList.push(res)} );
|
|
529
|
+
}
|
|
445
530
|
}
|
|
446
531
|
} else {
|
|
447
532
|
this.warn(`publish from State - LinkedState is not a function ${JSON.stringify(linkedFunct)}`);
|
|
448
533
|
}
|
|
449
534
|
} catch (e) {
|
|
450
535
|
this.sendError(e);
|
|
451
|
-
if (
|
|
536
|
+
if (has_elevated_debug) this.emit('device_debug', { ID:debugID, data: { states:[{id:state.ID, value:state.val, payload:'unknown'}], error: 'EXLINK' , IO:false }});
|
|
452
537
|
this.error('Exception caught in publishfromstate: ' + (e && e.message ? e.message : 'no error message given'));
|
|
453
538
|
}
|
|
454
539
|
|
|
@@ -464,7 +549,8 @@ class StatesController extends EventEmitter {
|
|
|
464
549
|
readAfterWriteStates = readAfterWriteStates.concat(readAfterWriteStateDesc.id));
|
|
465
550
|
}
|
|
466
551
|
|
|
467
|
-
|
|
552
|
+
if (stateList.length > 0)
|
|
553
|
+
this.emit('changed', deviceId, model, stateModel, stateList, options, debugID, has_elevated_debug );
|
|
468
554
|
}
|
|
469
555
|
|
|
470
556
|
async renameDevice(id, newName) {
|
|
@@ -481,7 +567,6 @@ class StatesController extends EventEmitter {
|
|
|
481
567
|
}
|
|
482
568
|
|
|
483
569
|
verifyDeviceName(id, model ,name) {
|
|
484
|
-
const savedId = id.replace(`${this.adapter.namespace}.`, '');
|
|
485
570
|
return this.localConfig.NameForId(id, model, name);
|
|
486
571
|
}
|
|
487
572
|
|
|
@@ -503,7 +588,7 @@ class StatesController extends EventEmitter {
|
|
|
503
588
|
}
|
|
504
589
|
|
|
505
590
|
} catch (error) {
|
|
506
|
-
this.adapter.log.
|
|
591
|
+
this.adapter.log.warn(`Cannot delete Object ${devId}: ${error && error.message ? error.message : 'without error message'}`);
|
|
507
592
|
}
|
|
508
593
|
}
|
|
509
594
|
|
|
@@ -576,35 +661,8 @@ class StatesController extends EventEmitter {
|
|
|
576
661
|
const stateId = devId + '.' + name;
|
|
577
662
|
const new_name = obj.common.name;
|
|
578
663
|
if (common) {
|
|
579
|
-
|
|
580
|
-
new_common
|
|
581
|
-
}
|
|
582
|
-
if (common.type !== undefined) {
|
|
583
|
-
new_common.type = common.type;
|
|
584
|
-
}
|
|
585
|
-
if (common.unit !== undefined) {
|
|
586
|
-
new_common.unit = common.unit;
|
|
587
|
-
}
|
|
588
|
-
if (common.states !== undefined) {
|
|
589
|
-
new_common.states = common.states;
|
|
590
|
-
}
|
|
591
|
-
if (common.read !== undefined) {
|
|
592
|
-
new_common.read = common.read;
|
|
593
|
-
}
|
|
594
|
-
if (common.write !== undefined) {
|
|
595
|
-
new_common.write = common.write;
|
|
596
|
-
}
|
|
597
|
-
if (common.role !== undefined) {
|
|
598
|
-
new_common.role = common.role;
|
|
599
|
-
}
|
|
600
|
-
if (common.min !== undefined) {
|
|
601
|
-
new_common.min = common.min;
|
|
602
|
-
}
|
|
603
|
-
if (common.max !== undefined) {
|
|
604
|
-
new_common.max = common.max;
|
|
605
|
-
}
|
|
606
|
-
if (common.icon !== undefined) {
|
|
607
|
-
new_common.icon = common.icon;
|
|
664
|
+
for (const key in common) {
|
|
665
|
+
if (common[key] !== undefined) new_common[key] = common[key];
|
|
608
666
|
}
|
|
609
667
|
}
|
|
610
668
|
// check if state exist
|
|
@@ -697,7 +755,7 @@ class StatesController extends EventEmitter {
|
|
|
697
755
|
if (this.debugActive) this.debug(`UpdateState: Device is deactivated ${devId} ${JSON.stringify(obj)}`);
|
|
698
756
|
}
|
|
699
757
|
} else {
|
|
700
|
-
if (this.debugActive) this.debug(`UpdateState: missing device ${devId}
|
|
758
|
+
if (this.debugActive) this.debug(`UpdateState: missing device ${devId}`);
|
|
701
759
|
}
|
|
702
760
|
}
|
|
703
761
|
|
|
@@ -760,15 +818,25 @@ class StatesController extends EventEmitter {
|
|
|
760
818
|
statesMapping.getByModel();
|
|
761
819
|
}
|
|
762
820
|
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
else
|
|
771
|
-
|
|
821
|
+
|
|
822
|
+
async getDefaultGroupIcon(id, members) {
|
|
823
|
+
let groupID = 0;
|
|
824
|
+
if (typeof id == 'string') {
|
|
825
|
+
const regexResult = id.match(new RegExp(/group_(\d+)/));
|
|
826
|
+
if (!regexResult) return '';
|
|
827
|
+
groupID = Number(regexResult[1]);
|
|
828
|
+
} else if (typeof id == 'number') {
|
|
829
|
+
groupID = id;
|
|
830
|
+
}
|
|
831
|
+
if (groupID <= 0) return;
|
|
832
|
+
if (typeof members != 'number') {
|
|
833
|
+
const group = await this.adapter.zbController.getGroupMembersFromController(groupID)
|
|
834
|
+
if (typeof group == 'object')
|
|
835
|
+
return `img/group_${Math.max(Math.min(group.length, 7), 0)}.png`
|
|
836
|
+
else
|
|
837
|
+
return 'img/group_x.png'
|
|
838
|
+
}
|
|
839
|
+
return `img/group_${Math.max(Math.min(members, 7), 0)}.png`
|
|
772
840
|
}
|
|
773
841
|
|
|
774
842
|
async updateDev(dev_id, dev_name, model, callback) {
|
|
@@ -777,7 +845,9 @@ class StatesController extends EventEmitter {
|
|
|
777
845
|
if (this.debugActive) this.debug(`UpdateDev called with ${dev_id}, ${dev_name}, ${model}, ${__dev_name}`);
|
|
778
846
|
const id = '' + dev_id;
|
|
779
847
|
const modelDesc = statesMapping.findModel(model);
|
|
780
|
-
const modelIcon = (model == 'group' ?
|
|
848
|
+
const modelIcon = (model == 'group' ?
|
|
849
|
+
await this.getDefaultGroupIcon(dev_id) :
|
|
850
|
+
modelDesc && modelDesc.icon ? modelDesc.icon : 'img/unknown.png');
|
|
781
851
|
let icon = this.localConfig.IconForId(dev_id, model, modelIcon);
|
|
782
852
|
|
|
783
853
|
// download icon if it external and not undefined
|
|
@@ -786,6 +856,8 @@ class StatesController extends EventEmitter {
|
|
|
786
856
|
} else {
|
|
787
857
|
const model_modif = model.replace(/\//g, '-');
|
|
788
858
|
const pathToAdminIcon = `img/${model_modif}.png`;
|
|
859
|
+
// await this.fetchIcon(icon, '')
|
|
860
|
+
|
|
789
861
|
|
|
790
862
|
if (icon.startsWith('http')) {
|
|
791
863
|
try {
|
|
@@ -824,39 +896,69 @@ class StatesController extends EventEmitter {
|
|
|
824
896
|
});
|
|
825
897
|
}
|
|
826
898
|
|
|
899
|
+
async streamToBuffer(readableStream) {
|
|
900
|
+
return new Promise((resolve, reject) => {
|
|
901
|
+
const chunks = [];
|
|
902
|
+
readableStream.on('data', data => {
|
|
903
|
+
if (typeof data === 'string') {
|
|
904
|
+
// Convert string to Buffer assuming UTF-8 encoding
|
|
905
|
+
chunks.push(Buffer.from(data, 'utf-8'));
|
|
906
|
+
} else if (data instanceof Buffer) {
|
|
907
|
+
chunks.push(data);
|
|
908
|
+
} else {
|
|
909
|
+
// Convert other data types to JSON and then to a Buffer
|
|
910
|
+
const jsonData = JSON.stringify(data);
|
|
911
|
+
chunks.push(Buffer.from(jsonData, 'utf-8'));
|
|
912
|
+
}
|
|
913
|
+
});
|
|
914
|
+
readableStream.on('end', () => {
|
|
915
|
+
resolve(Buffer.concat(chunks));
|
|
916
|
+
});
|
|
917
|
+
readableStream.on('error', (err) => {
|
|
918
|
+
this.error(`error getting buffer from stream: ${err && err.message ? err.message : 'no reason given'}`);
|
|
919
|
+
reject;
|
|
920
|
+
});
|
|
921
|
+
});
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
async fetchIcon(url, image_path) {
|
|
925
|
+
const response = await fetch(url);
|
|
926
|
+
const data = await this.streamToBuffer(response.body);
|
|
927
|
+
fs.writeFileSync('/opt/iobroker/iobroker-data/zigbee_0/test.png', response.data);
|
|
928
|
+
}
|
|
929
|
+
|
|
827
930
|
async downloadIcon(url, image_path) {
|
|
828
931
|
try {
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
// reject(err);
|
|
844
|
-
this.warn(`ERROR : icon path not found ${image_path}`);
|
|
845
|
-
}).finally(() => {
|
|
846
|
-
const idx = this.ImagesToDownload.indexOf(url);
|
|
847
|
-
if (idx > -1) {
|
|
848
|
-
this.ImagesToDownload.splice(idx, 1);
|
|
932
|
+
const namespace = `${this.adapter.name}.admin`;
|
|
933
|
+
this.ImagesToDownload.push(url);
|
|
934
|
+
return new Promise((resolve, reject) => {
|
|
935
|
+
this.info(`downloading ${url} to ${image_path}`);
|
|
936
|
+
axios({
|
|
937
|
+
method: 'get',
|
|
938
|
+
url: url,
|
|
939
|
+
responseType: 'stream' // Dies ist wichtig, um den Stream direkt zu erhalten
|
|
940
|
+
}).then(async response => {
|
|
941
|
+
const data = await this.streamToBuffer(response.data);
|
|
942
|
+
this.adapter.writeFile(namespace, image_path, data, (err) => {
|
|
943
|
+
if (err) {
|
|
944
|
+
this.error(`error writing ${image_path} to admin: ${err.message ? err.message : 'no message given'}`);
|
|
945
|
+
reject;
|
|
849
946
|
}
|
|
850
|
-
|
|
947
|
+
this.info(`downloaded ${url} to ${image_path}.`)
|
|
948
|
+
resolve;
|
|
851
949
|
});
|
|
950
|
+
}).catch(err => {
|
|
951
|
+
this.warn(`error downloading icon ${err && err.message ? err.message : 'no message given'}`);
|
|
952
|
+
}).finally(() => {
|
|
953
|
+
const idx = this.ImagesToDownload.indexOf(url);
|
|
954
|
+
if (idx > -1) {
|
|
955
|
+
this.ImagesToDownload.splice(idx, 1);
|
|
956
|
+
}
|
|
852
957
|
});
|
|
853
|
-
}
|
|
854
|
-
else {
|
|
855
|
-
this.info(`not downloading ${image_path} - file exists`)
|
|
856
|
-
}
|
|
958
|
+
});
|
|
857
959
|
}
|
|
858
960
|
catch (error) {
|
|
859
|
-
this.error('downloadIcon
|
|
961
|
+
this.error('error in downloadIcon: ', error && error.message ? error.message : 'no message given');
|
|
860
962
|
}
|
|
861
963
|
}
|
|
862
964
|
|
|
@@ -864,33 +966,8 @@ class StatesController extends EventEmitter {
|
|
|
864
966
|
const namespace = `${this.adapter.name}.admin`;
|
|
865
967
|
this.adapter.fileExists(namespace, target, async (err,result) => {
|
|
866
968
|
if (result) return;
|
|
867
|
-
const src = `${tmpdir()}/${path.basename(target)}`;
|
|
868
|
-
//const msg = `downloading ${url} to ${src}`;
|
|
869
969
|
if (this.ImagesToDownload.indexOf(url) ==-1) {
|
|
870
|
-
await this.downloadIcon(url,
|
|
871
|
-
try {
|
|
872
|
-
fs.readFile(src, (err, data) => {
|
|
873
|
-
if (err) {
|
|
874
|
-
this.error('unable to read ' + src + ' : '+ (err && err.message? err.message:' no message given'))
|
|
875
|
-
return;
|
|
876
|
-
}
|
|
877
|
-
if (data) {
|
|
878
|
-
this.adapter.writeFile(namespace, target, data, (err) => {
|
|
879
|
-
if (err) {
|
|
880
|
-
this.error('error writing file ' + target + JSON.stringify(err))
|
|
881
|
-
return;
|
|
882
|
-
}
|
|
883
|
-
this.info(`copied ${src} to ${target}.`)
|
|
884
|
-
fs.rm(src, (err) => {
|
|
885
|
-
if (err) this.warn(`error removing ${src} : ${JSON.stringify(err)}`);
|
|
886
|
-
});
|
|
887
|
-
})
|
|
888
|
-
}
|
|
889
|
-
})
|
|
890
|
-
}
|
|
891
|
-
catch (error) {
|
|
892
|
-
this.error('fs.readfile error : ', error);
|
|
893
|
-
}
|
|
970
|
+
await this.downloadIcon(url, target);
|
|
894
971
|
}
|
|
895
972
|
});
|
|
896
973
|
}
|
|
@@ -1065,8 +1142,10 @@ class StatesController extends EventEmitter {
|
|
|
1065
1142
|
else if (this.debugActive) this.debug(message);
|
|
1066
1143
|
}
|
|
1067
1144
|
const message = `No value published for device ${devId}`;
|
|
1068
|
-
if (!has_published
|
|
1069
|
-
|
|
1145
|
+
if (!has_published) {
|
|
1146
|
+
if (has_elevated_debug) this.emit('device_debug', { ID:debugId, data:{ error:'NOVAL', IO:true }, message:message});
|
|
1147
|
+
else if (this.debugActive) this.debug(message);
|
|
1148
|
+
}
|
|
1070
1149
|
}
|
|
1071
1150
|
else {
|
|
1072
1151
|
const message = `ELEVATED IE05 - NOSTATE: No states matching the payload ${JSON.stringify(payload)} for device ${devId}`;
|
|
@@ -1079,6 +1158,27 @@ class StatesController extends EventEmitter {
|
|
|
1079
1158
|
}
|
|
1080
1159
|
}
|
|
1081
1160
|
|
|
1161
|
+
postProcessConvertedFromZigbeeMessage(definition, payload, options, device) {
|
|
1162
|
+
// Apply calibration/precision options
|
|
1163
|
+
for (const [key, value] of Object.entries(payload)) {
|
|
1164
|
+
const definitionExposes = Array.isArray(definition.exposes) ? definition.exposes : definition.exposes(device, {});
|
|
1165
|
+
const expose = definitionExposes.find((e) => e.property === key);
|
|
1166
|
+
|
|
1167
|
+
if (!expose) return;
|
|
1168
|
+
|
|
1169
|
+
if (expose &&
|
|
1170
|
+
expose.name in zigbeeHerdsmanConvertersUtils.calibrateAndPrecisionRoundOptionsDefaultPrecision &&
|
|
1171
|
+
value !== '' &&
|
|
1172
|
+
typeof value === 'number') {
|
|
1173
|
+
try {
|
|
1174
|
+
payload[key] = zigbeeHerdsmanConvertersUtils.calibrateAndPrecisionRoundOptions(value, options, expose.name);
|
|
1175
|
+
} catch (error) {
|
|
1176
|
+
this.warn(`Failed to apply calibration to '${expose.name}': ${error && error.message ? error.message: 'no reason given'}`);
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1082
1182
|
async processConverters(converters, devId, model, mappedModel, message, meta, debugId) {
|
|
1083
1183
|
for (const converter of converters) {
|
|
1084
1184
|
const publish = (payload, dID) => {
|
|
@@ -1100,6 +1200,10 @@ class StatesController extends EventEmitter {
|
|
|
1100
1200
|
}
|
|
1101
1201
|
});
|
|
1102
1202
|
|
|
1203
|
+
if (Object.keys(payload).length > 0 && Object.keys(options).length > 0) {
|
|
1204
|
+
this.postProcessConvertedFromZigbeeMessage(mappedModel, payload, options, null);
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1103
1207
|
publish(payload, debugId);
|
|
1104
1208
|
}
|
|
1105
1209
|
}
|
|
@@ -1117,6 +1221,10 @@ class StatesController extends EventEmitter {
|
|
|
1117
1221
|
|
|
1118
1222
|
const has_elevated_debug = this.checkDebugDevice(devId);
|
|
1119
1223
|
const debugId = Date.now();
|
|
1224
|
+
if (entity.device.interviewing) {
|
|
1225
|
+
this.warn(`zigbee event for ${device.ieeeAddr} received during interview!`);
|
|
1226
|
+
return;
|
|
1227
|
+
}
|
|
1120
1228
|
|
|
1121
1229
|
// raw message data for logging and msg_from_zigbee
|
|
1122
1230
|
const msgForState = Object.assign({}, message);
|
|
@@ -1194,7 +1302,7 @@ class StatesController extends EventEmitter {
|
|
|
1194
1302
|
return;
|
|
1195
1303
|
}
|
|
1196
1304
|
|
|
1197
|
-
let converters = mappedModel.fromZigbee.filter(c => c && c.cluster === cluster && (
|
|
1305
|
+
let converters = [...mappedModel.fromZigbee,...mappedModel.toZigbee].filter(c => c && c.cluster === cluster && (
|
|
1198
1306
|
Array.isArray(c.type) ? c.type.includes(type) : c.type === type));
|
|
1199
1307
|
|
|
1200
1308
|
|
|
@@ -1204,7 +1312,7 @@ class StatesController extends EventEmitter {
|
|
|
1204
1312
|
}
|
|
1205
1313
|
|
|
1206
1314
|
if (!converters.length) {
|
|
1207
|
-
if (type !== 'readResponse') {
|
|
1315
|
+
if (type !== 'readResponse' && type !== 'commandQueryNextImageRequest') {
|
|
1208
1316
|
const message = `No converter available for '${mappedModel.model}' '${devId}' with cluster '${cluster}' and type '${type}'`;
|
|
1209
1317
|
if (has_elevated_debug) this.emit('device_debug', { ID:debugId, data: { error:'NOCONV', IO:true }, message:message});
|
|
1210
1318
|
else if (this.debugActive) this.debug(message);
|