iobroker.zigbee 1.6.15 → 1.6.16
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/.eslintignore +1 -1
- package/.eslintrc.json +36 -36
- package/.github/FUNDING.yml +3 -3
- package/.github/stale.yml +13 -13
- package/.github/workflows/test-and-release.yml +151 -151
- package/.travis/wiki.sh +27 -27
- package/LICENSE +21 -21
- package/README.md +424 -444
- package/admin/adapter-settings.js +244 -244
- package/admin/admin.js +2926 -2926
- package/admin/img/philips_hue_lom001.png +0 -0
- package/admin/index.html +159 -159
- package/admin/index_m.html +1158 -1158
- package/admin/moment.min.js +1 -1
- package/admin/shuffle.min.js +2 -2
- package/admin/tab_m.html +944 -944
- package/admin/vis-network.min.js +26 -26
- package/admin/words.js +108 -108
- package/docs/de/readme.md +27 -27
- package/docs/en/readme.md +30 -30
- package/docs/flashing_via_arduino_(en).md +110 -110
- package/docs/ru/readme.md +28 -28
- 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 +355 -348
- package/lib/backup.js +132 -132
- package/lib/binding.js +325 -325
- package/lib/colors.js +460 -460
- package/lib/commands.js +501 -501
- package/lib/developer.js +148 -148
- package/lib/devices.js +3144 -3144
- package/lib/exclude.js +168 -168
- package/lib/exposes.js +795 -795
- package/lib/groups.js +342 -342
- package/lib/json.js +60 -60
- package/lib/networkmap.js +56 -56
- package/lib/ota.js +179 -179
- package/lib/rgb.js +255 -255
- package/lib/seriallist.js +37 -37
- package/lib/states.js +6407 -6407
- package/lib/statescontroller.js +627 -627
- package/lib/tools.js +54 -54
- package/lib/utils.js +151 -151
- package/lib/zbBaseExtension.js +32 -32
- package/lib/zbDelayedAction.js +152 -152
- package/lib/zbDeviceAvailability.js +318 -318
- package/lib/zbDeviceConfigure.js +152 -152
- package/lib/zbDeviceEvent.js +49 -49
- package/lib/zigbeecontroller.js +946 -946
- package/package.json +77 -77
- package/support/docgen.js +93 -93
package/lib/statescontroller.js
CHANGED
|
@@ -1,627 +1,627 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const EventEmitter = require('events').EventEmitter;
|
|
4
|
-
const statesMapping = require('./devices');
|
|
5
|
-
const getAdId = require('./utils').getAdId;
|
|
6
|
-
const getZbId = require('./utils').getZbId;
|
|
7
|
-
const fs = require('fs');
|
|
8
|
-
var savedDeviceNames = {};
|
|
9
|
-
var knownUndefinedDevices = {};
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
class StatesController extends EventEmitter {
|
|
13
|
-
constructor(adapter) {
|
|
14
|
-
super();
|
|
15
|
-
this.adapter = adapter;
|
|
16
|
-
this.adapter.on('stateChange', this.onStateChange.bind(this));
|
|
17
|
-
this.query_device_block = [];
|
|
18
|
-
this.debugDevices = undefined;
|
|
19
|
-
let fn = adapter.expandFileName('dev_names.json');
|
|
20
|
-
this.dev_names_fn = fn.replace('.', '_');
|
|
21
|
-
this.retTimeoutHandle = null;
|
|
22
|
-
fs.readFile(this.dev_names_fn, (err, data) => {
|
|
23
|
-
if (!err) {
|
|
24
|
-
try
|
|
25
|
-
{
|
|
26
|
-
savedDeviceNames = JSON.parse(data);
|
|
27
|
-
}
|
|
28
|
-
catch
|
|
29
|
-
{
|
|
30
|
-
savedDeviceNames = {};
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
});
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
info(message, data) {
|
|
37
|
-
this.emit('log', 'info', message, data);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
error(message, data) {
|
|
41
|
-
this.emit('log', 'error', message, data);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
debug(message, data) {
|
|
45
|
-
this.emit('log', 'debug', message, data);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
warn(message, data) {
|
|
49
|
-
this.emit('log', 'warn', message, data);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
sendError(error, message) {
|
|
53
|
-
this.adapter.sendError(error, message);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
retainDeviceNames()
|
|
57
|
-
{
|
|
58
|
-
clearTimeout(this.retTimeoutHandle);
|
|
59
|
-
this.retTimeoutHanlde = setTimeout(()=> {
|
|
60
|
-
fs.writeFile(this.dev_names_fn, JSON.stringify(savedDeviceNames, null, 2), (err) => {
|
|
61
|
-
if (err)
|
|
62
|
-
this.error('error saving device names: ' + JSON.Stringify(err));
|
|
63
|
-
else
|
|
64
|
-
this.debug('saved device names');
|
|
65
|
-
});},5000);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
getDebugDevices() {
|
|
69
|
-
this.debugDevices = [];
|
|
70
|
-
this.adapter.getState(this.adapter.namespace + '.info.debugmessages', (err, state) => {
|
|
71
|
-
if (state) {
|
|
72
|
-
if (typeof(state.val) == 'string' && state.val.length > 2) this.debugDevices = state.val.split(';');
|
|
73
|
-
this.info('debug devices set to ' + JSON.stringify(this.debugDevices));
|
|
74
|
-
} else {
|
|
75
|
-
this.adapter.setObject('info.debugmessages', {
|
|
76
|
-
'type': 'state',
|
|
77
|
-
'common': {
|
|
78
|
-
'name': 'Log changes as warnings for',
|
|
79
|
-
'role': '',
|
|
80
|
-
'type': 'string',
|
|
81
|
-
'read': true,
|
|
82
|
-
'write': true,
|
|
83
|
-
},
|
|
84
|
-
'native': {},
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
});
|
|
88
|
-
this.adapter.setObject('info.undefinedDevices', {
|
|
89
|
-
'type': 'state',
|
|
90
|
-
'common': {
|
|
91
|
-
'name': 'Recorded undefined devices',
|
|
92
|
-
'role': '',
|
|
93
|
-
'type': 'string',
|
|
94
|
-
'read': true,
|
|
95
|
-
'write': false,
|
|
96
|
-
},
|
|
97
|
-
'native': {},
|
|
98
|
-
});
|
|
99
|
-
this.adapter.setStateAsync(`info.undefinedDevices`, JSON.stringify(knownUndefinedDevices), true);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
onStateChange(id, state){
|
|
103
|
-
if (!this.adapter.zbController || !this.adapter.zbController.connected()) return;
|
|
104
|
-
if (this.debugDevices === undefined) this.getDebugDevices();
|
|
105
|
-
if (state && !state.ack) {
|
|
106
|
-
if (id.endsWith('pairingCountdown') || id.endsWith('pairingMessage') || id.endsWith('connection')) return;
|
|
107
|
-
if (id.endsWith('debugmessages')) {
|
|
108
|
-
if (typeof(state.val) == 'string' && state.val.length > 2)
|
|
109
|
-
this.debugDevices = state.val.split(';');
|
|
110
|
-
else {
|
|
111
|
-
this.debugDevices = [];
|
|
112
|
-
}
|
|
113
|
-
return;
|
|
114
|
-
}
|
|
115
|
-
for (const addressPart of this.debugDevices) {
|
|
116
|
-
if (typeof(id) == 'string' && id.indexOf(addressPart) > -1)
|
|
117
|
-
{
|
|
118
|
-
this.warn(`ELEVATED: User stateChange ${id} ${JSON.stringify(state)}`);
|
|
119
|
-
break;
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
this.debug(`User stateChange ${id} ${JSON.stringify(state)}`);
|
|
123
|
-
const devId = getAdId(this.adapter, id); // iobroker device id
|
|
124
|
-
let deviceId = getZbId(id); // zigbee device id
|
|
125
|
-
// const stateKey = id.split('.')[3];
|
|
126
|
-
const arr = /zigbee.[0-9].[^.]+.(\S+)/gm.exec(id);
|
|
127
|
-
if (arr[1] === undefined) {
|
|
128
|
-
this.warn(`unable to extract id from state ${id}`);
|
|
129
|
-
return;
|
|
130
|
-
}
|
|
131
|
-
const stateKey = arr[1];
|
|
132
|
-
this.adapter.getObject(devId, (err, obj) => {
|
|
133
|
-
if (obj) {
|
|
134
|
-
const model = obj.common.type;
|
|
135
|
-
if (!model) return;
|
|
136
|
-
if (obj.common.deactivated) {
|
|
137
|
-
this.debug('State Change detected on deactivated Device - ignored');
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
140
|
-
if (model === 'group') {
|
|
141
|
-
deviceId = parseInt(deviceId.replace('0xgroup_', ''));
|
|
142
|
-
}
|
|
143
|
-
this.collectOptions(id.split('.')[2], model, options => {
|
|
144
|
-
this.publishFromState(deviceId, model, stateKey, state, options);
|
|
145
|
-
});
|
|
146
|
-
}
|
|
147
|
-
});
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
async collectOptions(devId, model, callback) {
|
|
152
|
-
const result = {};
|
|
153
|
-
// find model states for options and get it values
|
|
154
|
-
const devStates = await this.getDevStates('0x'+devId, model);
|
|
155
|
-
if (!devStates) {
|
|
156
|
-
callback(result);
|
|
157
|
-
return;
|
|
158
|
-
}
|
|
159
|
-
const states = devStates.states.filter((statedesc) => statedesc.isOption || statedesc.inOptions);
|
|
160
|
-
if (!states) {
|
|
161
|
-
callback(result);
|
|
162
|
-
return;
|
|
163
|
-
}
|
|
164
|
-
let cnt = 0;
|
|
165
|
-
try {
|
|
166
|
-
const len = states.length;
|
|
167
|
-
states.forEach(statedesc => {
|
|
168
|
-
const id = this.adapter.namespace + '.' + devId + '.' + statedesc.id;
|
|
169
|
-
this.adapter.getState(id, (err, state) => {
|
|
170
|
-
cnt = cnt + 1;
|
|
171
|
-
if (!err && state) {
|
|
172
|
-
result[statedesc.id] = state.val;
|
|
173
|
-
}
|
|
174
|
-
if (cnt === len) {
|
|
175
|
-
callback(result);
|
|
176
|
-
}
|
|
177
|
-
});
|
|
178
|
-
});
|
|
179
|
-
if (!len) callback(result);
|
|
180
|
-
} catch (error) {
|
|
181
|
-
this.sendError(error);
|
|
182
|
-
this.error(`Error collectOptions for ${devId}. Error: ${error.stack}`);
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
async getDevStates(deviceId, model) {
|
|
187
|
-
try {
|
|
188
|
-
let states = [];
|
|
189
|
-
let stateModel;
|
|
190
|
-
if (model === 'group') {
|
|
191
|
-
states = statesMapping.groupStates;
|
|
192
|
-
} else {
|
|
193
|
-
stateModel = statesMapping.findModel(model);
|
|
194
|
-
if (!stateModel) {
|
|
195
|
-
if (knownUndefinedDevices[deviceId])
|
|
196
|
-
{
|
|
197
|
-
knownUndefinedDevices[deviceId]++;
|
|
198
|
-
}
|
|
199
|
-
else {
|
|
200
|
-
knownUndefinedDevices[deviceId] = 1;
|
|
201
|
-
this.error('Device ' + deviceId + ' "' + model + '" not described in statesMapping.');
|
|
202
|
-
}
|
|
203
|
-
this.adapter.setStateAsync(`info.undefinedDevices`, JSON.stringify(knownUndefinedDevices), true);
|
|
204
|
-
states = statesMapping.commonStates;
|
|
205
|
-
} else {
|
|
206
|
-
states = stateModel.states;
|
|
207
|
-
}
|
|
208
|
-
if (typeof states === 'function' && !states.prototype) {
|
|
209
|
-
const entity = await this.adapter.zbController.resolveEntity(deviceId);
|
|
210
|
-
if (entity)
|
|
211
|
-
states = states(entity);
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
return {states: states, stateModel: stateModel};
|
|
215
|
-
} catch (error) {
|
|
216
|
-
this.sendError(error);
|
|
217
|
-
this.error(`Error getDevStates for ${deviceId}. Error: ${error.stack}`);
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
async publishFromState(deviceId, model, stateKey, state, options) {
|
|
222
|
-
if (this.debugDevices === undefined) this.getDebugDevices();
|
|
223
|
-
this.debug(`Change state '${stateKey}' at device ${deviceId} type '${model}'`);
|
|
224
|
-
for (const addressPart of this.debugDevices) {
|
|
225
|
-
if (typeof(deviceId) == 'string' && deviceId.indexOf(addressPart) > -1)
|
|
226
|
-
{
|
|
227
|
-
this.warn(`ELEVATED Change state '${stateKey}' at device ${deviceId} type '${model}'`);
|
|
228
|
-
break;
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
const devStates = await this.getDevStates(deviceId, model);
|
|
232
|
-
if (!devStates) {
|
|
233
|
-
return;
|
|
234
|
-
}
|
|
235
|
-
const commonStates = statesMapping.commonStates.find((statedesc) => stateKey === statedesc.id);
|
|
236
|
-
const stateDesc = (commonStates === undefined ? devStates.states.find((statedesc) => stateKey === statedesc.id) : commonStates);
|
|
237
|
-
const stateModel = devStates.stateModel;
|
|
238
|
-
if (!stateDesc) {
|
|
239
|
-
this.error(`No state available for '${model}' with key '${stateKey}'`);
|
|
240
|
-
return;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
const value = state.val;
|
|
244
|
-
if (value === undefined || value === '')
|
|
245
|
-
return;
|
|
246
|
-
let stateList = [{stateDesc: stateDesc, value: value, index: 0, timeout: 0}];
|
|
247
|
-
if (stateModel && stateModel.linkedStates) {
|
|
248
|
-
stateModel.linkedStates.forEach((linkedFunct) => {
|
|
249
|
-
try {
|
|
250
|
-
if (typeof linkedFunct === 'function') {
|
|
251
|
-
const res = linkedFunct(stateDesc, value, options, this.adapter.config.disableQueue);
|
|
252
|
-
if (res) {
|
|
253
|
-
stateList = stateList.concat(res);
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
else {
|
|
257
|
-
this.warn('publish from State - LinkedState is not a function ' + JSON.stringify(linkedFunct));
|
|
258
|
-
}
|
|
259
|
-
} catch (e) {
|
|
260
|
-
this.sendError(e);
|
|
261
|
-
this.error('Exception caught in publishfromstate');
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
});
|
|
265
|
-
// sort by index
|
|
266
|
-
stateList.sort((a, b) => {
|
|
267
|
-
return a.index - b.index;
|
|
268
|
-
});
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
// holds the states for for read after write requests
|
|
272
|
-
let readAfterWriteStates = [];
|
|
273
|
-
if (stateModel && stateModel.readAfterWriteStates) {
|
|
274
|
-
stateModel.readAfterWriteStates.forEach((readAfterWriteStateDesc) => {
|
|
275
|
-
readAfterWriteStates = readAfterWriteStates.concat(readAfterWriteStateDesc.id);
|
|
276
|
-
});
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
this.emit('changed', deviceId, model, stateModel, stateList, options);
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
renameDevice(id, newName) {
|
|
283
|
-
this.storeDeviceName(id, newName);
|
|
284
|
-
this.adapter.extendObject(id, {common: {name: newName}});
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
setDeviceActivated(id, active) {
|
|
288
|
-
this.adapter.extendObject(id, {common: {deactivated: active }})
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
storeDeviceName(id, name) {
|
|
292
|
-
savedDeviceNames[id.replace(`${this.adapter.namespace}.`, '')] = name;
|
|
293
|
-
this.retainDeviceNames();
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
verifyDeviceName(id, name) {
|
|
297
|
-
const savedId = id.replace(`${this.adapter.namespace}.`, '');
|
|
298
|
-
if (!savedDeviceNames.hasOwnProperty(savedId)) {
|
|
299
|
-
savedDeviceNames[savedId] = name;
|
|
300
|
-
this.retainDeviceNames();
|
|
301
|
-
}
|
|
302
|
-
return savedDeviceNames[savedId];
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
deleteDeviceStates(devId, callback) {
|
|
306
|
-
this.adapter.getStatesOf(devId, (err, states) => {
|
|
307
|
-
if (!err && states) {
|
|
308
|
-
states.forEach((state) => {
|
|
309
|
-
this.adapter.deleteState(devId, null, state._id);
|
|
310
|
-
});
|
|
311
|
-
}
|
|
312
|
-
this.adapter.deleteDevice(devId, () => {
|
|
313
|
-
callback && callback();
|
|
314
|
-
});
|
|
315
|
-
});
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
async deleteDeviceStatesAsync(devId) {
|
|
319
|
-
const states = await this.adapter.getStatesOf(devId);
|
|
320
|
-
if (states) {
|
|
321
|
-
await this.adapter.deleteState(devId, null, state._id);
|
|
322
|
-
}
|
|
323
|
-
await this.adapter.deleteDevice(devId);
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
// eslint-disable-next-line no-unused-vars
|
|
327
|
-
async deleteOrphanedDeviceStates(ieeeAddr, model, force, callback) {
|
|
328
|
-
const devStates = await this.getDevStates(ieeeAddr, model);
|
|
329
|
-
const commonStates = statesMapping.commonStates;
|
|
330
|
-
const devId = ieeeAddr.substr(2);
|
|
331
|
-
this.adapter.getStatesOf(devId, (err, states) => {
|
|
332
|
-
if (!err && states) {
|
|
333
|
-
states.forEach((state) => {
|
|
334
|
-
let statename = state._id;
|
|
335
|
-
const arr = /zigbee.[0-9].[^.]+.(\S+)/gm.exec(statename);
|
|
336
|
-
if (arr[1] === undefined) {
|
|
337
|
-
this.warn(`unable to extract id from state ${statename}`);
|
|
338
|
-
const idx = statename.lastIndexOf('.');
|
|
339
|
-
if (idx > -1) statename = statename.slice(idx+1);
|
|
340
|
-
} else {
|
|
341
|
-
statename = arr[1];
|
|
342
|
-
}
|
|
343
|
-
if (commonStates.find((statedesc) => statename === statedesc.id) === undefined &&
|
|
344
|
-
devStates.states.find((statedesc) => statename === statedesc.id) === undefined) {
|
|
345
|
-
if (state.common.hasOwnProperty('custom') && !force) {
|
|
346
|
-
this.info(`keeping disconnected state ${JSON.stringify(statename)} of ${devId} `);
|
|
347
|
-
} else {
|
|
348
|
-
this.info(`deleting disconnected state ${JSON.stringify(statename)} of ${devId} `);
|
|
349
|
-
this.adapter.deleteState(devId, null, state._id);
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
else {
|
|
353
|
-
this.debug(`keeping connecte state ${JSON.stringify(statename)} of ${devId} `);
|
|
354
|
-
}
|
|
355
|
-
});
|
|
356
|
-
}
|
|
357
|
-
});
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
updateStateWithTimeout(dev_id, name, value, common, timeout, outValue) {
|
|
361
|
-
this.updateState(dev_id, name, value, common);
|
|
362
|
-
setTimeout(() => this.updateState(dev_id, name, outValue, common), timeout);
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
updateState(devId, name, value, common) {
|
|
366
|
-
this.adapter.getObject(devId, (err, obj) => {
|
|
367
|
-
if (obj) {
|
|
368
|
-
if (!obj.common.deactivated) {
|
|
369
|
-
const new_common = {name: name};
|
|
370
|
-
const id = devId + '.' + name;
|
|
371
|
-
const new_name = obj.common.name;
|
|
372
|
-
if (common) {
|
|
373
|
-
if (common.name !== undefined) {
|
|
374
|
-
new_common.name = common.name;
|
|
375
|
-
}
|
|
376
|
-
if (common.type !== undefined) {
|
|
377
|
-
new_common.type = common.type;
|
|
378
|
-
}
|
|
379
|
-
if (common.unit !== undefined) {
|
|
380
|
-
new_common.unit = common.unit;
|
|
381
|
-
}
|
|
382
|
-
if (common.states !== undefined) {
|
|
383
|
-
new_common.states = common.states;
|
|
384
|
-
}
|
|
385
|
-
if (common.read !== undefined) {
|
|
386
|
-
new_common.read = common.read;
|
|
387
|
-
}
|
|
388
|
-
if (common.write !== undefined) {
|
|
389
|
-
new_common.write = common.write;
|
|
390
|
-
}
|
|
391
|
-
if (common.role !== undefined) {
|
|
392
|
-
new_common.role = common.role;
|
|
393
|
-
}
|
|
394
|
-
if (common.min !== undefined) {
|
|
395
|
-
new_common.min = common.min;
|
|
396
|
-
}
|
|
397
|
-
if (common.max !== undefined) {
|
|
398
|
-
new_common.max = common.max;
|
|
399
|
-
}
|
|
400
|
-
if (common.icon !== undefined) {
|
|
401
|
-
new_common.icon = common.icon;
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
// check if state exist
|
|
405
|
-
this.adapter.getObject(id, (err, stobj) => {
|
|
406
|
-
let hasChanges = false;
|
|
407
|
-
if (stobj) {
|
|
408
|
-
// update state - not change name and role (user can it changed)
|
|
409
|
-
if (stobj.common.name)
|
|
410
|
-
delete new_common.name;
|
|
411
|
-
else
|
|
412
|
-
new_common.name = new_name + ' ' + new_common.name;
|
|
413
|
-
delete new_common.role;
|
|
414
|
-
|
|
415
|
-
// check whether any common property is different
|
|
416
|
-
if (stobj.common) {
|
|
417
|
-
for (const property in new_common) {
|
|
418
|
-
if (stobj.common.hasOwnProperty(property)) {
|
|
419
|
-
if (stobj.common[property] === new_common[property]) {
|
|
420
|
-
delete new_common[property];
|
|
421
|
-
} else {
|
|
422
|
-
hasChanges = true;
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
} else {
|
|
428
|
-
hasChanges = true;
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
// only change object when any common property has changed
|
|
432
|
-
if (hasChanges) {
|
|
433
|
-
this.adapter.extendObject(id, {type: 'state', common: new_common, native: {} }, () => {
|
|
434
|
-
value !== undefined && this.setState_typed(id, value, true, (stobj) ? stobj.common.type : new_common.type);
|
|
435
|
-
});
|
|
436
|
-
} else if (value !== undefined) {
|
|
437
|
-
this.setState_typed(id, value, true, stobj.common.type);
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
});
|
|
441
|
-
}
|
|
442
|
-
else this.debug(`UpdateState: Device is deactivated ${devId} ${JSON.stringify(obj)}`);
|
|
443
|
-
} else {
|
|
444
|
-
this.debug(`UpdateState: missing device ${devId} ${JSON.stringify(obj)}`);
|
|
445
|
-
}
|
|
446
|
-
});
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
setState_typed(id, value, ack, type, callback)
|
|
450
|
-
{
|
|
451
|
-
// never set a null or undefined value
|
|
452
|
-
if (value === null || value === undefined) return;
|
|
453
|
-
if (!type) {
|
|
454
|
-
this.debug("SetState_typed called without type");
|
|
455
|
-
// identify datatype, recursively call this function with set datatype
|
|
456
|
-
this.adapter.getObject(id, (err, obj) => {
|
|
457
|
-
if (obj && obj.common)
|
|
458
|
-
this.setState_typed(id, value, ack, obj.common.type, callback);
|
|
459
|
-
else {
|
|
460
|
-
this.setState_typed(id, value, ack, 'noobj', callback);
|
|
461
|
-
}
|
|
462
|
-
});
|
|
463
|
-
return;
|
|
464
|
-
}
|
|
465
|
-
if (typeof value != type) {
|
|
466
|
-
this.debug("SetState_typed : converting " + JSON.stringify(value) + " for " + id + " from " + typeof value + " to " + type);
|
|
467
|
-
switch (type) {
|
|
468
|
-
case 'number':
|
|
469
|
-
value = parseFloat(value);
|
|
470
|
-
if (isNaN (value)) value = 0;
|
|
471
|
-
break;
|
|
472
|
-
case 'string':
|
|
473
|
-
case 'text': value = JSON.stringify(value); break;
|
|
474
|
-
case 'boolean':
|
|
475
|
-
if (typeof value == 'number') {
|
|
476
|
-
value = (value != 0);
|
|
477
|
-
break;
|
|
478
|
-
}
|
|
479
|
-
const sval = JSON.stringify(value).toLowerCase().trim();
|
|
480
|
-
value = (sval == 'true' || sval == 'yes' || sval == 'on');
|
|
481
|
-
break;
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
this.adapter.setState(id, value, ack, callback);
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
updateDev(dev_id, dev_name, model, callback) {
|
|
488
|
-
const __dev_name = this.verifyDeviceName(dev_id, dev_name);
|
|
489
|
-
const id = '' + dev_id;
|
|
490
|
-
const modelDesc = statesMapping.findModel(model);
|
|
491
|
-
let icon = (modelDesc && modelDesc.icon) ? modelDesc.icon : 'img/unknown.png';
|
|
492
|
-
// clear icon if it external
|
|
493
|
-
icon = (icon.startsWith('http')) ? undefined : icon;
|
|
494
|
-
this.adapter.setObjectNotExists(id, {
|
|
495
|
-
type: 'device',
|
|
496
|
-
// actually this is an error, so device.common has no attribute type. It must be in native part
|
|
497
|
-
common: {name: __dev_name, type: model, icon: icon},
|
|
498
|
-
native: {id: dev_id}
|
|
499
|
-
}, () => {
|
|
500
|
-
// update type and icon
|
|
501
|
-
this.adapter.extendObject(id, {common: {type: model, icon: icon}}, callback);
|
|
502
|
-
});
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
async syncDevStates(dev, model) {
|
|
506
|
-
const devId = dev.ieeeAddr.substr(2);
|
|
507
|
-
// devId - iobroker device id
|
|
508
|
-
const devStates = await this.getDevStates(dev.ieeeAddr, model);
|
|
509
|
-
if (!devStates) {
|
|
510
|
-
return;
|
|
511
|
-
}
|
|
512
|
-
const states = statesMapping.commonStates.concat(devStates.states);
|
|
513
|
-
|
|
514
|
-
for (const stateInd in states) {
|
|
515
|
-
if (!states.hasOwnProperty(stateInd)) continue;
|
|
516
|
-
|
|
517
|
-
const statedesc = states[stateInd];
|
|
518
|
-
if (statedesc === undefined)
|
|
519
|
-
{
|
|
520
|
-
this.error(`syncDevStates: Illegal state in ${JSON.stringify(dev)} - ${JSON.stringify(stateInd)}`);
|
|
521
|
-
return;
|
|
522
|
-
}
|
|
523
|
-
// Filter out non routers or devices that are battery driven for the availability flag
|
|
524
|
-
if (statedesc.id === 'available')
|
|
525
|
-
if (!(dev.type === 'Router') || dev.powerSource === 'Battery')
|
|
526
|
-
continue;
|
|
527
|
-
// lazy states
|
|
528
|
-
if (statedesc.lazy) continue;
|
|
529
|
-
|
|
530
|
-
const common = {
|
|
531
|
-
name: statedesc.name,
|
|
532
|
-
type: statedesc.type,
|
|
533
|
-
unit: statedesc.unit,
|
|
534
|
-
read: statedesc.read,
|
|
535
|
-
write: statedesc.write,
|
|
536
|
-
icon: statedesc.icon,
|
|
537
|
-
role: statedesc.role,
|
|
538
|
-
min: statedesc.min,
|
|
539
|
-
max: statedesc.max,
|
|
540
|
-
states: statedesc.states,
|
|
541
|
-
};
|
|
542
|
-
this.updateState(devId, statedesc.id, undefined, common);
|
|
543
|
-
}
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
async getExcludeExposes(allExcludesObj) {
|
|
548
|
-
statesMapping.fillStatesWithExposes(allExcludesObj);
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
async publishToState(devId, model, payload) {
|
|
553
|
-
const devStates = await this.getDevStates('0x'+devId, model);
|
|
554
|
-
let has_debug=false;
|
|
555
|
-
if (this.debugDevices === undefined) this.getDebugDevices();
|
|
556
|
-
for (const addressPart of this.debugDevices) {
|
|
557
|
-
if (typeof(devId) == 'string' && devId.indexOf(addressPart) > -1)
|
|
558
|
-
{
|
|
559
|
-
if (payload.hasOwnProperty('msg_from_zigbee')) break;
|
|
560
|
-
this.warn(`ELEVATED publishToState: message received '${JSON.stringify(payload)}' from device ${devId} type '${model}'`);
|
|
561
|
-
has_debug = true;
|
|
562
|
-
break;
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
if (!devStates) {
|
|
566
|
-
return;
|
|
567
|
-
}
|
|
568
|
-
// find states for payload
|
|
569
|
-
if (devStates.states !== undefined) {
|
|
570
|
-
const states = statesMapping.commonStates.concat(
|
|
571
|
-
devStates.states.filter((statedesc) => payload.hasOwnProperty(statedesc.prop || statedesc.id))
|
|
572
|
-
);
|
|
573
|
-
for (const stateInd in states) {
|
|
574
|
-
const statedesc = states[stateInd];
|
|
575
|
-
let value;
|
|
576
|
-
if (statedesc.getter) {
|
|
577
|
-
value = statedesc.getter(payload);
|
|
578
|
-
} else {
|
|
579
|
-
value = payload[statedesc.prop || statedesc.id];
|
|
580
|
-
}
|
|
581
|
-
// checking value
|
|
582
|
-
if (value === undefined || value === null) continue;
|
|
583
|
-
let stateID = statedesc.id;
|
|
584
|
-
|
|
585
|
-
if (has_debug && statedesc.id != 'msg_from_zigbee') {
|
|
586
|
-
this.warn(`ELEVATED publishToState: value generated '${JSON.stringify(value)}' from device ${devId} for '${statedesc.name}'`);
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
const common = {
|
|
590
|
-
name: statedesc.name,
|
|
591
|
-
type: statedesc.type,
|
|
592
|
-
unit: statedesc.unit,
|
|
593
|
-
read: statedesc.read,
|
|
594
|
-
write: statedesc.write,
|
|
595
|
-
icon: statedesc.icon,
|
|
596
|
-
role: statedesc.role,
|
|
597
|
-
min: statedesc.min,
|
|
598
|
-
max: statedesc.max,
|
|
599
|
-
};
|
|
600
|
-
if ( (typeof(value)== 'object') && (value.hasOwnProperty('stateid'))) {
|
|
601
|
-
stateID = stateID + '.' + value.stateid;
|
|
602
|
-
if (value.hasOwnProperty('unit')) common.unit = value.unit;
|
|
603
|
-
common.name = (value.name? value.name:value.stateid);
|
|
604
|
-
common.role = (value.role ? 'value.'+value.role:'number');
|
|
605
|
-
value = value.value;
|
|
606
|
-
|
|
607
|
-
}
|
|
608
|
-
// if need return value to back after timeout
|
|
609
|
-
if (statedesc.isEvent) {
|
|
610
|
-
this.updateStateWithTimeout(devId, statedesc.id, value, common, 300, !value);
|
|
611
|
-
} else {
|
|
612
|
-
if (statedesc.prepublish) {
|
|
613
|
-
this.collectOptions(devId, model, (options) => {
|
|
614
|
-
statedesc.prepublish(devId, value, (newvalue) => {
|
|
615
|
-
this.updateState(devId, stateID, newvalue, common);
|
|
616
|
-
}, options);
|
|
617
|
-
});
|
|
618
|
-
} else {
|
|
619
|
-
this.updateState(devId, stateID, value, common);
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
|
-
}
|
|
624
|
-
}
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
module.exports = StatesController;
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const EventEmitter = require('events').EventEmitter;
|
|
4
|
+
const statesMapping = require('./devices');
|
|
5
|
+
const getAdId = require('./utils').getAdId;
|
|
6
|
+
const getZbId = require('./utils').getZbId;
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
var savedDeviceNames = {};
|
|
9
|
+
var knownUndefinedDevices = {};
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class StatesController extends EventEmitter {
|
|
13
|
+
constructor(adapter) {
|
|
14
|
+
super();
|
|
15
|
+
this.adapter = adapter;
|
|
16
|
+
this.adapter.on('stateChange', this.onStateChange.bind(this));
|
|
17
|
+
this.query_device_block = [];
|
|
18
|
+
this.debugDevices = undefined;
|
|
19
|
+
let fn = adapter.expandFileName('dev_names.json');
|
|
20
|
+
this.dev_names_fn = fn.replace('.', '_');
|
|
21
|
+
this.retTimeoutHandle = null;
|
|
22
|
+
fs.readFile(this.dev_names_fn, (err, data) => {
|
|
23
|
+
if (!err) {
|
|
24
|
+
try
|
|
25
|
+
{
|
|
26
|
+
savedDeviceNames = JSON.parse(data);
|
|
27
|
+
}
|
|
28
|
+
catch
|
|
29
|
+
{
|
|
30
|
+
savedDeviceNames = {};
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
info(message, data) {
|
|
37
|
+
this.emit('log', 'info', message, data);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
error(message, data) {
|
|
41
|
+
this.emit('log', 'error', message, data);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
debug(message, data) {
|
|
45
|
+
this.emit('log', 'debug', message, data);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
warn(message, data) {
|
|
49
|
+
this.emit('log', 'warn', message, data);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
sendError(error, message) {
|
|
53
|
+
this.adapter.sendError(error, message);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
retainDeviceNames()
|
|
57
|
+
{
|
|
58
|
+
clearTimeout(this.retTimeoutHandle);
|
|
59
|
+
this.retTimeoutHanlde = setTimeout(()=> {
|
|
60
|
+
fs.writeFile(this.dev_names_fn, JSON.stringify(savedDeviceNames, null, 2), (err) => {
|
|
61
|
+
if (err)
|
|
62
|
+
this.error('error saving device names: ' + JSON.Stringify(err));
|
|
63
|
+
else
|
|
64
|
+
this.debug('saved device names');
|
|
65
|
+
});},5000);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
getDebugDevices() {
|
|
69
|
+
this.debugDevices = [];
|
|
70
|
+
this.adapter.getState(this.adapter.namespace + '.info.debugmessages', (err, state) => {
|
|
71
|
+
if (state) {
|
|
72
|
+
if (typeof(state.val) == 'string' && state.val.length > 2) this.debugDevices = state.val.split(';');
|
|
73
|
+
this.info('debug devices set to ' + JSON.stringify(this.debugDevices));
|
|
74
|
+
} else {
|
|
75
|
+
this.adapter.setObject('info.debugmessages', {
|
|
76
|
+
'type': 'state',
|
|
77
|
+
'common': {
|
|
78
|
+
'name': 'Log changes as warnings for',
|
|
79
|
+
'role': '',
|
|
80
|
+
'type': 'string',
|
|
81
|
+
'read': true,
|
|
82
|
+
'write': true,
|
|
83
|
+
},
|
|
84
|
+
'native': {},
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
this.adapter.setObject('info.undefinedDevices', {
|
|
89
|
+
'type': 'state',
|
|
90
|
+
'common': {
|
|
91
|
+
'name': 'Recorded undefined devices',
|
|
92
|
+
'role': '',
|
|
93
|
+
'type': 'string',
|
|
94
|
+
'read': true,
|
|
95
|
+
'write': false,
|
|
96
|
+
},
|
|
97
|
+
'native': {},
|
|
98
|
+
});
|
|
99
|
+
this.adapter.setStateAsync(`info.undefinedDevices`, JSON.stringify(knownUndefinedDevices), true);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
onStateChange(id, state){
|
|
103
|
+
if (!this.adapter.zbController || !this.adapter.zbController.connected()) return;
|
|
104
|
+
if (this.debugDevices === undefined) this.getDebugDevices();
|
|
105
|
+
if (state && !state.ack) {
|
|
106
|
+
if (id.endsWith('pairingCountdown') || id.endsWith('pairingMessage') || id.endsWith('connection')) return;
|
|
107
|
+
if (id.endsWith('debugmessages')) {
|
|
108
|
+
if (typeof(state.val) == 'string' && state.val.length > 2)
|
|
109
|
+
this.debugDevices = state.val.split(';');
|
|
110
|
+
else {
|
|
111
|
+
this.debugDevices = [];
|
|
112
|
+
}
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
for (const addressPart of this.debugDevices) {
|
|
116
|
+
if (typeof(id) == 'string' && id.indexOf(addressPart) > -1)
|
|
117
|
+
{
|
|
118
|
+
this.warn(`ELEVATED: User stateChange ${id} ${JSON.stringify(state)}`);
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
this.debug(`User stateChange ${id} ${JSON.stringify(state)}`);
|
|
123
|
+
const devId = getAdId(this.adapter, id); // iobroker device id
|
|
124
|
+
let deviceId = getZbId(id); // zigbee device id
|
|
125
|
+
// const stateKey = id.split('.')[3];
|
|
126
|
+
const arr = /zigbee.[0-9].[^.]+.(\S+)/gm.exec(id);
|
|
127
|
+
if (arr[1] === undefined) {
|
|
128
|
+
this.warn(`unable to extract id from state ${id}`);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
const stateKey = arr[1];
|
|
132
|
+
this.adapter.getObject(devId, (err, obj) => {
|
|
133
|
+
if (obj) {
|
|
134
|
+
const model = obj.common.type;
|
|
135
|
+
if (!model) return;
|
|
136
|
+
if (obj.common.deactivated) {
|
|
137
|
+
this.debug('State Change detected on deactivated Device - ignored');
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
if (model === 'group') {
|
|
141
|
+
deviceId = parseInt(deviceId.replace('0xgroup_', ''));
|
|
142
|
+
}
|
|
143
|
+
this.collectOptions(id.split('.')[2], model, options => {
|
|
144
|
+
this.publishFromState(deviceId, model, stateKey, state, options);
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async collectOptions(devId, model, callback) {
|
|
152
|
+
const result = {};
|
|
153
|
+
// find model states for options and get it values
|
|
154
|
+
const devStates = await this.getDevStates('0x'+devId, model);
|
|
155
|
+
if (!devStates) {
|
|
156
|
+
callback(result);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
const states = devStates.states.filter((statedesc) => statedesc.isOption || statedesc.inOptions);
|
|
160
|
+
if (!states) {
|
|
161
|
+
callback(result);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
let cnt = 0;
|
|
165
|
+
try {
|
|
166
|
+
const len = states.length;
|
|
167
|
+
states.forEach(statedesc => {
|
|
168
|
+
const id = this.adapter.namespace + '.' + devId + '.' + statedesc.id;
|
|
169
|
+
this.adapter.getState(id, (err, state) => {
|
|
170
|
+
cnt = cnt + 1;
|
|
171
|
+
if (!err && state) {
|
|
172
|
+
result[statedesc.id] = state.val;
|
|
173
|
+
}
|
|
174
|
+
if (cnt === len) {
|
|
175
|
+
callback(result);
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
if (!len) callback(result);
|
|
180
|
+
} catch (error) {
|
|
181
|
+
this.sendError(error);
|
|
182
|
+
this.error(`Error collectOptions for ${devId}. Error: ${error.stack}`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async getDevStates(deviceId, model) {
|
|
187
|
+
try {
|
|
188
|
+
let states = [];
|
|
189
|
+
let stateModel;
|
|
190
|
+
if (model === 'group') {
|
|
191
|
+
states = statesMapping.groupStates;
|
|
192
|
+
} else {
|
|
193
|
+
stateModel = statesMapping.findModel(model);
|
|
194
|
+
if (!stateModel) {
|
|
195
|
+
if (knownUndefinedDevices[deviceId])
|
|
196
|
+
{
|
|
197
|
+
knownUndefinedDevices[deviceId]++;
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
knownUndefinedDevices[deviceId] = 1;
|
|
201
|
+
this.error('Device ' + deviceId + ' "' + model + '" not described in statesMapping.');
|
|
202
|
+
}
|
|
203
|
+
this.adapter.setStateAsync(`info.undefinedDevices`, JSON.stringify(knownUndefinedDevices), true);
|
|
204
|
+
states = statesMapping.commonStates;
|
|
205
|
+
} else {
|
|
206
|
+
states = stateModel.states;
|
|
207
|
+
}
|
|
208
|
+
if (typeof states === 'function' && !states.prototype) {
|
|
209
|
+
const entity = await this.adapter.zbController.resolveEntity(deviceId);
|
|
210
|
+
if (entity)
|
|
211
|
+
states = states(entity);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return {states: states, stateModel: stateModel};
|
|
215
|
+
} catch (error) {
|
|
216
|
+
this.sendError(error);
|
|
217
|
+
this.error(`Error getDevStates for ${deviceId}. Error: ${error.stack}`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async publishFromState(deviceId, model, stateKey, state, options) {
|
|
222
|
+
if (this.debugDevices === undefined) this.getDebugDevices();
|
|
223
|
+
this.debug(`Change state '${stateKey}' at device ${deviceId} type '${model}'`);
|
|
224
|
+
for (const addressPart of this.debugDevices) {
|
|
225
|
+
if (typeof(deviceId) == 'string' && deviceId.indexOf(addressPart) > -1)
|
|
226
|
+
{
|
|
227
|
+
this.warn(`ELEVATED Change state '${stateKey}' at device ${deviceId} type '${model}'`);
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
const devStates = await this.getDevStates(deviceId, model);
|
|
232
|
+
if (!devStates) {
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
const commonStates = statesMapping.commonStates.find((statedesc) => stateKey === statedesc.id);
|
|
236
|
+
const stateDesc = (commonStates === undefined ? devStates.states.find((statedesc) => stateKey === statedesc.id) : commonStates);
|
|
237
|
+
const stateModel = devStates.stateModel;
|
|
238
|
+
if (!stateDesc) {
|
|
239
|
+
this.error(`No state available for '${model}' with key '${stateKey}'`);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const value = state.val;
|
|
244
|
+
if (value === undefined || value === '')
|
|
245
|
+
return;
|
|
246
|
+
let stateList = [{stateDesc: stateDesc, value: value, index: 0, timeout: 0}];
|
|
247
|
+
if (stateModel && stateModel.linkedStates) {
|
|
248
|
+
stateModel.linkedStates.forEach((linkedFunct) => {
|
|
249
|
+
try {
|
|
250
|
+
if (typeof linkedFunct === 'function') {
|
|
251
|
+
const res = linkedFunct(stateDesc, value, options, this.adapter.config.disableQueue);
|
|
252
|
+
if (res) {
|
|
253
|
+
stateList = stateList.concat(res);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
else {
|
|
257
|
+
this.warn('publish from State - LinkedState is not a function ' + JSON.stringify(linkedFunct));
|
|
258
|
+
}
|
|
259
|
+
} catch (e) {
|
|
260
|
+
this.sendError(e);
|
|
261
|
+
this.error('Exception caught in publishfromstate');
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
});
|
|
265
|
+
// sort by index
|
|
266
|
+
stateList.sort((a, b) => {
|
|
267
|
+
return a.index - b.index;
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// holds the states for for read after write requests
|
|
272
|
+
let readAfterWriteStates = [];
|
|
273
|
+
if (stateModel && stateModel.readAfterWriteStates) {
|
|
274
|
+
stateModel.readAfterWriteStates.forEach((readAfterWriteStateDesc) => {
|
|
275
|
+
readAfterWriteStates = readAfterWriteStates.concat(readAfterWriteStateDesc.id);
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
this.emit('changed', deviceId, model, stateModel, stateList, options);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
renameDevice(id, newName) {
|
|
283
|
+
this.storeDeviceName(id, newName);
|
|
284
|
+
this.adapter.extendObject(id, {common: {name: newName}});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
setDeviceActivated(id, active) {
|
|
288
|
+
this.adapter.extendObject(id, {common: {deactivated: active }})
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
storeDeviceName(id, name) {
|
|
292
|
+
savedDeviceNames[id.replace(`${this.adapter.namespace}.`, '')] = name;
|
|
293
|
+
this.retainDeviceNames();
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
verifyDeviceName(id, name) {
|
|
297
|
+
const savedId = id.replace(`${this.adapter.namespace}.`, '');
|
|
298
|
+
if (!savedDeviceNames.hasOwnProperty(savedId)) {
|
|
299
|
+
savedDeviceNames[savedId] = name;
|
|
300
|
+
this.retainDeviceNames();
|
|
301
|
+
}
|
|
302
|
+
return savedDeviceNames[savedId];
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
deleteDeviceStates(devId, callback) {
|
|
306
|
+
this.adapter.getStatesOf(devId, (err, states) => {
|
|
307
|
+
if (!err && states) {
|
|
308
|
+
states.forEach((state) => {
|
|
309
|
+
this.adapter.deleteState(devId, null, state._id);
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
this.adapter.deleteDevice(devId, () => {
|
|
313
|
+
callback && callback();
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
async deleteDeviceStatesAsync(devId) {
|
|
319
|
+
const states = await this.adapter.getStatesOf(devId);
|
|
320
|
+
if (states) {
|
|
321
|
+
await this.adapter.deleteState(devId, null, state._id);
|
|
322
|
+
}
|
|
323
|
+
await this.adapter.deleteDevice(devId);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// eslint-disable-next-line no-unused-vars
|
|
327
|
+
async deleteOrphanedDeviceStates(ieeeAddr, model, force, callback) {
|
|
328
|
+
const devStates = await this.getDevStates(ieeeAddr, model);
|
|
329
|
+
const commonStates = statesMapping.commonStates;
|
|
330
|
+
const devId = ieeeAddr.substr(2);
|
|
331
|
+
this.adapter.getStatesOf(devId, (err, states) => {
|
|
332
|
+
if (!err && states) {
|
|
333
|
+
states.forEach((state) => {
|
|
334
|
+
let statename = state._id;
|
|
335
|
+
const arr = /zigbee.[0-9].[^.]+.(\S+)/gm.exec(statename);
|
|
336
|
+
if (arr[1] === undefined) {
|
|
337
|
+
this.warn(`unable to extract id from state ${statename}`);
|
|
338
|
+
const idx = statename.lastIndexOf('.');
|
|
339
|
+
if (idx > -1) statename = statename.slice(idx+1);
|
|
340
|
+
} else {
|
|
341
|
+
statename = arr[1];
|
|
342
|
+
}
|
|
343
|
+
if (commonStates.find((statedesc) => statename === statedesc.id) === undefined &&
|
|
344
|
+
devStates.states.find((statedesc) => statename === statedesc.id) === undefined) {
|
|
345
|
+
if (state.common.hasOwnProperty('custom') && !force) {
|
|
346
|
+
this.info(`keeping disconnected state ${JSON.stringify(statename)} of ${devId} `);
|
|
347
|
+
} else {
|
|
348
|
+
this.info(`deleting disconnected state ${JSON.stringify(statename)} of ${devId} `);
|
|
349
|
+
this.adapter.deleteState(devId, null, state._id);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
else {
|
|
353
|
+
this.debug(`keeping connecte state ${JSON.stringify(statename)} of ${devId} `);
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
updateStateWithTimeout(dev_id, name, value, common, timeout, outValue) {
|
|
361
|
+
this.updateState(dev_id, name, value, common);
|
|
362
|
+
setTimeout(() => this.updateState(dev_id, name, outValue, common), timeout);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
updateState(devId, name, value, common) {
|
|
366
|
+
this.adapter.getObject(devId, (err, obj) => {
|
|
367
|
+
if (obj) {
|
|
368
|
+
if (!obj.common.deactivated) {
|
|
369
|
+
const new_common = {name: name};
|
|
370
|
+
const id = devId + '.' + name;
|
|
371
|
+
const new_name = obj.common.name;
|
|
372
|
+
if (common) {
|
|
373
|
+
if (common.name !== undefined) {
|
|
374
|
+
new_common.name = common.name;
|
|
375
|
+
}
|
|
376
|
+
if (common.type !== undefined) {
|
|
377
|
+
new_common.type = common.type;
|
|
378
|
+
}
|
|
379
|
+
if (common.unit !== undefined) {
|
|
380
|
+
new_common.unit = common.unit;
|
|
381
|
+
}
|
|
382
|
+
if (common.states !== undefined) {
|
|
383
|
+
new_common.states = common.states;
|
|
384
|
+
}
|
|
385
|
+
if (common.read !== undefined) {
|
|
386
|
+
new_common.read = common.read;
|
|
387
|
+
}
|
|
388
|
+
if (common.write !== undefined) {
|
|
389
|
+
new_common.write = common.write;
|
|
390
|
+
}
|
|
391
|
+
if (common.role !== undefined) {
|
|
392
|
+
new_common.role = common.role;
|
|
393
|
+
}
|
|
394
|
+
if (common.min !== undefined) {
|
|
395
|
+
new_common.min = common.min;
|
|
396
|
+
}
|
|
397
|
+
if (common.max !== undefined) {
|
|
398
|
+
new_common.max = common.max;
|
|
399
|
+
}
|
|
400
|
+
if (common.icon !== undefined) {
|
|
401
|
+
new_common.icon = common.icon;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
// check if state exist
|
|
405
|
+
this.adapter.getObject(id, (err, stobj) => {
|
|
406
|
+
let hasChanges = false;
|
|
407
|
+
if (stobj) {
|
|
408
|
+
// update state - not change name and role (user can it changed)
|
|
409
|
+
if (stobj.common.name)
|
|
410
|
+
delete new_common.name;
|
|
411
|
+
else
|
|
412
|
+
new_common.name = new_name + ' ' + new_common.name;
|
|
413
|
+
delete new_common.role;
|
|
414
|
+
|
|
415
|
+
// check whether any common property is different
|
|
416
|
+
if (stobj.common) {
|
|
417
|
+
for (const property in new_common) {
|
|
418
|
+
if (stobj.common.hasOwnProperty(property)) {
|
|
419
|
+
if (stobj.common[property] === new_common[property]) {
|
|
420
|
+
delete new_common[property];
|
|
421
|
+
} else {
|
|
422
|
+
hasChanges = true;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
} else {
|
|
428
|
+
hasChanges = true;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// only change object when any common property has changed
|
|
432
|
+
if (hasChanges) {
|
|
433
|
+
this.adapter.extendObject(id, {type: 'state', common: new_common, native: {} }, () => {
|
|
434
|
+
value !== undefined && this.setState_typed(id, value, true, (stobj) ? stobj.common.type : new_common.type);
|
|
435
|
+
});
|
|
436
|
+
} else if (value !== undefined) {
|
|
437
|
+
this.setState_typed(id, value, true, stobj.common.type);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
else this.debug(`UpdateState: Device is deactivated ${devId} ${JSON.stringify(obj)}`);
|
|
443
|
+
} else {
|
|
444
|
+
this.debug(`UpdateState: missing device ${devId} ${JSON.stringify(obj)}`);
|
|
445
|
+
}
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
setState_typed(id, value, ack, type, callback)
|
|
450
|
+
{
|
|
451
|
+
// never set a null or undefined value
|
|
452
|
+
if (value === null || value === undefined) return;
|
|
453
|
+
if (!type) {
|
|
454
|
+
this.debug("SetState_typed called without type");
|
|
455
|
+
// identify datatype, recursively call this function with set datatype
|
|
456
|
+
this.adapter.getObject(id, (err, obj) => {
|
|
457
|
+
if (obj && obj.common)
|
|
458
|
+
this.setState_typed(id, value, ack, obj.common.type, callback);
|
|
459
|
+
else {
|
|
460
|
+
this.setState_typed(id, value, ack, 'noobj', callback);
|
|
461
|
+
}
|
|
462
|
+
});
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
if (typeof value != type) {
|
|
466
|
+
this.debug("SetState_typed : converting " + JSON.stringify(value) + " for " + id + " from " + typeof value + " to " + type);
|
|
467
|
+
switch (type) {
|
|
468
|
+
case 'number':
|
|
469
|
+
value = parseFloat(value);
|
|
470
|
+
if (isNaN (value)) value = 0;
|
|
471
|
+
break;
|
|
472
|
+
case 'string':
|
|
473
|
+
case 'text': value = JSON.stringify(value); break;
|
|
474
|
+
case 'boolean':
|
|
475
|
+
if (typeof value == 'number') {
|
|
476
|
+
value = (value != 0);
|
|
477
|
+
break;
|
|
478
|
+
}
|
|
479
|
+
const sval = JSON.stringify(value).toLowerCase().trim();
|
|
480
|
+
value = (sval == 'true' || sval == 'yes' || sval == 'on');
|
|
481
|
+
break;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
this.adapter.setState(id, value, ack, callback);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
updateDev(dev_id, dev_name, model, callback) {
|
|
488
|
+
const __dev_name = this.verifyDeviceName(dev_id, dev_name);
|
|
489
|
+
const id = '' + dev_id;
|
|
490
|
+
const modelDesc = statesMapping.findModel(model);
|
|
491
|
+
let icon = (modelDesc && modelDesc.icon) ? modelDesc.icon : 'img/unknown.png';
|
|
492
|
+
// clear icon if it external
|
|
493
|
+
icon = (icon.startsWith('http')) ? undefined : icon;
|
|
494
|
+
this.adapter.setObjectNotExists(id, {
|
|
495
|
+
type: 'device',
|
|
496
|
+
// actually this is an error, so device.common has no attribute type. It must be in native part
|
|
497
|
+
common: {name: __dev_name, type: model, icon: icon},
|
|
498
|
+
native: {id: dev_id}
|
|
499
|
+
}, () => {
|
|
500
|
+
// update type and icon
|
|
501
|
+
this.adapter.extendObject(id, {common: {type: model, icon: icon}}, callback);
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
async syncDevStates(dev, model) {
|
|
506
|
+
const devId = dev.ieeeAddr.substr(2);
|
|
507
|
+
// devId - iobroker device id
|
|
508
|
+
const devStates = await this.getDevStates(dev.ieeeAddr, model);
|
|
509
|
+
if (!devStates) {
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
const states = statesMapping.commonStates.concat(devStates.states);
|
|
513
|
+
|
|
514
|
+
for (const stateInd in states) {
|
|
515
|
+
if (!states.hasOwnProperty(stateInd)) continue;
|
|
516
|
+
|
|
517
|
+
const statedesc = states[stateInd];
|
|
518
|
+
if (statedesc === undefined)
|
|
519
|
+
{
|
|
520
|
+
this.error(`syncDevStates: Illegal state in ${JSON.stringify(dev)} - ${JSON.stringify(stateInd)}`);
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
// Filter out non routers or devices that are battery driven for the availability flag
|
|
524
|
+
if (statedesc.id === 'available')
|
|
525
|
+
if (!(dev.type === 'Router') || dev.powerSource === 'Battery')
|
|
526
|
+
continue;
|
|
527
|
+
// lazy states
|
|
528
|
+
if (statedesc.lazy) continue;
|
|
529
|
+
|
|
530
|
+
const common = {
|
|
531
|
+
name: statedesc.name,
|
|
532
|
+
type: statedesc.type,
|
|
533
|
+
unit: statedesc.unit,
|
|
534
|
+
read: statedesc.read,
|
|
535
|
+
write: statedesc.write,
|
|
536
|
+
icon: statedesc.icon,
|
|
537
|
+
role: statedesc.role,
|
|
538
|
+
min: statedesc.min,
|
|
539
|
+
max: statedesc.max,
|
|
540
|
+
states: statedesc.states,
|
|
541
|
+
};
|
|
542
|
+
this.updateState(devId, statedesc.id, undefined, common);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
async getExcludeExposes(allExcludesObj) {
|
|
548
|
+
statesMapping.fillStatesWithExposes(allExcludesObj);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
async publishToState(devId, model, payload) {
|
|
553
|
+
const devStates = await this.getDevStates('0x'+devId, model);
|
|
554
|
+
let has_debug=false;
|
|
555
|
+
if (this.debugDevices === undefined) this.getDebugDevices();
|
|
556
|
+
for (const addressPart of this.debugDevices) {
|
|
557
|
+
if (typeof(devId) == 'string' && devId.indexOf(addressPart) > -1)
|
|
558
|
+
{
|
|
559
|
+
if (payload.hasOwnProperty('msg_from_zigbee')) break;
|
|
560
|
+
this.warn(`ELEVATED publishToState: message received '${JSON.stringify(payload)}' from device ${devId} type '${model}'`);
|
|
561
|
+
has_debug = true;
|
|
562
|
+
break;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
if (!devStates) {
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
// find states for payload
|
|
569
|
+
if (devStates.states !== undefined) {
|
|
570
|
+
const states = statesMapping.commonStates.concat(
|
|
571
|
+
devStates.states.filter((statedesc) => payload.hasOwnProperty(statedesc.prop || statedesc.id))
|
|
572
|
+
);
|
|
573
|
+
for (const stateInd in states) {
|
|
574
|
+
const statedesc = states[stateInd];
|
|
575
|
+
let value;
|
|
576
|
+
if (statedesc.getter) {
|
|
577
|
+
value = statedesc.getter(payload);
|
|
578
|
+
} else {
|
|
579
|
+
value = payload[statedesc.prop || statedesc.id];
|
|
580
|
+
}
|
|
581
|
+
// checking value
|
|
582
|
+
if (value === undefined || value === null) continue;
|
|
583
|
+
let stateID = statedesc.id;
|
|
584
|
+
|
|
585
|
+
if (has_debug && statedesc.id != 'msg_from_zigbee') {
|
|
586
|
+
this.warn(`ELEVATED publishToState: value generated '${JSON.stringify(value)}' from device ${devId} for '${statedesc.name}'`);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
const common = {
|
|
590
|
+
name: statedesc.name,
|
|
591
|
+
type: statedesc.type,
|
|
592
|
+
unit: statedesc.unit,
|
|
593
|
+
read: statedesc.read,
|
|
594
|
+
write: statedesc.write,
|
|
595
|
+
icon: statedesc.icon,
|
|
596
|
+
role: statedesc.role,
|
|
597
|
+
min: statedesc.min,
|
|
598
|
+
max: statedesc.max,
|
|
599
|
+
};
|
|
600
|
+
if ( (typeof(value)== 'object') && (value.hasOwnProperty('stateid'))) {
|
|
601
|
+
stateID = stateID + '.' + value.stateid;
|
|
602
|
+
if (value.hasOwnProperty('unit')) common.unit = value.unit;
|
|
603
|
+
common.name = (value.name? value.name:value.stateid);
|
|
604
|
+
common.role = (value.role ? 'value.'+value.role:'number');
|
|
605
|
+
value = value.value;
|
|
606
|
+
|
|
607
|
+
}
|
|
608
|
+
// if need return value to back after timeout
|
|
609
|
+
if (statedesc.isEvent) {
|
|
610
|
+
this.updateStateWithTimeout(devId, statedesc.id, value, common, 300, !value);
|
|
611
|
+
} else {
|
|
612
|
+
if (statedesc.prepublish) {
|
|
613
|
+
this.collectOptions(devId, model, (options) => {
|
|
614
|
+
statedesc.prepublish(devId, value, (newvalue) => {
|
|
615
|
+
this.updateState(devId, stateID, newvalue, common);
|
|
616
|
+
}, options);
|
|
617
|
+
});
|
|
618
|
+
} else {
|
|
619
|
+
this.updateState(devId, stateID, value, common);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
module.exports = StatesController;
|