iobroker.zigbee 2.0.0 → 2.0.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 +40 -10
- 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/de/readme.md +21 -28
- 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 +55 -41
- package/lib/binding.js +1 -1
- package/lib/colors.js +7 -0
- package/lib/commands.js +136 -20
- package/lib/developer.js +0 -0
- package/lib/devices.js +88 -74
- package/lib/exclude.js +30 -54
- package/lib/exposes.js +247 -290
- package/lib/groups.js +84 -29
- package/lib/localConfig.js +301 -0
- package/lib/ota.js +5 -4
- package/lib/statescontroller.js +452 -185
- package/lib/utils.js +5 -3
- package/lib/zbDeviceAvailability.js +16 -30
- package/lib/zbDeviceConfigure.js +55 -28
- package/lib/zbDeviceEvent.js +2 -13
- package/lib/zigbeecontroller.js +335 -214
- package/main.js +181 -65
- 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,14 @@ 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 = {};
|
|
34
|
+
this.stashedUnknownModels = [];
|
|
32
35
|
}
|
|
33
36
|
|
|
34
37
|
info(message, data) {
|
|
@@ -51,30 +54,82 @@ class StatesController extends EventEmitter {
|
|
|
51
54
|
this.adapter.sendError(error, message);
|
|
52
55
|
}
|
|
53
56
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
this.
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
this.
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
57
|
+
TriggerUpload(delay) {
|
|
58
|
+
if (this.timeoutHandleUpload) return;
|
|
59
|
+
this.warn('triggering upload')
|
|
60
|
+
this.timeoutHandleUpload = this.adapter.setTimeout(() => {
|
|
61
|
+
try {
|
|
62
|
+
this.warn('executing upload')
|
|
63
|
+
exec('iobroker upload zigbee', (error, stdout, stderr) => {
|
|
64
|
+
if (error) this.error('exec error: ' + JSON.stringify(error));
|
|
65
|
+
this.warn('upload done');
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
this.error('error on upload exec');
|
|
70
|
+
}
|
|
71
|
+
}, delay);
|
|
65
72
|
}
|
|
66
73
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
74
|
+
getStashedErrors() {
|
|
75
|
+
const rv = [];
|
|
76
|
+
if (Object.keys(this.stashedErrors).length > 0)
|
|
77
|
+
{
|
|
78
|
+
rv.push('<p><b>Stashed Messages</b></p>')
|
|
79
|
+
rv.push(Object.values(this.stashedErrors).join('<br>'));
|
|
80
|
+
}
|
|
81
|
+
if (this.stashedUnknownModels.length > 0) {
|
|
82
|
+
rv.push('<p><b>Devices whithout Model definition</b></p>')
|
|
83
|
+
rv.push()
|
|
84
|
+
}
|
|
85
|
+
return rv;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async AddModelFromHerdsman(device, model) {
|
|
89
|
+
// this.warn('addModelFromHerdsman ' + JSON.stringify(model) + ' ' + JSON.stringify(this.localConfig.getOverrideWithKey(model, 'legacy', true)));
|
|
90
|
+
if (this.localConfig.getOverrideWithKey(model, 'legacy', true)) {
|
|
91
|
+
//this.warn('legacy for ' + model);
|
|
92
|
+
await this.addLegacyDevice(model);
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
const pre = statesMapping.devices.length;
|
|
96
|
+
await statesMapping.addExposeToDevices(device, this, model);
|
|
97
|
+
const post = statesMapping.devices.length;
|
|
98
|
+
//this.warn('expose for ' + model + ' '+ pre + '->'+post);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async getDebugDevices(callback) {
|
|
103
|
+
if (this.debugDevices === undefined) {
|
|
104
|
+
this.debugDevices = [];
|
|
105
|
+
const state = await this.adapter.getStateAsync(`${this.adapter.namespace}.info.debugmessages`);
|
|
70
106
|
if (state) {
|
|
71
107
|
if (typeof state.val === 'string' && state.val.length > 2) {
|
|
72
108
|
this.debugDevices = state.val.split(';');
|
|
73
109
|
}
|
|
74
110
|
this.info(`debug devices set to ${JSON.stringify(this.debugDevices)}`);
|
|
111
|
+
if (callback) callback(this.debugDevices);
|
|
75
112
|
}
|
|
76
|
-
}
|
|
113
|
+
} else {
|
|
114
|
+
// this.info(`debug devices was already set to ${JSON.stringify(this.debugDevices)}`);
|
|
115
|
+
callback(this.debugDevices)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
77
118
|
|
|
119
|
+
async toggleDeviceDebug(id) {
|
|
120
|
+
const arr = /zigbee.[0-9].([^.]+)/gm.exec(id);
|
|
121
|
+
if (arr[1] === undefined) {
|
|
122
|
+
this.warn(`unable to extract id from state ${id}`);
|
|
123
|
+
return [];
|
|
124
|
+
}
|
|
125
|
+
const stateKey = arr[1];
|
|
126
|
+
if (this.debugDevices === undefined) this.debugDevices = await this.getDebugDevices()
|
|
127
|
+
const idx = this.debugDevices.indexOf(stateKey);
|
|
128
|
+
if (idx < 0) this.debugDevices.push(stateKey);
|
|
129
|
+
else this.debugDevices.splice(idx, 1);
|
|
130
|
+
await this.adapter.setStateAsync(`${this.adapter.namespace}.info.debugmessages`, this.debugDevices.join(';'), true);
|
|
131
|
+
this.info('debug devices set to ' + JSON.stringify(this.debugDevices));
|
|
132
|
+
return this.debugDevices;
|
|
78
133
|
}
|
|
79
134
|
|
|
80
135
|
checkDebugDevice(dev) {
|
|
@@ -82,9 +137,12 @@ class StatesController extends EventEmitter {
|
|
|
82
137
|
if (this.debugDevices === undefined) {
|
|
83
138
|
this.getDebugDevices();
|
|
84
139
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
140
|
+
else
|
|
141
|
+
{
|
|
142
|
+
for (const addressPart of this.debugDevices) {
|
|
143
|
+
if (typeof dev === 'string' && dev.includes(addressPart)) {
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
88
146
|
}
|
|
89
147
|
}
|
|
90
148
|
return false;
|
|
@@ -101,10 +159,10 @@ class StatesController extends EventEmitter {
|
|
|
101
159
|
if (id.endsWith('debugmessages')) {
|
|
102
160
|
if (typeof state.val === 'string' && state.val.length > 2) {
|
|
103
161
|
this.debugDevices = state.val.split(';');
|
|
104
|
-
|
|
105
162
|
} else {
|
|
106
163
|
this.debugDevices = [];
|
|
107
164
|
}
|
|
165
|
+
this.info('debug devices set to ' + JSON.stringify(this.debugDevices));
|
|
108
166
|
return;
|
|
109
167
|
}
|
|
110
168
|
|
|
@@ -132,7 +190,13 @@ class StatesController extends EventEmitter {
|
|
|
132
190
|
return;
|
|
133
191
|
}
|
|
134
192
|
if (model === 'group') {
|
|
135
|
-
|
|
193
|
+
const match = deviceId.match(/group_(\d+)/gm);
|
|
194
|
+
if (match) {
|
|
195
|
+
deviceId = parseInt(match[1]);
|
|
196
|
+
this.publishFromState(deviceId, model, stateKey, state, {});
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
136
200
|
}
|
|
137
201
|
this.collectOptions(id.split('.')[2], model, options =>
|
|
138
202
|
this.publishFromState(deviceId, model, stateKey, state, options));
|
|
@@ -143,7 +207,7 @@ class StatesController extends EventEmitter {
|
|
|
143
207
|
|
|
144
208
|
async collectOptions(devId, model, callback) {
|
|
145
209
|
const result = {};
|
|
146
|
-
// find model states for options and get it values
|
|
210
|
+
// find model states for options and get it values. No options for groups !!!
|
|
147
211
|
const devStates = await this.getDevStates('0x' + devId, model);
|
|
148
212
|
if (devStates == null || devStates == undefined || devStates.states == null || devStates.states == undefined) {
|
|
149
213
|
callback(result);
|
|
@@ -188,7 +252,13 @@ class StatesController extends EventEmitter {
|
|
|
188
252
|
} else {
|
|
189
253
|
stateModel = statesMapping.findModel(model);
|
|
190
254
|
if (!stateModel) {
|
|
191
|
-
|
|
255
|
+
// try to get the model from the exposes
|
|
256
|
+
if (!this.stashedErrors.hasOwnProperty(`${deviceId}.nomodel`))
|
|
257
|
+
{
|
|
258
|
+
this.warn(`Device ${deviceId} "${model}" not found.`);
|
|
259
|
+
this.stashedErrors[`${deviceId}.nomodel`] = `Device ${deviceId} "${model}" not found.`;
|
|
260
|
+
}
|
|
261
|
+
|
|
192
262
|
states = statesMapping.commonStates;
|
|
193
263
|
} else {
|
|
194
264
|
states = stateModel.states;
|
|
@@ -207,6 +277,34 @@ class StatesController extends EventEmitter {
|
|
|
207
277
|
}
|
|
208
278
|
}
|
|
209
279
|
|
|
280
|
+
async triggerComposite(_deviceId, model, stateDesc, interactive) {
|
|
281
|
+
const deviceId = (_deviceId.replace('0x', ''));
|
|
282
|
+
const idParts = stateDesc.id.split('.').slice(-2);
|
|
283
|
+
const key = `${deviceId}.${idParts[0]}`;
|
|
284
|
+
const handle = this.compositeData[key]
|
|
285
|
+
const factor = (interactive ? 10 : 1);
|
|
286
|
+
if (handle) this.adapter.clearTimeout(handle);
|
|
287
|
+
this.compositeData[key] = this.adapter.setTimeout(async () => {
|
|
288
|
+
delete this.compositeData[key];
|
|
289
|
+
const parts = stateDesc.id.split('.');
|
|
290
|
+
parts.pop();
|
|
291
|
+
|
|
292
|
+
this.adapter.getStatesOf(deviceId, parts.join('.'),async (err,states) => {
|
|
293
|
+
if (!err && states) {
|
|
294
|
+
const components = {};
|
|
295
|
+
for (const stateobj of states) {
|
|
296
|
+
const ckey = stateobj._id.split('.').pop();
|
|
297
|
+
const state = await this.adapter.getState(stateobj._id);
|
|
298
|
+
if (state && state.val != undefined) {
|
|
299
|
+
components[ckey] = state.val;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
this.adapter.setState(`${deviceId}.${stateDesc.compositeState}`, JSON.stringify(components));
|
|
303
|
+
}
|
|
304
|
+
})
|
|
305
|
+
}, (stateDesc.compositeTimeout ? stateDesc.compositeTimeout : 100) * factor);
|
|
306
|
+
}
|
|
307
|
+
|
|
210
308
|
async publishFromState(deviceId, model, stateKey, state, options) {
|
|
211
309
|
this.debug(`Change state '${stateKey}' at device ${deviceId} type '${model}'`);
|
|
212
310
|
const elevated = this.checkDebugDevice(deviceId);
|
|
@@ -232,7 +330,7 @@ class StatesController extends EventEmitter {
|
|
|
232
330
|
this.error(`ELEVATED OE2: no value for device ${deviceId} type '${model}'`);
|
|
233
331
|
return;
|
|
234
332
|
}
|
|
235
|
-
let stateList = [{stateDesc: stateDesc, value: value, index: 0, timeout: 0}];
|
|
333
|
+
let stateList = [{stateDesc: stateDesc, value: value, index: 0, timeout: 0, source:state.from}];
|
|
236
334
|
if (stateModel && stateModel.linkedStates) {
|
|
237
335
|
stateModel.linkedStates.forEach(linkedFunct => {
|
|
238
336
|
try {
|
|
@@ -264,55 +362,51 @@ class StatesController extends EventEmitter {
|
|
|
264
362
|
this.emit('changed', deviceId, model, stateModel, stateList, options);
|
|
265
363
|
}
|
|
266
364
|
|
|
267
|
-
renameDevice(id, newName) {
|
|
268
|
-
this.
|
|
269
|
-
this.adapter.
|
|
365
|
+
async renameDevice(id, newName) {
|
|
366
|
+
const stateId = id.replace(`${this.adapter.namespace}.`, '')
|
|
367
|
+
const obj = await this.adapter.getObjectAsync(id);
|
|
368
|
+
if (newName == null || newName == undefined) newName = '';
|
|
369
|
+
let objName = newName;
|
|
370
|
+
if (newName.length < 1 || newName == obj.common.type) {
|
|
371
|
+
objName = (obj.common.type == 'group' ? stateId.replace('_',' ') : obj.common.type);
|
|
372
|
+
}
|
|
373
|
+
this.localConfig.updateDeviceName(stateId, newName);
|
|
374
|
+
this.debug('rename device: newname ~' + newName + '~ objName ~' + objName + '~')
|
|
375
|
+
this.adapter.extendObject(id, {common: {name: objName}});
|
|
270
376
|
}
|
|
271
377
|
|
|
272
|
-
|
|
273
|
-
this.adapter.
|
|
378
|
+
verifyDeviceName(id, model ,name) {
|
|
379
|
+
const savedId = id.replace(`${this.adapter.namespace}.`, '');
|
|
380
|
+
return this.localConfig.NameForId(id, model, name);
|
|
274
381
|
}
|
|
275
382
|
|
|
276
|
-
async rebuildRetainDeviceNames() {
|
|
277
|
-
savedDeviceNamesDB = {};
|
|
278
|
-
const devList = await this.adapter.getAdapterObjectsAsync();
|
|
279
383
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
savedDeviceNamesDB[devList[key]._id.replace(`${this.adapter.namespace}.`, '')] = devList[key].common.name;
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
this.retainDeviceNames();
|
|
384
|
+
setDeviceActivated(id, active) {
|
|
385
|
+
this.adapter.extendObject(id, {common: {deactivated: active}});
|
|
286
386
|
}
|
|
287
387
|
|
|
288
388
|
storeDeviceName(id, name) {
|
|
289
|
-
|
|
290
|
-
this.retainDeviceNames();
|
|
389
|
+
return this.localConfig.updateDeviceName(id, name);
|
|
291
390
|
}
|
|
292
391
|
|
|
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
392
|
|
|
302
393
|
async deleteObj(devId) {
|
|
303
394
|
const options = { recursive:true };
|
|
304
395
|
try {
|
|
305
|
-
this.adapter.delObject(devId,options), (err) => {
|
|
396
|
+
this.adapter.delObject(devId,options), (err) => {
|
|
397
|
+
this.adapter.log.info(`Cannot delete Object ${devId}: ${err}`);
|
|
398
|
+
}
|
|
306
399
|
|
|
307
|
-
} catch (
|
|
308
|
-
this.adapter.log.info(`Cannot delete
|
|
400
|
+
} catch (error) {
|
|
401
|
+
this.adapter.log.info(`Cannot delete Object ${devId}: ${error && error.message ? error.message : 'without error message'}`);
|
|
309
402
|
}
|
|
310
403
|
}
|
|
311
404
|
|
|
312
|
-
async deleteOrphanedDeviceStates(ieeeAddr, model, force, callback) {
|
|
405
|
+
async deleteOrphanedDeviceStates(ieeeAddr, model, force, callback, markOnly) {
|
|
313
406
|
const devStates = await this.getDevStates(ieeeAddr, model);
|
|
314
407
|
const commonStates = statesMapping.commonStates;
|
|
315
408
|
const devId = ieeeAddr.substr(2);
|
|
409
|
+
const messages = [];
|
|
316
410
|
this.adapter.getStatesOf(devId, (err, states) => {
|
|
317
411
|
if (!err && states) {
|
|
318
412
|
states.forEach((state) => {
|
|
@@ -330,17 +424,35 @@ class StatesController extends EventEmitter {
|
|
|
330
424
|
if (commonStates.find(statedesc => statename === statedesc.id) === undefined &&
|
|
331
425
|
devStates.states.find(statedesc => statename === statedesc.id) === undefined
|
|
332
426
|
) {
|
|
333
|
-
|
|
334
|
-
|
|
427
|
+
this.cleanupRequired |= markOnly;
|
|
428
|
+
if (state.common.hasOwnProperty('custom') && !force && !markOnly) {
|
|
429
|
+
messages.push(`keeping disconnected state ${JSON.stringify(statename)} of ${devId} `)
|
|
430
|
+
this.info(`keeping disconnected state ${JSON.stringify(statename)} of ${devId} `);
|
|
431
|
+
this.cleanupRequired == true;
|
|
335
432
|
} else {
|
|
336
|
-
|
|
337
|
-
|
|
433
|
+
if (markOnly) {
|
|
434
|
+
this.adapter.extendObjectAsync(devId.concat('.',statename), {common: {color:'#E67E22'}})
|
|
435
|
+
|
|
436
|
+
}
|
|
437
|
+
else
|
|
438
|
+
{
|
|
439
|
+
try {
|
|
440
|
+
this.info(`deleting disconnected state ${JSON.stringify(statename)} of ${devId} `);
|
|
441
|
+
messages.push(`deleting disconnected state ${JSON.stringify(statename)} of ${devId} `);
|
|
442
|
+
this.deleteObj(devId.concat('.',statename));
|
|
443
|
+
}
|
|
444
|
+
catch (error) {
|
|
445
|
+
//messages.push(`ERROR: failed to delete state ${JSON.stringify(statename)} of ${devId} `)
|
|
446
|
+
}
|
|
447
|
+
}
|
|
338
448
|
}
|
|
339
449
|
} else {
|
|
340
450
|
this.debug(`keeping connecte state ${JSON.stringify(statename)} of ${devId} `);
|
|
451
|
+
messages.push(`keeping connecte state ${JSON.stringify(statename)} of ${devId} `);
|
|
341
452
|
}
|
|
342
453
|
});
|
|
343
454
|
}
|
|
455
|
+
if (callback) callback(messages);
|
|
344
456
|
});
|
|
345
457
|
}
|
|
346
458
|
|
|
@@ -349,89 +461,134 @@ class StatesController extends EventEmitter {
|
|
|
349
461
|
setTimeout(() => this.updateState(dev_id, name, outValue, common), timeout);
|
|
350
462
|
}
|
|
351
463
|
|
|
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
|
-
}
|
|
464
|
+
async updateState(devId, name, value, common) {
|
|
465
|
+
const obj = await this.adapter.getObjectAsync(devId)
|
|
466
|
+
if (obj) {
|
|
467
|
+
if (!obj.common.deactivated) {
|
|
468
|
+
const new_common = {name: name, color:null};
|
|
469
|
+
const stateId = devId + '.' + name;
|
|
470
|
+
const new_name = obj.common.name;
|
|
471
|
+
if (common) {
|
|
472
|
+
if (common.name !== undefined) {
|
|
473
|
+
new_common.name = common.name;
|
|
390
474
|
}
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
475
|
+
if (common.type !== undefined) {
|
|
476
|
+
new_common.type = common.type;
|
|
477
|
+
}
|
|
478
|
+
if (common.unit !== undefined) {
|
|
479
|
+
new_common.unit = common.unit;
|
|
480
|
+
}
|
|
481
|
+
if (common.states !== undefined) {
|
|
482
|
+
new_common.states = common.states;
|
|
483
|
+
}
|
|
484
|
+
if (common.read !== undefined) {
|
|
485
|
+
new_common.read = common.read;
|
|
486
|
+
}
|
|
487
|
+
if (common.write !== undefined) {
|
|
488
|
+
new_common.write = common.write;
|
|
489
|
+
}
|
|
490
|
+
if (common.role !== undefined) {
|
|
491
|
+
new_common.role = common.role;
|
|
492
|
+
}
|
|
493
|
+
if (common.min !== undefined) {
|
|
494
|
+
new_common.min = common.min;
|
|
495
|
+
}
|
|
496
|
+
if (common.max !== undefined) {
|
|
497
|
+
new_common.max = common.max;
|
|
498
|
+
}
|
|
499
|
+
if (common.icon !== undefined) {
|
|
500
|
+
new_common.icon = common.icon;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
// check if state exist
|
|
504
|
+
const stobj = await this.adapter.getObjectAsync(stateId);
|
|
505
|
+
let hasChanges = false;
|
|
506
|
+
if (stobj) {
|
|
507
|
+
// update state - not change name and role (user can it changed)
|
|
508
|
+
if (stobj.common.name) {
|
|
509
|
+
delete new_common.name;
|
|
510
|
+
} else {
|
|
511
|
+
new_common.name = `${new_name} ${new_common.name}`;
|
|
512
|
+
}
|
|
513
|
+
delete new_common.role;
|
|
514
|
+
|
|
515
|
+
// check whether any common property is different
|
|
516
|
+
if (stobj.common) {
|
|
517
|
+
for (const property in new_common) {
|
|
518
|
+
if (stobj.common.hasOwnProperty(property)) {
|
|
519
|
+
if (stobj.common[property] === new_common[property]) {
|
|
520
|
+
delete new_common[property];
|
|
521
|
+
} else {
|
|
522
|
+
hasChanges = true;
|
|
413
523
|
}
|
|
414
524
|
}
|
|
415
|
-
} else {
|
|
416
|
-
hasChanges = true;
|
|
417
525
|
}
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
526
|
+
}
|
|
527
|
+
} else {
|
|
528
|
+
const matches = stateId.match((/\./g));
|
|
529
|
+
if (matches && matches.length>1) {
|
|
530
|
+
const channels = stateId.split('.');
|
|
531
|
+
const SubChannels = [channels.shift()];
|
|
532
|
+
channels.pop();
|
|
533
|
+
for (const channel of channels) {
|
|
534
|
+
SubChannels.push(channel);
|
|
535
|
+
const id = SubChannels.join('.');
|
|
536
|
+
await this.adapter.extendObjectAsync(id, {type: 'channel', common: { name:channel}, native:{}})
|
|
425
537
|
}
|
|
538
|
+
}
|
|
539
|
+
hasChanges = true;
|
|
540
|
+
}
|
|
426
541
|
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
542
|
+
// only change object when any common property has changed
|
|
543
|
+
// first check value
|
|
544
|
+
if (value !== undefined) {
|
|
545
|
+
const type = stobj ? stobj.common.type : new_common.type;
|
|
546
|
+
if (type === 'number') {
|
|
547
|
+
const minval = (stobj ? stobj.common.min: new_common.min) || -Number.MAX_VALUE;
|
|
548
|
+
const maxval = (stobj ? stobj.common.max: new_common.max) || Number.MAX_VALUE;
|
|
549
|
+
let nval = (typeof value == 'number' ? value : parseFloat(value));
|
|
550
|
+
if (isNaN(nval)) {
|
|
551
|
+
if (minval !== undefined && typeof minval === 'number')
|
|
552
|
+
nval = minval;
|
|
553
|
+
else
|
|
554
|
+
nval = 0;
|
|
555
|
+
}
|
|
556
|
+
if (nval < minval) {
|
|
557
|
+
hasChanges = true;
|
|
558
|
+
new_common.color = '#FF0000'
|
|
559
|
+
value = minval
|
|
560
|
+
if (!this.stashedErrors.hasOwnProperty(`${stateId}.min`))
|
|
561
|
+
{
|
|
562
|
+
this.error(`State value for ${stateId} has value "${nval}" less than min "${minval}". - this eror is recorded and not repeated`);
|
|
563
|
+
this.stashedErrors[`${stateId}.min`] = `State value for ${stateId} has value "${nval}." less than min "${minval}"`;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
if (nval > maxval) {
|
|
567
|
+
hasChanges = true;
|
|
568
|
+
hasChanges = true;
|
|
569
|
+
new_common.color = '#FF0000'
|
|
570
|
+
value = maxval;
|
|
571
|
+
if (!this.stashedErrors.hasOwnProperty(`${stateId}.max`))
|
|
572
|
+
{
|
|
573
|
+
this.error(`State value for ${stateId} has value "${nval}" more than max "${maxval}". - this eror is recorded and not repeated`);
|
|
574
|
+
this.stashedErrors[`${stateId}.max`] = `State value for ${stateId} has value "${nval}" more than max "${maxval}".`;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
//
|
|
580
|
+
if (hasChanges) {
|
|
581
|
+
this.adapter.extendObject(stateId, {type: 'state', common: new_common, native: {}}, () =>
|
|
582
|
+
value !== undefined && this.setState_typed(stateId, value, true, stobj ? stobj.common.type : new_common.type));
|
|
583
|
+
} else if (value !== undefined) {
|
|
584
|
+
this.setState_typed(stateId, value, true, stobj.common.type);
|
|
430
585
|
}
|
|
431
586
|
} else {
|
|
432
|
-
this.debug(`UpdateState:
|
|
587
|
+
this.debug(`UpdateState: Device is deactivated ${devId} ${JSON.stringify(obj)}`);
|
|
433
588
|
}
|
|
434
|
-
}
|
|
589
|
+
} else {
|
|
590
|
+
this.debug(`UpdateState: missing device ${devId} ${JSON.stringify(obj)}`);
|
|
591
|
+
}
|
|
435
592
|
}
|
|
436
593
|
|
|
437
594
|
setState_typed(id, value, ack, type, callback) {
|
|
@@ -476,28 +633,56 @@ class StatesController extends EventEmitter {
|
|
|
476
633
|
this.adapter.setState(id, value, ack, callback);
|
|
477
634
|
}
|
|
478
635
|
|
|
479
|
-
|
|
480
|
-
const
|
|
636
|
+
async applyLegacyDevices() {
|
|
637
|
+
const legacyModels = await this.localConfig.getLegacyModels();
|
|
638
|
+
const modelarr1 = [];
|
|
639
|
+
//this.warn('devices are' + modelarr1.join(','));
|
|
640
|
+
statesMapping.devices.forEach(item => modelarr1.push(item.models));
|
|
641
|
+
//this.warn('legacy models are ' + JSON.stringify(legacyModels));
|
|
642
|
+
statesMapping.setLegacyDevices(legacyModels);
|
|
643
|
+
const modelarr2 = [];
|
|
644
|
+
statesMapping.devices.forEach(item => modelarr2.push(item.models));
|
|
645
|
+
//this.warn('devices are' + modelarr2.join(','));
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
async addLegacyDevice(model) {
|
|
649
|
+
statesMapping.setLegacyDevices([model]);
|
|
650
|
+
statesMapping.getByModel();
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
async getDefaultGroupIcon(id) {
|
|
654
|
+
const regexResult = id.match(new RegExp(/group_(\d+)/));
|
|
655
|
+
if (!regexResult) return '';
|
|
656
|
+
const groupID = Number(regexResult[1]);
|
|
657
|
+
const group = await this.adapter.zbController.getGroupMembersFromController(groupID)
|
|
658
|
+
if (typeof group == 'object')
|
|
659
|
+
return `img/group_${Math.max(Math.min(group.length, 7), 0)}.png`
|
|
660
|
+
else
|
|
661
|
+
return 'img/group_x.png'
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
async updateDev(dev_id, dev_name, model, callback) {
|
|
665
|
+
|
|
666
|
+
const __dev_name = this.verifyDeviceName(dev_id, model, (dev_name ? dev_name : model));
|
|
667
|
+
this.debug(`UpdateDev called with ${dev_id}, ${dev_name}, ${model}, ${__dev_name}`);
|
|
481
668
|
const id = '' + dev_id;
|
|
482
669
|
const modelDesc = statesMapping.findModel(model);
|
|
483
|
-
|
|
670
|
+
const modelIcon = (model == 'group' ? await this.getDefaultGroupIcon(dev_id) : modelDesc && modelDesc.icon ? modelDesc.icon : 'img/unknown.png');
|
|
671
|
+
let icon = this.localConfig.IconForId(dev_id, model, modelIcon);
|
|
484
672
|
|
|
485
|
-
// download icon if it external and not
|
|
673
|
+
// download icon if it external and not undefined
|
|
486
674
|
if (model === undefined) {
|
|
487
675
|
this.warn(`download icon ${__dev_name} for undefined Device not available. Check your devices.`);
|
|
488
676
|
} else {
|
|
489
677
|
const model_modif = model.replace(/\//g, '-');
|
|
490
|
-
const
|
|
678
|
+
const pathToAdminIcon = `img/${model_modif}.png`;
|
|
491
679
|
|
|
492
680
|
if (icon.startsWith('http')) {
|
|
493
681
|
try {
|
|
494
|
-
|
|
495
|
-
this.warn(`download icon from ${icon} saved into ${pathToIcon}`);
|
|
496
|
-
this.downloadIcon(icon, pathToIcon);
|
|
497
|
-
}
|
|
682
|
+
this.downloadIconToAdmin(icon, pathToAdminIcon)
|
|
498
683
|
icon = `img/${model_modif}.png`;
|
|
499
684
|
} catch (e) {
|
|
500
|
-
this.warn(`ERROR : icon not found
|
|
685
|
+
this.warn(`ERROR : icon not found at ${icon}`);
|
|
501
686
|
}
|
|
502
687
|
}
|
|
503
688
|
}
|
|
@@ -509,6 +694,7 @@ class StatesController extends EventEmitter {
|
|
|
509
694
|
name: __dev_name,
|
|
510
695
|
type: model,
|
|
511
696
|
icon,
|
|
697
|
+
modelIcon: modelIcon,
|
|
512
698
|
color: null,
|
|
513
699
|
statusStates: {onlineId: `${this.adapter.namespace}.${dev_id}.available`}
|
|
514
700
|
},
|
|
@@ -517,8 +703,10 @@ class StatesController extends EventEmitter {
|
|
|
517
703
|
// update type and icon
|
|
518
704
|
this.adapter.extendObject(id, {
|
|
519
705
|
common: {
|
|
706
|
+
name: __dev_name,
|
|
520
707
|
type: model,
|
|
521
708
|
icon,
|
|
709
|
+
modelIcon: modelIcon,
|
|
522
710
|
color: null,
|
|
523
711
|
statusStates: {onlineId: `${this.adapter.namespace}.${dev_id}.available`}
|
|
524
712
|
}
|
|
@@ -527,26 +715,84 @@ class StatesController extends EventEmitter {
|
|
|
527
715
|
}
|
|
528
716
|
|
|
529
717
|
async downloadIcon(url, image_path) {
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
url
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
718
|
+
try {
|
|
719
|
+
if (!fs.existsSync(image_path)) {
|
|
720
|
+
this.ImagesToDownload.push(url);
|
|
721
|
+
return new Promise((resolve, reject) => {
|
|
722
|
+
this.info(`downloading ${url} to ${image_path}`);
|
|
723
|
+
axios({
|
|
724
|
+
method: 'get',
|
|
725
|
+
url: url,
|
|
726
|
+
responseType: 'stream' // Dies ist wichtig, um den Stream direkt zu erhalten
|
|
727
|
+
}).then(response => {
|
|
728
|
+
const writer = fs.createWriteStream(image_path);
|
|
729
|
+
response.data.pipe(writer);
|
|
730
|
+
writer.on('finish', resolve);
|
|
731
|
+
writer.on('error', reject);
|
|
732
|
+
}).catch(err => {
|
|
733
|
+
// reject(err);
|
|
734
|
+
this.warn(`ERROR : icon path not found ${image_path}`);
|
|
735
|
+
}).finally(() => {
|
|
736
|
+
const idx = this.ImagesToDownload.indexOf(url);
|
|
737
|
+
if (idx > -1) {
|
|
738
|
+
this.ImagesToDownload.splice(idx, 1);
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
});
|
|
544
742
|
});
|
|
545
|
-
}
|
|
743
|
+
}
|
|
744
|
+
else {
|
|
745
|
+
this.info(`not downloading ${image_path} - file exists`)
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
catch (error) {
|
|
749
|
+
this.error('downloadIcon ', error);
|
|
546
750
|
}
|
|
547
751
|
}
|
|
548
752
|
|
|
753
|
+
async downloadIconToAdmin(url, target) {
|
|
754
|
+
const namespace = `${this.adapter.name}.admin`;
|
|
755
|
+
this.adapter.fileExists(namespace, target, async (err,result) => {
|
|
756
|
+
if (result) return;
|
|
757
|
+
const src = `${tmpdir()}/${path.basename(target)}`;
|
|
758
|
+
const msg = `downloading ${url} to ${src}`;
|
|
759
|
+
if (this.ImagesToDownload.indexOf(url) ==-1) {
|
|
760
|
+
await this.downloadIcon(url, src)
|
|
761
|
+
try {
|
|
762
|
+
fs.readFile(src, (err, data) => {
|
|
763
|
+
if (err) {
|
|
764
|
+
this.error('unable to read ' + src + ' : '+ (err && err.message? err.message:' no message given'))
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
if (data) {
|
|
768
|
+
this.adapter.writeFile(namespace, target, data, (err) => {
|
|
769
|
+
if (err) {
|
|
770
|
+
this.error('error writing file ' + target + JSON.stringify(err))
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
this.info(`copied ${src} to ${target}.`)
|
|
774
|
+
fs.rm(src, (err) => {
|
|
775
|
+
if (err) this.warn(`error removing ${src} : ${JSON.stringify(err)}`);
|
|
776
|
+
else this.info(`removed ${src}`)
|
|
777
|
+
});
|
|
778
|
+
})
|
|
779
|
+
}
|
|
780
|
+
})
|
|
781
|
+
}
|
|
782
|
+
catch (error) {
|
|
783
|
+
this.error('fs.readfile error : ', error);
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
CleanupRequired(set) {
|
|
790
|
+
if (typeof set === 'boolean') this.cleanupRequired = set;
|
|
791
|
+
return this.cleanupRequired;
|
|
792
|
+
}
|
|
793
|
+
|
|
549
794
|
async syncDevStates(dev, model) {
|
|
795
|
+
this.debug('synchronizing device states for ' + dev.ieeeAddr + ' (' + model + ')');
|
|
550
796
|
const devId = dev.ieeeAddr.substr(2);
|
|
551
797
|
// devId - iobroker device id
|
|
552
798
|
const devStates = await this.getDevStates(dev.ieeeAddr, model);
|
|
@@ -590,24 +836,44 @@ class StatesController extends EventEmitter {
|
|
|
590
836
|
};
|
|
591
837
|
this.updateState(devId, statedesc.id, undefined, common);
|
|
592
838
|
}
|
|
839
|
+
this.deleteOrphanedDeviceStates(dev.ieeeAddr, model, false, undefined, true);
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
async getExposes() {
|
|
843
|
+
await this.localConfig.init();
|
|
844
|
+
await this.applyLegacyDevices();
|
|
845
|
+
try {
|
|
846
|
+
statesMapping.fillStatesWithExposes(this);
|
|
847
|
+
}
|
|
848
|
+
catch (error) {
|
|
849
|
+
this.error(`Error applying exposes: ${error && error.message ? error.message : 'no error message'} ${error && error.stack ? error.stack : ''}`);
|
|
850
|
+
}
|
|
593
851
|
}
|
|
594
852
|
|
|
595
|
-
async
|
|
596
|
-
|
|
853
|
+
async elevatedMessage(device, message, isError) {
|
|
854
|
+
if (isError) this.error(message); else this.warn(message);
|
|
855
|
+
// emit data here for debug tab later
|
|
856
|
+
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
async elevatedDebugMessage(id, message, isError) {
|
|
860
|
+
if (isError) this.error(message); else this.warn(message);
|
|
861
|
+
this.emit('debugmessage', {id: id, message:message});
|
|
597
862
|
}
|
|
598
863
|
|
|
599
864
|
async publishToState(devId, model, payload) {
|
|
600
865
|
const devStates = await this.getDevStates(`0x${devId}`, model);
|
|
601
|
-
|
|
602
|
-
|
|
866
|
+
|
|
867
|
+
const has_elevated_debug = (this.checkDebugDevice(devId) && !payload.hasOwnProperty('msg_from_zigbee'));
|
|
868
|
+
|
|
869
|
+
|
|
870
|
+
if (has_elevated_debug)
|
|
603
871
|
{
|
|
604
|
-
|
|
605
|
-
this.warn(`ELEVATED I1: message received '${JSON.stringify(payload)}' from device ${devId} type '${model}'`);
|
|
606
|
-
has_debug = true;
|
|
607
|
-
}
|
|
872
|
+
this.elevatedMessage(devId, `ELEVATED I01: message received '${JSON.stringify(payload)}' from device ${devId} type '${model}'`, false);
|
|
608
873
|
}
|
|
609
874
|
if (!devStates) {
|
|
610
|
-
if (
|
|
875
|
+
if (has_elevated_debug)
|
|
876
|
+
this.elevatedMessage(devId, `ELEVATED IE02: no device states for device ${devId} type '${model}'`, true)
|
|
611
877
|
return;
|
|
612
878
|
}
|
|
613
879
|
// find states for payload
|
|
@@ -634,8 +900,8 @@ class StatesController extends EventEmitter {
|
|
|
634
900
|
|
|
635
901
|
let stateID = statedesc.id;
|
|
636
902
|
|
|
637
|
-
if (
|
|
638
|
-
this.
|
|
903
|
+
if (has_elevated_debug) {
|
|
904
|
+
this.elevatedMessage(devId, `ELEVATED I02: value generated '${JSON.stringify(value)}' from device ${devId} for '${statedesc.name}'`, false);
|
|
639
905
|
}
|
|
640
906
|
|
|
641
907
|
const common = {
|
|
@@ -662,7 +928,7 @@ class StatesController extends EventEmitter {
|
|
|
662
928
|
|
|
663
929
|
// if needs to return value to back after timeout
|
|
664
930
|
if (statedesc.isEvent) {
|
|
665
|
-
this.updateStateWithTimeout(devId, statedesc.id, value, common, 300, !value);
|
|
931
|
+
this.updateStateWithTimeout(devId, statedesc.id, value, common, 300, (typeof value == typeof (!value) ? !value : ''));
|
|
666
932
|
} else {
|
|
667
933
|
if (statedesc.prepublish) {
|
|
668
934
|
this.collectOptions(devId, model, options =>
|
|
@@ -677,19 +943,20 @@ class StatesController extends EventEmitter {
|
|
|
677
943
|
}
|
|
678
944
|
} catch (e) {
|
|
679
945
|
this.debug(`No states in device ${devId} : payload ${JSON.stringify(payload)}`);
|
|
680
|
-
if (
|
|
681
|
-
this.
|
|
946
|
+
if (has_elevated_debug)
|
|
947
|
+
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
948
|
}
|
|
683
|
-
if (!has_published &&
|
|
684
|
-
this.
|
|
949
|
+
if (!has_published && has_elevated_debug) {
|
|
950
|
+
this.elevatedMessage(devId, `ELEVATED IE04: No value published for device ${devId}`, true);
|
|
685
951
|
|
|
686
952
|
}
|
|
687
953
|
}
|
|
688
954
|
else {
|
|
689
|
-
if (
|
|
690
|
-
this.
|
|
955
|
+
if (has_elevated_debug)
|
|
956
|
+
this.elevatedMessage(devId, `ELEVATED IE05: No states matching the payload ${JSON.stringify(payload)} for device ${devId}`, true);
|
|
691
957
|
}
|
|
692
958
|
}
|
|
959
|
+
|
|
693
960
|
}
|
|
694
961
|
|
|
695
962
|
module.exports = StatesController;
|