iobroker.zigbee 2.0.0 → 2.0.1
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 +27 -9
- package/admin/admin.js +312 -125
- package/admin/img/PTM 215Z.png +0 -0
- package/admin/img/group_0.png +0 -0
- package/admin/img/group_x.png +0 -0
- package/admin/img/philips_hue_lom001.png +0 -0
- package/admin/index_m.html +95 -45
- package/admin/tab_m.html +116 -48
- package/docs/de/img/Zigbee_config_de.png +0 -0
- package/docs/de/img/Zigbee_tab_de.png +0 -0
- package/docs/en/img/Zigbee_config_en.png +0 -0
- package/docs/en/img/Zigbee_tab_en.png +0 -0
- package/docs/tutorial/groups-1.png +0 -0
- package/docs/tutorial/groups-2.png +0 -0
- package/docs/tutorial/tab-dev-1.png +0 -0
- package/io-package.json +40 -39
- package/lib/binding.js +1 -1
- package/lib/colors.js +7 -0
- package/lib/commands.js +127 -17
- package/lib/developer.js +0 -0
- package/lib/devices.js +78 -74
- package/lib/exclude.js +30 -54
- package/lib/exposes.js +203 -246
- package/lib/groups.js +84 -29
- package/lib/localConfig.js +295 -0
- package/lib/ota.js +0 -0
- package/lib/statescontroller.js +410 -183
- package/lib/utils.js +1 -1
- package/lib/zbDeviceAvailability.js +15 -23
- package/lib/zbDeviceConfigure.js +0 -0
- package/lib/zbDeviceEvent.js +2 -13
- package/lib/zigbeecontroller.js +299 -207
- package/main.js +145 -56
- package/package.json +8 -7
package/lib/statescontroller.js
CHANGED
|
@@ -6,8 +6,15 @@ const getAdId = require('./utils').getAdId;
|
|
|
6
6
|
const getZbId = require('./utils').getZbId;
|
|
7
7
|
const fs = require('fs');
|
|
8
8
|
const axios = require('axios');
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
const localConfig = require('./localConfig');
|
|
10
|
+
//const { deviceAddCustomCluster } = require('zigbee-herdsman-converters/lib/modernExtend');
|
|
11
|
+
//const { setDefaultAutoSelectFamilyAttemptTimeout } = require('net');
|
|
12
|
+
//const { runInThisContext } = require('vm');
|
|
13
|
+
//const { time } = require('console');
|
|
14
|
+
const { exec } = require('child_process');
|
|
15
|
+
const { tmpdir } = require('os');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
const { numberWithinRange } = require('zigbee-herdsman-converters/lib/utils');
|
|
11
18
|
|
|
12
19
|
|
|
13
20
|
class StatesController extends EventEmitter {
|
|
@@ -17,18 +24,13 @@ class StatesController extends EventEmitter {
|
|
|
17
24
|
this.adapter.on('stateChange', this.onStateChange.bind(this));
|
|
18
25
|
this.query_device_block = [];
|
|
19
26
|
this.debugDevices = undefined;
|
|
20
|
-
const fn = adapter.expandFileName('dev_names.json');
|
|
21
|
-
this.dev_names_fn = fn.replace('.', '_');
|
|
22
27
|
this.retTimeoutHandle = null;
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
});
|
|
28
|
+
this.localConfig = new localConfig(adapter);
|
|
29
|
+
this.compositeData = {};
|
|
30
|
+
this.cleanupRequired = false;
|
|
31
|
+
this.timeoutHandleUpload = null;
|
|
32
|
+
this.ImagesToDownload = [];
|
|
33
|
+
this.stashedErrors = {};
|
|
32
34
|
}
|
|
33
35
|
|
|
34
36
|
info(message, data) {
|
|
@@ -51,30 +53,59 @@ class StatesController extends EventEmitter {
|
|
|
51
53
|
this.adapter.sendError(error, message);
|
|
52
54
|
}
|
|
53
55
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
this.
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
this.
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
56
|
+
TriggerUpload(delay) {
|
|
57
|
+
if (this.timeoutHandleUpload) return;
|
|
58
|
+
this.warn('triggering upload')
|
|
59
|
+
this.timeoutHandleUpload = this.adapter.setTimeout(() => {
|
|
60
|
+
try {
|
|
61
|
+
this.warn('executing upload')
|
|
62
|
+
exec('iobroker upload zigbee', (error, stdout, stderr) => {
|
|
63
|
+
if (error) this.error('exec error: ' + JSON.stringify(error));
|
|
64
|
+
this.warn('upload done');
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
this.error('error on upload exec');
|
|
69
|
+
}
|
|
70
|
+
}, delay);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
getStashedErrors() {
|
|
74
|
+
if (!this.stashedErrors) return [];
|
|
75
|
+
return Object.values(this.stashedErrors);
|
|
65
76
|
}
|
|
66
77
|
|
|
67
|
-
getDebugDevices() {
|
|
68
|
-
this.debugDevices
|
|
69
|
-
|
|
78
|
+
async getDebugDevices(callback) {
|
|
79
|
+
if (this.debugDevices === undefined) {
|
|
80
|
+
this.debugDevices = [];
|
|
81
|
+
const state = await this.adapter.getStateAsync(`${this.adapter.namespace}.info.debugmessages`);
|
|
70
82
|
if (state) {
|
|
71
83
|
if (typeof state.val === 'string' && state.val.length > 2) {
|
|
72
84
|
this.debugDevices = state.val.split(';');
|
|
73
85
|
}
|
|
74
86
|
this.info(`debug devices set to ${JSON.stringify(this.debugDevices)}`);
|
|
87
|
+
if (callback) callback(this.debugDevices);
|
|
75
88
|
}
|
|
76
|
-
}
|
|
89
|
+
} else {
|
|
90
|
+
// this.info(`debug devices was already set to ${JSON.stringify(this.debugDevices)}`);
|
|
91
|
+
callback(this.debugDevices)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
77
94
|
|
|
95
|
+
async toggleDeviceDebug(id) {
|
|
96
|
+
const arr = /zigbee.[0-9].([^.]+)/gm.exec(id);
|
|
97
|
+
if (arr[1] === undefined) {
|
|
98
|
+
this.warn(`unable to extract id from state ${id}`);
|
|
99
|
+
return [];
|
|
100
|
+
}
|
|
101
|
+
const stateKey = arr[1];
|
|
102
|
+
if (this.debugDevices === undefined) this.debugDevices = await this.getDebugDevices()
|
|
103
|
+
const idx = this.debugDevices.indexOf(stateKey);
|
|
104
|
+
if (idx < 0) this.debugDevices.push(stateKey);
|
|
105
|
+
else this.debugDevices.splice(idx, 1);
|
|
106
|
+
await this.adapter.setStateAsync(`${this.adapter.namespace}.info.debugmessages`, this.debugDevices.join(';'), true);
|
|
107
|
+
this.info('debug devices set to ' + JSON.stringify(this.debugDevices));
|
|
108
|
+
return this.debugDevices;
|
|
78
109
|
}
|
|
79
110
|
|
|
80
111
|
checkDebugDevice(dev) {
|
|
@@ -82,9 +113,12 @@ class StatesController extends EventEmitter {
|
|
|
82
113
|
if (this.debugDevices === undefined) {
|
|
83
114
|
this.getDebugDevices();
|
|
84
115
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
116
|
+
else
|
|
117
|
+
{
|
|
118
|
+
for (const addressPart of this.debugDevices) {
|
|
119
|
+
if (typeof dev === 'string' && dev.includes(addressPart)) {
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
88
122
|
}
|
|
89
123
|
}
|
|
90
124
|
return false;
|
|
@@ -101,10 +135,10 @@ class StatesController extends EventEmitter {
|
|
|
101
135
|
if (id.endsWith('debugmessages')) {
|
|
102
136
|
if (typeof state.val === 'string' && state.val.length > 2) {
|
|
103
137
|
this.debugDevices = state.val.split(';');
|
|
104
|
-
|
|
105
138
|
} else {
|
|
106
139
|
this.debugDevices = [];
|
|
107
140
|
}
|
|
141
|
+
this.info('debug devices set to ' + JSON.stringify(this.debugDevices));
|
|
108
142
|
return;
|
|
109
143
|
}
|
|
110
144
|
|
|
@@ -132,7 +166,13 @@ class StatesController extends EventEmitter {
|
|
|
132
166
|
return;
|
|
133
167
|
}
|
|
134
168
|
if (model === 'group') {
|
|
135
|
-
|
|
169
|
+
const match = deviceId.match(/group_(\d+)/gm);
|
|
170
|
+
if (match) {
|
|
171
|
+
deviceId = parseInt(match[1]);
|
|
172
|
+
this.publishFromState(deviceId, model, stateKey, state, {});
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
136
176
|
}
|
|
137
177
|
this.collectOptions(id.split('.')[2], model, options =>
|
|
138
178
|
this.publishFromState(deviceId, model, stateKey, state, options));
|
|
@@ -143,7 +183,7 @@ class StatesController extends EventEmitter {
|
|
|
143
183
|
|
|
144
184
|
async collectOptions(devId, model, callback) {
|
|
145
185
|
const result = {};
|
|
146
|
-
// find model states for options and get it values
|
|
186
|
+
// find model states for options and get it values. No options for groups !!!
|
|
147
187
|
const devStates = await this.getDevStates('0x' + devId, model);
|
|
148
188
|
if (devStates == null || devStates == undefined || devStates.states == null || devStates.states == undefined) {
|
|
149
189
|
callback(result);
|
|
@@ -207,6 +247,34 @@ class StatesController extends EventEmitter {
|
|
|
207
247
|
}
|
|
208
248
|
}
|
|
209
249
|
|
|
250
|
+
async triggerComposite(_deviceId, model, stateDesc, interactive) {
|
|
251
|
+
const deviceId = (_deviceId.replace('0x', ''));
|
|
252
|
+
const idParts = stateDesc.id.split('.').slice(-2);
|
|
253
|
+
const key = `${deviceId}.${idParts[0]}`;
|
|
254
|
+
const handle = this.compositeData[key]
|
|
255
|
+
const factor = (interactive ? 10 : 1);
|
|
256
|
+
if (handle) this.adapter.clearTimeout(handle);
|
|
257
|
+
this.compositeData[key] = this.adapter.setTimeout(async () => {
|
|
258
|
+
delete this.compositeData[key];
|
|
259
|
+
const parts = stateDesc.id.split('.');
|
|
260
|
+
parts.pop();
|
|
261
|
+
|
|
262
|
+
this.adapter.getStatesOf(deviceId, parts.join('.'),async (err,states) => {
|
|
263
|
+
if (!err && states) {
|
|
264
|
+
const components = {};
|
|
265
|
+
for (const stateobj of states) {
|
|
266
|
+
const ckey = stateobj._id.split('.').pop();
|
|
267
|
+
const state = await this.adapter.getState(stateobj._id);
|
|
268
|
+
if (state && state.val != undefined) {
|
|
269
|
+
components[ckey] = state.val;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
this.adapter.setState(`${deviceId}.${stateDesc.compositeState}`, JSON.stringify(components));
|
|
273
|
+
}
|
|
274
|
+
})
|
|
275
|
+
}, (stateDesc.compositeTimeout ? stateDesc.compositeTimeout : 100) * factor);
|
|
276
|
+
}
|
|
277
|
+
|
|
210
278
|
async publishFromState(deviceId, model, stateKey, state, options) {
|
|
211
279
|
this.debug(`Change state '${stateKey}' at device ${deviceId} type '${model}'`);
|
|
212
280
|
const elevated = this.checkDebugDevice(deviceId);
|
|
@@ -232,7 +300,7 @@ class StatesController extends EventEmitter {
|
|
|
232
300
|
this.error(`ELEVATED OE2: no value for device ${deviceId} type '${model}'`);
|
|
233
301
|
return;
|
|
234
302
|
}
|
|
235
|
-
let stateList = [{stateDesc: stateDesc, value: value, index: 0, timeout: 0}];
|
|
303
|
+
let stateList = [{stateDesc: stateDesc, value: value, index: 0, timeout: 0, source:state.from}];
|
|
236
304
|
if (stateModel && stateModel.linkedStates) {
|
|
237
305
|
stateModel.linkedStates.forEach(linkedFunct => {
|
|
238
306
|
try {
|
|
@@ -264,55 +332,51 @@ class StatesController extends EventEmitter {
|
|
|
264
332
|
this.emit('changed', deviceId, model, stateModel, stateList, options);
|
|
265
333
|
}
|
|
266
334
|
|
|
267
|
-
renameDevice(id, newName) {
|
|
268
|
-
this.
|
|
269
|
-
this.adapter.
|
|
335
|
+
async renameDevice(id, newName) {
|
|
336
|
+
const stateId = id.replace(`${this.adapter.namespace}.`, '')
|
|
337
|
+
const obj = await this.adapter.getObjectAsync(id);
|
|
338
|
+
if (newName == null || newName == undefined) newName = '';
|
|
339
|
+
let objName = newName;
|
|
340
|
+
if (newName.length < 1 || newName == obj.common.type) {
|
|
341
|
+
objName = (obj.common.type == 'group' ? stateId.replace('_',' ') : obj.common.type);
|
|
342
|
+
}
|
|
343
|
+
this.localConfig.updateDeviceName(stateId, newName);
|
|
344
|
+
this.debug('rename device: newname ~' + newName + '~ objName ~' + objName + '~')
|
|
345
|
+
this.adapter.extendObject(id, {common: {name: objName}});
|
|
270
346
|
}
|
|
271
347
|
|
|
272
|
-
|
|
273
|
-
this.adapter.
|
|
348
|
+
verifyDeviceName(id, model ,name) {
|
|
349
|
+
const savedId = id.replace(`${this.adapter.namespace}.`, '');
|
|
350
|
+
return this.localConfig.NameForId(id, model, name);
|
|
274
351
|
}
|
|
275
352
|
|
|
276
|
-
async rebuildRetainDeviceNames() {
|
|
277
|
-
savedDeviceNamesDB = {};
|
|
278
|
-
const devList = await this.adapter.getAdapterObjectsAsync();
|
|
279
353
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
savedDeviceNamesDB[devList[key]._id.replace(`${this.adapter.namespace}.`, '')] = devList[key].common.name;
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
this.retainDeviceNames();
|
|
354
|
+
setDeviceActivated(id, active) {
|
|
355
|
+
this.adapter.extendObject(id, {common: {deactivated: active}});
|
|
286
356
|
}
|
|
287
357
|
|
|
288
358
|
storeDeviceName(id, name) {
|
|
289
|
-
|
|
290
|
-
this.retainDeviceNames();
|
|
359
|
+
return this.localConfig.updateDeviceName(id, name);
|
|
291
360
|
}
|
|
292
361
|
|
|
293
|
-
verifyDeviceName(id, name) {
|
|
294
|
-
const savedId = id.replace(`${this.adapter.namespace}.`, '');
|
|
295
|
-
if (!savedDeviceNamesDB.hasOwnProperty(savedId)) {
|
|
296
|
-
savedDeviceNamesDB[savedId] = name;
|
|
297
|
-
this.retainDeviceNames();
|
|
298
|
-
}
|
|
299
|
-
return savedDeviceNamesDB[savedId];
|
|
300
|
-
}
|
|
301
362
|
|
|
302
363
|
async deleteObj(devId) {
|
|
303
364
|
const options = { recursive:true };
|
|
304
365
|
try {
|
|
305
|
-
this.adapter.delObject(devId,options), (err) => {
|
|
366
|
+
this.adapter.delObject(devId,options), (err) => {
|
|
367
|
+
this.adapter.log.info(`Cannot delete Object ${devId}: ${err}`);
|
|
368
|
+
}
|
|
306
369
|
|
|
307
|
-
} catch (
|
|
308
|
-
this.adapter.log.info(`Cannot delete
|
|
370
|
+
} catch (error) {
|
|
371
|
+
this.adapter.log.info(`Cannot delete Object ${devId}: ${error && error.message ? error.message : 'without error message'}`);
|
|
309
372
|
}
|
|
310
373
|
}
|
|
311
374
|
|
|
312
|
-
async deleteOrphanedDeviceStates(ieeeAddr, model, force, callback) {
|
|
375
|
+
async deleteOrphanedDeviceStates(ieeeAddr, model, force, callback, markOnly) {
|
|
313
376
|
const devStates = await this.getDevStates(ieeeAddr, model);
|
|
314
377
|
const commonStates = statesMapping.commonStates;
|
|
315
378
|
const devId = ieeeAddr.substr(2);
|
|
379
|
+
const messages = [];
|
|
316
380
|
this.adapter.getStatesOf(devId, (err, states) => {
|
|
317
381
|
if (!err && states) {
|
|
318
382
|
states.forEach((state) => {
|
|
@@ -330,17 +394,35 @@ class StatesController extends EventEmitter {
|
|
|
330
394
|
if (commonStates.find(statedesc => statename === statedesc.id) === undefined &&
|
|
331
395
|
devStates.states.find(statedesc => statename === statedesc.id) === undefined
|
|
332
396
|
) {
|
|
333
|
-
|
|
334
|
-
|
|
397
|
+
this.cleanupRequired |= markOnly;
|
|
398
|
+
if (state.common.hasOwnProperty('custom') && !force && !markOnly) {
|
|
399
|
+
messages.push(`keeping disconnected state ${JSON.stringify(statename)} of ${devId} `)
|
|
400
|
+
this.info(`keeping disconnected state ${JSON.stringify(statename)} of ${devId} `);
|
|
401
|
+
this.cleanupRequired == true;
|
|
335
402
|
} else {
|
|
336
|
-
|
|
337
|
-
|
|
403
|
+
if (markOnly) {
|
|
404
|
+
this.adapter.extendObjectAsync(devId.concat('.',statename), {common: {color:'#E67E22'}})
|
|
405
|
+
|
|
406
|
+
}
|
|
407
|
+
else
|
|
408
|
+
{
|
|
409
|
+
try {
|
|
410
|
+
this.info(`deleting disconnected state ${JSON.stringify(statename)} of ${devId} `);
|
|
411
|
+
messages.push(`deleting disconnected state ${JSON.stringify(statename)} of ${devId} `);
|
|
412
|
+
this.deleteObj(devId.concat('.',statename));
|
|
413
|
+
}
|
|
414
|
+
catch (error) {
|
|
415
|
+
//messages.push(`ERROR: failed to delete state ${JSON.stringify(statename)} of ${devId} `)
|
|
416
|
+
}
|
|
417
|
+
}
|
|
338
418
|
}
|
|
339
419
|
} else {
|
|
340
420
|
this.debug(`keeping connecte state ${JSON.stringify(statename)} of ${devId} `);
|
|
421
|
+
messages.push(`keeping connecte state ${JSON.stringify(statename)} of ${devId} `);
|
|
341
422
|
}
|
|
342
423
|
});
|
|
343
424
|
}
|
|
425
|
+
if (callback) callback(messages);
|
|
344
426
|
});
|
|
345
427
|
}
|
|
346
428
|
|
|
@@ -349,89 +431,134 @@ class StatesController extends EventEmitter {
|
|
|
349
431
|
setTimeout(() => this.updateState(dev_id, name, outValue, common), timeout);
|
|
350
432
|
}
|
|
351
433
|
|
|
352
|
-
updateState(devId, name, value, common) {
|
|
353
|
-
this.adapter.
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
}
|
|
363
|
-
if (common.type !== undefined) {
|
|
364
|
-
new_common.type = common.type;
|
|
365
|
-
}
|
|
366
|
-
if (common.unit !== undefined) {
|
|
367
|
-
new_common.unit = common.unit;
|
|
368
|
-
}
|
|
369
|
-
if (common.states !== undefined) {
|
|
370
|
-
new_common.states = common.states;
|
|
371
|
-
}
|
|
372
|
-
if (common.read !== undefined) {
|
|
373
|
-
new_common.read = common.read;
|
|
374
|
-
}
|
|
375
|
-
if (common.write !== undefined) {
|
|
376
|
-
new_common.write = common.write;
|
|
377
|
-
}
|
|
378
|
-
if (common.role !== undefined) {
|
|
379
|
-
new_common.role = common.role;
|
|
380
|
-
}
|
|
381
|
-
if (common.min !== undefined) {
|
|
382
|
-
new_common.min = common.min;
|
|
383
|
-
}
|
|
384
|
-
if (common.max !== undefined) {
|
|
385
|
-
new_common.max = common.max;
|
|
386
|
-
}
|
|
387
|
-
if (common.icon !== undefined) {
|
|
388
|
-
new_common.icon = common.icon;
|
|
389
|
-
}
|
|
434
|
+
async updateState(devId, name, value, common) {
|
|
435
|
+
const obj = await this.adapter.getObjectAsync(devId)
|
|
436
|
+
if (obj) {
|
|
437
|
+
if (!obj.common.deactivated) {
|
|
438
|
+
const new_common = {name: name, color:null};
|
|
439
|
+
const stateId = devId + '.' + name;
|
|
440
|
+
const new_name = obj.common.name;
|
|
441
|
+
if (common) {
|
|
442
|
+
if (common.name !== undefined) {
|
|
443
|
+
new_common.name = common.name;
|
|
390
444
|
}
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
445
|
+
if (common.type !== undefined) {
|
|
446
|
+
new_common.type = common.type;
|
|
447
|
+
}
|
|
448
|
+
if (common.unit !== undefined) {
|
|
449
|
+
new_common.unit = common.unit;
|
|
450
|
+
}
|
|
451
|
+
if (common.states !== undefined) {
|
|
452
|
+
new_common.states = common.states;
|
|
453
|
+
}
|
|
454
|
+
if (common.read !== undefined) {
|
|
455
|
+
new_common.read = common.read;
|
|
456
|
+
}
|
|
457
|
+
if (common.write !== undefined) {
|
|
458
|
+
new_common.write = common.write;
|
|
459
|
+
}
|
|
460
|
+
if (common.role !== undefined) {
|
|
461
|
+
new_common.role = common.role;
|
|
462
|
+
}
|
|
463
|
+
if (common.min !== undefined) {
|
|
464
|
+
new_common.min = common.min;
|
|
465
|
+
}
|
|
466
|
+
if (common.max !== undefined) {
|
|
467
|
+
new_common.max = common.max;
|
|
468
|
+
}
|
|
469
|
+
if (common.icon !== undefined) {
|
|
470
|
+
new_common.icon = common.icon;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
// check if state exist
|
|
474
|
+
const stobj = await this.adapter.getObjectAsync(stateId);
|
|
475
|
+
let hasChanges = false;
|
|
476
|
+
if (stobj) {
|
|
477
|
+
// update state - not change name and role (user can it changed)
|
|
478
|
+
if (stobj.common.name) {
|
|
479
|
+
delete new_common.name;
|
|
480
|
+
} else {
|
|
481
|
+
new_common.name = `${new_name} ${new_common.name}`;
|
|
482
|
+
}
|
|
483
|
+
delete new_common.role;
|
|
484
|
+
|
|
485
|
+
// check whether any common property is different
|
|
486
|
+
if (stobj.common) {
|
|
487
|
+
for (const property in new_common) {
|
|
488
|
+
if (stobj.common.hasOwnProperty(property)) {
|
|
489
|
+
if (stobj.common[property] === new_common[property]) {
|
|
490
|
+
delete new_common[property];
|
|
491
|
+
} else {
|
|
492
|
+
hasChanges = true;
|
|
413
493
|
}
|
|
414
494
|
}
|
|
415
|
-
} else {
|
|
416
|
-
hasChanges = true;
|
|
417
495
|
}
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
496
|
+
}
|
|
497
|
+
} else {
|
|
498
|
+
const matches = stateId.match((/\./g));
|
|
499
|
+
if (matches && matches.length>1) {
|
|
500
|
+
const channels = stateId.split('.');
|
|
501
|
+
const SubChannels = [channels.shift()];
|
|
502
|
+
channels.pop();
|
|
503
|
+
for (const channel of channels) {
|
|
504
|
+
SubChannels.push(channel);
|
|
505
|
+
const id = SubChannels.join('.');
|
|
506
|
+
await this.adapter.extendObjectAsync(id, {type: 'channel', common: { name:channel}, native:{}})
|
|
425
507
|
}
|
|
508
|
+
}
|
|
509
|
+
hasChanges = true;
|
|
510
|
+
}
|
|
426
511
|
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
512
|
+
// only change object when any common property has changed
|
|
513
|
+
// first check value
|
|
514
|
+
if (value !== undefined) {
|
|
515
|
+
const type = stobj ? stobj.common.type : new_common.type;
|
|
516
|
+
if (type === 'number') {
|
|
517
|
+
const minval = (stobj ? stobj.common.min: new_common.min) || -Number.MAX_VALUE;
|
|
518
|
+
const maxval = (stobj ? stobj.common.max: new_common.max) || Number.MAX_VALUE;
|
|
519
|
+
let nval = (typeof value == 'number' ? value : parseFloat(value));
|
|
520
|
+
if (isNaN(nval)) {
|
|
521
|
+
if (minval !== undefined && typeof minval === 'number')
|
|
522
|
+
nval = minval;
|
|
523
|
+
else
|
|
524
|
+
nval = 0;
|
|
525
|
+
}
|
|
526
|
+
if (nval < minval) {
|
|
527
|
+
hasChanges = true;
|
|
528
|
+
new_common.color = '#FF0000'
|
|
529
|
+
value = minval
|
|
530
|
+
if (!this.stashedErrors.hasOwnProperty(`${stateId}.min`))
|
|
531
|
+
{
|
|
532
|
+
this.error(`State value for ${stateId} has value "${nval}" less than min "${minval}". - this eror is recorded and not repeated`);
|
|
533
|
+
this.stashedErrors[`${stateId}.min`] = `State value for ${stateId} has value "${nval}." less than min "${minval}"`;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
if (nval > maxval) {
|
|
537
|
+
hasChanges = true;
|
|
538
|
+
hasChanges = true;
|
|
539
|
+
new_common.color = '#FF0000'
|
|
540
|
+
value = maxval;
|
|
541
|
+
if (!this.stashedErrors.hasOwnProperty(`${stateId}.max`))
|
|
542
|
+
{
|
|
543
|
+
this.error(`State value for ${stateId} has value "${nval}" more than max "${maxval}". - this eror is recorded and not repeated`);
|
|
544
|
+
this.stashedErrors[`${stateId}.max`] = `State value for ${stateId} has value "${nval}" more than max "${maxval}".`;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
//
|
|
550
|
+
if (hasChanges) {
|
|
551
|
+
this.adapter.extendObject(stateId, {type: 'state', common: new_common, native: {}}, () =>
|
|
552
|
+
value !== undefined && this.setState_typed(stateId, value, true, stobj ? stobj.common.type : new_common.type));
|
|
553
|
+
} else if (value !== undefined) {
|
|
554
|
+
this.setState_typed(stateId, value, true, stobj.common.type);
|
|
430
555
|
}
|
|
431
556
|
} else {
|
|
432
|
-
this.debug(`UpdateState:
|
|
557
|
+
this.debug(`UpdateState: Device is deactivated ${devId} ${JSON.stringify(obj)}`);
|
|
433
558
|
}
|
|
434
|
-
}
|
|
559
|
+
} else {
|
|
560
|
+
this.debug(`UpdateState: missing device ${devId} ${JSON.stringify(obj)}`);
|
|
561
|
+
}
|
|
435
562
|
}
|
|
436
563
|
|
|
437
564
|
setState_typed(id, value, ack, type, callback) {
|
|
@@ -476,28 +603,51 @@ class StatesController extends EventEmitter {
|
|
|
476
603
|
this.adapter.setState(id, value, ack, callback);
|
|
477
604
|
}
|
|
478
605
|
|
|
479
|
-
|
|
480
|
-
const
|
|
606
|
+
async applyLegacyDevices() {
|
|
607
|
+
const legacyModels = await this.localConfig.getLegacyModels();
|
|
608
|
+
const modelarr1 = [];
|
|
609
|
+
//this.warn('devices are' + modelarr1.join(','));
|
|
610
|
+
statesMapping.devices.forEach(item => modelarr1.push(item.models));
|
|
611
|
+
//this.warn('legacy models are ' + JSON.stringify(legacyModels));
|
|
612
|
+
statesMapping.setLegacyDevices(legacyModels);
|
|
613
|
+
const modelarr2 = [];
|
|
614
|
+
statesMapping.devices.forEach(item => modelarr2.push(item.models));
|
|
615
|
+
//this.warn('devices are' + modelarr2.join(','));
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
async getDefaultGroupIcon(id) {
|
|
619
|
+
const regexResult = id.match(new RegExp(/group_(\d+)/));
|
|
620
|
+
if (!regexResult) return '';
|
|
621
|
+
const groupID = Number(regexResult[1]);
|
|
622
|
+
const group = await this.adapter.zbController.getGroupMembersFromController(groupID)
|
|
623
|
+
if (typeof group == 'object')
|
|
624
|
+
return `img/group_${Math.max(Math.min(group.length, 7), 0)}.png`
|
|
625
|
+
else
|
|
626
|
+
return 'img/group_x.png'
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
async updateDev(dev_id, dev_name, model, callback) {
|
|
630
|
+
|
|
631
|
+
const __dev_name = this.verifyDeviceName(dev_id, model, (dev_name ? dev_name : model));
|
|
632
|
+
this.debug(`UpdateDev called with ${dev_id}, ${dev_name}, ${model}, ${__dev_name}`);
|
|
481
633
|
const id = '' + dev_id;
|
|
482
634
|
const modelDesc = statesMapping.findModel(model);
|
|
483
|
-
|
|
635
|
+
const modelIcon = (model == 'group' ? await this.getDefaultGroupIcon(dev_id) : modelDesc && modelDesc.icon ? modelDesc.icon : 'img/unknown.png');
|
|
636
|
+
let icon = this.localConfig.IconForId(dev_id, model, modelIcon);
|
|
484
637
|
|
|
485
|
-
// download icon if it external and not
|
|
638
|
+
// download icon if it external and not undefined
|
|
486
639
|
if (model === undefined) {
|
|
487
640
|
this.warn(`download icon ${__dev_name} for undefined Device not available. Check your devices.`);
|
|
488
641
|
} else {
|
|
489
642
|
const model_modif = model.replace(/\//g, '-');
|
|
490
|
-
const
|
|
643
|
+
const pathToAdminIcon = `img/${model_modif}.png`;
|
|
491
644
|
|
|
492
645
|
if (icon.startsWith('http')) {
|
|
493
646
|
try {
|
|
494
|
-
|
|
495
|
-
this.warn(`download icon from ${icon} saved into ${pathToIcon}`);
|
|
496
|
-
this.downloadIcon(icon, pathToIcon);
|
|
497
|
-
}
|
|
647
|
+
this.downloadIconToAdmin(icon, pathToAdminIcon)
|
|
498
648
|
icon = `img/${model_modif}.png`;
|
|
499
649
|
} catch (e) {
|
|
500
|
-
this.warn(`ERROR : icon not found
|
|
650
|
+
this.warn(`ERROR : icon not found at ${icon}`);
|
|
501
651
|
}
|
|
502
652
|
}
|
|
503
653
|
}
|
|
@@ -509,6 +659,7 @@ class StatesController extends EventEmitter {
|
|
|
509
659
|
name: __dev_name,
|
|
510
660
|
type: model,
|
|
511
661
|
icon,
|
|
662
|
+
modelIcon: modelIcon,
|
|
512
663
|
color: null,
|
|
513
664
|
statusStates: {onlineId: `${this.adapter.namespace}.${dev_id}.available`}
|
|
514
665
|
},
|
|
@@ -517,8 +668,10 @@ class StatesController extends EventEmitter {
|
|
|
517
668
|
// update type and icon
|
|
518
669
|
this.adapter.extendObject(id, {
|
|
519
670
|
common: {
|
|
671
|
+
name: __dev_name,
|
|
520
672
|
type: model,
|
|
521
673
|
icon,
|
|
674
|
+
modelIcon: modelIcon,
|
|
522
675
|
color: null,
|
|
523
676
|
statusStates: {onlineId: `${this.adapter.namespace}.${dev_id}.available`}
|
|
524
677
|
}
|
|
@@ -527,26 +680,84 @@ class StatesController extends EventEmitter {
|
|
|
527
680
|
}
|
|
528
681
|
|
|
529
682
|
async downloadIcon(url, image_path) {
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
url
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
683
|
+
try {
|
|
684
|
+
if (!fs.existsSync(image_path)) {
|
|
685
|
+
this.ImagesToDownload.push(url);
|
|
686
|
+
return new Promise((resolve, reject) => {
|
|
687
|
+
this.info(`downloading ${url} to ${image_path}`);
|
|
688
|
+
axios({
|
|
689
|
+
method: 'get',
|
|
690
|
+
url: url,
|
|
691
|
+
responseType: 'stream' // Dies ist wichtig, um den Stream direkt zu erhalten
|
|
692
|
+
}).then(response => {
|
|
693
|
+
const writer = fs.createWriteStream(image_path);
|
|
694
|
+
response.data.pipe(writer);
|
|
695
|
+
writer.on('finish', resolve);
|
|
696
|
+
writer.on('error', reject);
|
|
697
|
+
}).catch(err => {
|
|
698
|
+
// reject(err);
|
|
699
|
+
this.warn(`ERROR : icon path not found ${image_path}`);
|
|
700
|
+
}).finally(() => {
|
|
701
|
+
const idx = this.ImagesToDownload.indexOf(url);
|
|
702
|
+
if (idx > -1) {
|
|
703
|
+
this.ImagesToDownload.splice(idx, 1);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
});
|
|
544
707
|
});
|
|
545
|
-
}
|
|
708
|
+
}
|
|
709
|
+
else {
|
|
710
|
+
this.info(`not downloading ${image_path} - file exists`)
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
catch (error) {
|
|
714
|
+
this.error('downloadIcon ', error);
|
|
546
715
|
}
|
|
547
716
|
}
|
|
548
717
|
|
|
718
|
+
async downloadIconToAdmin(url, target) {
|
|
719
|
+
const namespace = `${this.adapter.name}.admin`;
|
|
720
|
+
this.adapter.fileExists(namespace, target, async (err,result) => {
|
|
721
|
+
if (result) return;
|
|
722
|
+
const src = `${tmpdir()}/${path.basename(target)}`;
|
|
723
|
+
const msg = `downloading ${url} to ${src}`;
|
|
724
|
+
if (this.ImagesToDownload.indexOf(url) ==-1) {
|
|
725
|
+
await this.downloadIcon(url, src)
|
|
726
|
+
try {
|
|
727
|
+
fs.readFile(src, (err, data) => {
|
|
728
|
+
if (err) {
|
|
729
|
+
this.error('unable to read ' + src + ' : '+ (err && err.message? err.message:' no message given'))
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
if (data) {
|
|
733
|
+
this.adapter.writeFile(namespace, target, data, (err) => {
|
|
734
|
+
if (err) {
|
|
735
|
+
this.error('error writing file ' + target + JSON.stringify(err))
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
this.info(`copied ${src} to ${target}.`)
|
|
739
|
+
fs.rm(src, (err) => {
|
|
740
|
+
if (err) this.warn(`error removing ${src} : ${JSON.stringify(err)}`);
|
|
741
|
+
else this.info(`removed ${src}`)
|
|
742
|
+
});
|
|
743
|
+
})
|
|
744
|
+
}
|
|
745
|
+
})
|
|
746
|
+
}
|
|
747
|
+
catch (error) {
|
|
748
|
+
this.error('fs.readfile error : ', error);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
CleanupRequired(set) {
|
|
755
|
+
if (typeof set === 'boolean') this.cleanupRequired = set;
|
|
756
|
+
return this.cleanupRequired;
|
|
757
|
+
}
|
|
758
|
+
|
|
549
759
|
async syncDevStates(dev, model) {
|
|
760
|
+
this.debug('synchronizing device states for ' + dev.ieeeAddr + ' (' + model + ')');
|
|
550
761
|
const devId = dev.ieeeAddr.substr(2);
|
|
551
762
|
// devId - iobroker device id
|
|
552
763
|
const devStates = await this.getDevStates(dev.ieeeAddr, model);
|
|
@@ -590,24 +801,39 @@ class StatesController extends EventEmitter {
|
|
|
590
801
|
};
|
|
591
802
|
this.updateState(devId, statedesc.id, undefined, common);
|
|
592
803
|
}
|
|
804
|
+
this.deleteOrphanedDeviceStates(dev.ieeeAddr, model, false, undefined, true);
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
async getExposes() {
|
|
808
|
+
await this.localConfig.init();
|
|
809
|
+
await this.applyLegacyDevices();
|
|
810
|
+
statesMapping.fillStatesWithExposes();
|
|
593
811
|
}
|
|
594
812
|
|
|
595
|
-
async
|
|
596
|
-
|
|
813
|
+
async elevatedMessage(device, message, isError) {
|
|
814
|
+
if (isError) this.error(message); else this.warn(message);
|
|
815
|
+
// emit data here for debug tab later
|
|
816
|
+
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
async elevatedDebugMessage(id, message, isError) {
|
|
820
|
+
if (isError) this.error(message); else this.warn(message);
|
|
821
|
+
this.emit('debugmessage', {id: id, message:message});
|
|
597
822
|
}
|
|
598
823
|
|
|
599
824
|
async publishToState(devId, model, payload) {
|
|
600
825
|
const devStates = await this.getDevStates(`0x${devId}`, model);
|
|
601
|
-
|
|
602
|
-
|
|
826
|
+
|
|
827
|
+
const has_elevated_debug = (this.checkDebugDevice(devId) && !payload.hasOwnProperty('msg_from_zigbee'));
|
|
828
|
+
|
|
829
|
+
|
|
830
|
+
if (has_elevated_debug)
|
|
603
831
|
{
|
|
604
|
-
|
|
605
|
-
this.warn(`ELEVATED I1: message received '${JSON.stringify(payload)}' from device ${devId} type '${model}'`);
|
|
606
|
-
has_debug = true;
|
|
607
|
-
}
|
|
832
|
+
this.elevatedMessage(devId, `ELEVATED I01: message received '${JSON.stringify(payload)}' from device ${devId} type '${model}'`, false);
|
|
608
833
|
}
|
|
609
834
|
if (!devStates) {
|
|
610
|
-
if (
|
|
835
|
+
if (has_elevated_debug)
|
|
836
|
+
this.elevatedMessage(devId, `ELEVATED IE02: no device states for device ${devId} type '${model}'`, true)
|
|
611
837
|
return;
|
|
612
838
|
}
|
|
613
839
|
// find states for payload
|
|
@@ -634,8 +860,8 @@ class StatesController extends EventEmitter {
|
|
|
634
860
|
|
|
635
861
|
let stateID = statedesc.id;
|
|
636
862
|
|
|
637
|
-
if (
|
|
638
|
-
this.
|
|
863
|
+
if (has_elevated_debug) {
|
|
864
|
+
this.elevatedMessage(devId, `ELEVATED I02: value generated '${JSON.stringify(value)}' from device ${devId} for '${statedesc.name}'`, false);
|
|
639
865
|
}
|
|
640
866
|
|
|
641
867
|
const common = {
|
|
@@ -677,19 +903,20 @@ class StatesController extends EventEmitter {
|
|
|
677
903
|
}
|
|
678
904
|
} catch (e) {
|
|
679
905
|
this.debug(`No states in device ${devId} : payload ${JSON.stringify(payload)}`);
|
|
680
|
-
if (
|
|
681
|
-
this.
|
|
906
|
+
if (has_elevated_debug)
|
|
907
|
+
this.elevatedMessage(devId, `ELEVATED IE03: error when enumerating states of ${devId} for payload ${JSON.stringify(payload)}, ${(e ? e.name : 'undefined')} (${(e ? e.message : '')}).`, true);
|
|
682
908
|
}
|
|
683
|
-
if (!has_published &&
|
|
684
|
-
this.
|
|
909
|
+
if (!has_published && has_elevated_debug) {
|
|
910
|
+
this.elevatedMessage(devId, `ELEVATED IE04: No value published for device ${devId}`, true);
|
|
685
911
|
|
|
686
912
|
}
|
|
687
913
|
}
|
|
688
914
|
else {
|
|
689
|
-
if (
|
|
690
|
-
this.
|
|
915
|
+
if (has_elevated_debug)
|
|
916
|
+
this.elevatedMessage(devId, `ELEVATED IE05: No states matching the payload ${JSON.stringify(payload)} for device ${devId}`, true);
|
|
691
917
|
}
|
|
692
918
|
}
|
|
919
|
+
|
|
693
920
|
}
|
|
694
921
|
|
|
695
922
|
module.exports = StatesController;
|