iobroker.zigbee 3.2.5 → 3.3.1-alpha.0
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 +16 -0
- package/admin/admin.js +376 -267
- package/admin/index_m.html +21 -32
- package/admin/tab_m.html +14 -2
- package/io-package.json +31 -31
- package/lib/commands.js +120 -76
- package/lib/exclude.js +1 -1
- package/lib/exposes.js +187 -77
- package/lib/groups.js +28 -15
- package/lib/{devices.js → legacy/devices.js} +27 -3
- package/lib/{states.js → legacy/states.js} +3 -3
- package/lib/localConfig.js +42 -0
- package/lib/models.js +615 -0
- package/lib/networkmap.js +15 -5
- package/lib/statescontroller.js +312 -297
- package/lib/utils.js +3 -4
- package/lib/zbBaseExtension.js +4 -0
- package/lib/zbDeviceAvailability.js +16 -23
- package/lib/zbDeviceConfigure.js +21 -8
- package/lib/zigbeecontroller.js +134 -88
- package/main.js +38 -42
- package/package.json +14 -15
package/lib/localConfig.js
CHANGED
|
@@ -20,6 +20,7 @@ class localConfig extends EventEmitter {
|
|
|
20
20
|
|
|
21
21
|
async onReady()
|
|
22
22
|
{
|
|
23
|
+
this.init();
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
async onUnload(callback)
|
|
@@ -299,6 +300,21 @@ class localConfig extends EventEmitter {
|
|
|
299
300
|
try {
|
|
300
301
|
const data_js = JSON.parse(content);
|
|
301
302
|
this.localData = data_js;
|
|
303
|
+
// fix the legacy change. Any 'legacy' option found on a model will be moved to 'options.legacy'
|
|
304
|
+
//
|
|
305
|
+
let changed = false;
|
|
306
|
+
for (const mopt of Object.values(this.localData.by_model)) {
|
|
307
|
+
if (mopt?.hasOwnProperty('legacy')) {
|
|
308
|
+
const opt = mopt?.options || {};
|
|
309
|
+
if (!opt.hasOwnProperty('legacy')) {
|
|
310
|
+
opt.legacy = mopt.legacy;
|
|
311
|
+
mopt.options = opt;
|
|
312
|
+
}
|
|
313
|
+
delete mopt.legacy;
|
|
314
|
+
changed = true;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
if (changed) this.retainData(false);
|
|
302
318
|
}
|
|
303
319
|
catch (error) {
|
|
304
320
|
this.error(`unable to parse data read from ${fn} : ${error.message ? error.message : 'undefined error'}`);
|
|
@@ -364,6 +380,32 @@ class localConfig extends EventEmitter {
|
|
|
364
380
|
return rv;
|
|
365
381
|
}
|
|
366
382
|
|
|
383
|
+
getOption(source, key, option, defaultvalue) {
|
|
384
|
+
const sourceEntries = source[key] || {};
|
|
385
|
+
if (sourceEntries.hasOwnProperty(option)) return sourceEntries[option];
|
|
386
|
+
if (typeof sourceEntries.options == 'object') {
|
|
387
|
+
return sourceEntries.options.hasOwnProperty(option) ? sourceEntries.options[option] : defaultvalue;
|
|
388
|
+
}
|
|
389
|
+
return defaultvalue;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
getModelOption(model_id, option, defaultvalue) {
|
|
393
|
+
return this.getOption(this.localData.by_model, model_id, option, defaultvalue);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
getDeviceOption(device_id, model_id, option, dOnly) {
|
|
397
|
+
const modelOption = dOnly ? this.getModelOption(model_id, option) : undefined;
|
|
398
|
+
const dId = device_id.replace('0x','');
|
|
399
|
+
const deviceOption = this.getOption(this.localData.by_id, dId, option);
|
|
400
|
+
if (modelOption && typeof modelOption == 'object' && deviceOption && typeof deviceOption == 'object') {
|
|
401
|
+
for (const key of Object.keys(modelOption)) {
|
|
402
|
+
if (modelOption[key] != undefined && deviceOption[key]==undefined) deviceOption[key] = modelOption[key];
|
|
403
|
+
}
|
|
404
|
+
return deviceOption
|
|
405
|
+
}
|
|
406
|
+
else return deviceOption || modelOption
|
|
407
|
+
}
|
|
408
|
+
|
|
367
409
|
getByModel(id) {
|
|
368
410
|
return this.localData.by_model[id] || {};
|
|
369
411
|
}
|
package/lib/models.js
ADDED
|
@@ -0,0 +1,615 @@
|
|
|
1
|
+
const legacyDevices = require('./legacy/devices.js');
|
|
2
|
+
const legacyStates = require('./legacy/states').states;
|
|
3
|
+
const { getModelRegEx } = require('./utils.js')
|
|
4
|
+
const { applyHerdsmanModel } = require('./exposes.js');
|
|
5
|
+
const { findByDevice } = require('zigbee-herdsman-converters');
|
|
6
|
+
|
|
7
|
+
const herdsmanModelInfo = new Map();
|
|
8
|
+
const UUIDbyDevice = new Map();
|
|
9
|
+
|
|
10
|
+
const { ParseColor } = require('./colors');
|
|
11
|
+
const { rgb_to_cie, cie_to_rgb } = require('./rgb');
|
|
12
|
+
const { decimalToHex, adapterLevelToBulbLevel, bulbLevelToAdapterLevel , toMired } = require('./utils');
|
|
13
|
+
|
|
14
|
+
const states = {
|
|
15
|
+
// common states
|
|
16
|
+
link_quality: {
|
|
17
|
+
id: 'link_quality',
|
|
18
|
+
prop: 'linkquality',
|
|
19
|
+
name: 'Link quality',
|
|
20
|
+
icon: undefined,
|
|
21
|
+
role: 'value',
|
|
22
|
+
write: false,
|
|
23
|
+
read: true,
|
|
24
|
+
type: 'number',
|
|
25
|
+
min: 0,
|
|
26
|
+
max: 255,
|
|
27
|
+
isCommonState:true,
|
|
28
|
+
},
|
|
29
|
+
available: {
|
|
30
|
+
id: 'available',
|
|
31
|
+
prop: 'available',
|
|
32
|
+
name: 'Available',
|
|
33
|
+
icon: undefined,
|
|
34
|
+
role: 'indicator.reachable',
|
|
35
|
+
write: false,
|
|
36
|
+
read: true,
|
|
37
|
+
type: 'boolean',
|
|
38
|
+
isCommonState:true,
|
|
39
|
+
isInternalState: true,
|
|
40
|
+
},
|
|
41
|
+
device_query: { // button to trigger device read
|
|
42
|
+
id: 'device_query',
|
|
43
|
+
prop: 'device_query',
|
|
44
|
+
name: 'Trigger device query',
|
|
45
|
+
icon: undefined,
|
|
46
|
+
role: 'button',
|
|
47
|
+
write: true,
|
|
48
|
+
read: false,
|
|
49
|
+
type: 'boolean',
|
|
50
|
+
isCommonState:true,
|
|
51
|
+
isInternalState: true,
|
|
52
|
+
},
|
|
53
|
+
from_zigbee: {
|
|
54
|
+
id: 'msg_from_zigbee',
|
|
55
|
+
name: 'Message from Zigbee',
|
|
56
|
+
icon: undefined,
|
|
57
|
+
role: 'state',
|
|
58
|
+
write: false,
|
|
59
|
+
read: true,
|
|
60
|
+
type: 'string',
|
|
61
|
+
isCommonState:true,
|
|
62
|
+
isInternalState: true,
|
|
63
|
+
},
|
|
64
|
+
send_payload: {
|
|
65
|
+
id: 'send_payload',
|
|
66
|
+
name: 'Send to Device',
|
|
67
|
+
icon: undefined,
|
|
68
|
+
role: 'state',
|
|
69
|
+
write: true,
|
|
70
|
+
read: true,
|
|
71
|
+
type: 'string',
|
|
72
|
+
isCommonState:true,
|
|
73
|
+
isInternalState: true,
|
|
74
|
+
},
|
|
75
|
+
state: {
|
|
76
|
+
id: 'state',
|
|
77
|
+
name: 'Switch state',
|
|
78
|
+
icon: undefined,
|
|
79
|
+
role: 'switch',
|
|
80
|
+
write: true,
|
|
81
|
+
read: true,
|
|
82
|
+
type: 'boolean',
|
|
83
|
+
getter: payload => (payload.state === 'ON'),
|
|
84
|
+
setter: (value) => (value) ? 'ON' : 'OFF',
|
|
85
|
+
setterOpt: (value, options) => {
|
|
86
|
+
const stateValue = (value ? 'ON' : 'OFF');
|
|
87
|
+
return {...options, state: stateValue};
|
|
88
|
+
},
|
|
89
|
+
inOptions: true,
|
|
90
|
+
},
|
|
91
|
+
// group states
|
|
92
|
+
groupstateupdate: {
|
|
93
|
+
id: 'stateupdate',
|
|
94
|
+
name: 'Set group by member states',
|
|
95
|
+
icon: undefined,
|
|
96
|
+
role: 'state',
|
|
97
|
+
write: true,
|
|
98
|
+
read: true,
|
|
99
|
+
type: 'string',
|
|
100
|
+
states: {off:'off',max:'max',min:'min',avg:'avg',mat:'mat'},
|
|
101
|
+
def:'off',
|
|
102
|
+
isInternalState: true,
|
|
103
|
+
},
|
|
104
|
+
groupmemberupdate: {
|
|
105
|
+
id: 'memberupdate',
|
|
106
|
+
name: 'Read member states',
|
|
107
|
+
icon: undefined,
|
|
108
|
+
role: 'switch',
|
|
109
|
+
write: true,
|
|
110
|
+
read: true,
|
|
111
|
+
type: 'boolean',
|
|
112
|
+
isInternalState: true,
|
|
113
|
+
},
|
|
114
|
+
brightness: {
|
|
115
|
+
id: 'brightness',
|
|
116
|
+
name: 'Brightness',
|
|
117
|
+
icon: undefined,
|
|
118
|
+
role: 'level.dimmer',
|
|
119
|
+
write: true,
|
|
120
|
+
read: true,
|
|
121
|
+
type: 'number',
|
|
122
|
+
unit: '',
|
|
123
|
+
min: 0,
|
|
124
|
+
max: 100,
|
|
125
|
+
getter: payload => {
|
|
126
|
+
return bulbLevelToAdapterLevel(payload.brightness);
|
|
127
|
+
},
|
|
128
|
+
setter: (value, options) => {
|
|
129
|
+
return adapterLevelToBulbLevel(value);
|
|
130
|
+
},
|
|
131
|
+
setterOpt: (value, options) => {
|
|
132
|
+
const hasTransitionTime = options && options.hasOwnProperty('transition_time');
|
|
133
|
+
const transitionTime = hasTransitionTime ? options.transition_time : 0;
|
|
134
|
+
const preparedOptions = {...options, transition: transitionTime};
|
|
135
|
+
preparedOptions.brightness = adapterLevelToBulbLevel(value);
|
|
136
|
+
return preparedOptions;
|
|
137
|
+
},
|
|
138
|
+
readResponse: (resp) => {
|
|
139
|
+
const respObj = resp[0];
|
|
140
|
+
if (respObj.status === 0 && respObj.attrData != undefined) {
|
|
141
|
+
return bulbLevelToAdapterLevel(respObj.attrData);
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
brightness_step: {
|
|
146
|
+
id: 'brightness_step',
|
|
147
|
+
prop: 'brightness_step',
|
|
148
|
+
name: 'Brightness stepping',
|
|
149
|
+
icon: undefined,
|
|
150
|
+
role: 'level',
|
|
151
|
+
write: true,
|
|
152
|
+
read: false,
|
|
153
|
+
type: 'number',
|
|
154
|
+
min: -50,
|
|
155
|
+
max: 50
|
|
156
|
+
},
|
|
157
|
+
brightness_move: {
|
|
158
|
+
id: 'brightness_move',
|
|
159
|
+
prop: 'brightness_move',
|
|
160
|
+
name: 'Dimming',
|
|
161
|
+
icon: undefined,
|
|
162
|
+
role: 'level',
|
|
163
|
+
write: true,
|
|
164
|
+
read: false,
|
|
165
|
+
type: 'number',
|
|
166
|
+
min: -50,
|
|
167
|
+
max: 50
|
|
168
|
+
},
|
|
169
|
+
colortemp_move: {
|
|
170
|
+
id: 'colortemp_move',
|
|
171
|
+
prop: 'color_temp_move',
|
|
172
|
+
name: 'Colortemp change',
|
|
173
|
+
icon: undefined,
|
|
174
|
+
role: 'level',
|
|
175
|
+
write: true,
|
|
176
|
+
read: false,
|
|
177
|
+
type: 'number',
|
|
178
|
+
min: -50,
|
|
179
|
+
max: 50
|
|
180
|
+
},
|
|
181
|
+
hue_move: {
|
|
182
|
+
id: 'hue_move',
|
|
183
|
+
prop: 'hue_move',
|
|
184
|
+
name: 'Hue change',
|
|
185
|
+
icon: undefined,
|
|
186
|
+
role: 'level',
|
|
187
|
+
write: true,
|
|
188
|
+
read: false,
|
|
189
|
+
type: 'number',
|
|
190
|
+
min: -50,
|
|
191
|
+
max: 50
|
|
192
|
+
},
|
|
193
|
+
saturation_move: {
|
|
194
|
+
id: 'saturation_move',
|
|
195
|
+
prop: 'saturation_move',
|
|
196
|
+
name: 'Saturation change',
|
|
197
|
+
icon: undefined,
|
|
198
|
+
role: 'level',
|
|
199
|
+
write: true,
|
|
200
|
+
read: false,
|
|
201
|
+
type: 'number',
|
|
202
|
+
min: -50,
|
|
203
|
+
max: 50
|
|
204
|
+
},
|
|
205
|
+
transition_time: {
|
|
206
|
+
id: 'transition_time',
|
|
207
|
+
name: 'Transition time',
|
|
208
|
+
icon: undefined,
|
|
209
|
+
role: 'state',
|
|
210
|
+
write: true,
|
|
211
|
+
read: false,
|
|
212
|
+
type: 'number',
|
|
213
|
+
unit: 'sec',
|
|
214
|
+
isOption: true,
|
|
215
|
+
},
|
|
216
|
+
colortemp: {
|
|
217
|
+
id: 'colortemp',
|
|
218
|
+
prop: 'color_temp',
|
|
219
|
+
name: 'Color temperature',
|
|
220
|
+
icon: undefined,
|
|
221
|
+
role: 'level.color.temperature',
|
|
222
|
+
write: true,
|
|
223
|
+
read: true,
|
|
224
|
+
type: 'number',
|
|
225
|
+
min: undefined,
|
|
226
|
+
max: undefined,
|
|
227
|
+
setter: (value) => {
|
|
228
|
+
return toMired(value);
|
|
229
|
+
},
|
|
230
|
+
setterOpt: (value, options) => {
|
|
231
|
+
const hasTransitionTime = options && options.hasOwnProperty('transition_time');
|
|
232
|
+
const transitionTime = hasTransitionTime ? options.transition_time : 0;
|
|
233
|
+
return {...options, transition: transitionTime};
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
color: {
|
|
237
|
+
id: 'color',
|
|
238
|
+
prop: 'color',
|
|
239
|
+
name: 'Color',
|
|
240
|
+
icon: undefined,
|
|
241
|
+
role: 'level.color',
|
|
242
|
+
write: true,
|
|
243
|
+
read: true,
|
|
244
|
+
type: 'string',
|
|
245
|
+
setter: (value) => {
|
|
246
|
+
|
|
247
|
+
// convert RGB to XY for set
|
|
248
|
+
let xy = [0, 0];
|
|
249
|
+
const rgbcolor = ParseColor(value);
|
|
250
|
+
xy = rgb_to_cie(rgbcolor.r, rgbcolor.g, rgbcolor.b);
|
|
251
|
+
return {
|
|
252
|
+
x: xy[0],
|
|
253
|
+
y: xy[1]
|
|
254
|
+
};
|
|
255
|
+
},
|
|
256
|
+
setterOpt: (value, options) => {
|
|
257
|
+
const hasTransitionTime = options && options.hasOwnProperty('transition_time');
|
|
258
|
+
const transitionTime = hasTransitionTime ? options.transition_time : 0;
|
|
259
|
+
return {...options, transition: transitionTime};
|
|
260
|
+
},
|
|
261
|
+
getter: payload => {
|
|
262
|
+
if (payload.color && payload.color.hasOwnProperty('x') && payload.color.hasOwnProperty('y')) {
|
|
263
|
+
const colorval = cie_to_rgb(payload.color.x, payload.color.y);
|
|
264
|
+
return '#' + decimalToHex(colorval[0],2) + decimalToHex(colorval[1],2) + decimalToHex(colorval[2],2);
|
|
265
|
+
} else {
|
|
266
|
+
return undefined;
|
|
267
|
+
}
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
hex_color: {
|
|
271
|
+
id:`hex_color`,
|
|
272
|
+
name: `Hex Color`,
|
|
273
|
+
icon: undefined,
|
|
274
|
+
role: 'level.color.rgb',
|
|
275
|
+
write: true,
|
|
276
|
+
read: true,
|
|
277
|
+
type: 'string',
|
|
278
|
+
setter: value => {
|
|
279
|
+
// hex color (no named allowed)
|
|
280
|
+
const rgbcolor = ParseColor(value, true);
|
|
281
|
+
return rgbcolor;
|
|
282
|
+
},
|
|
283
|
+
setterOpt: (value, options) => {
|
|
284
|
+
const hasTransitionTime = options && options.hasOwnProperty('transition_time');
|
|
285
|
+
const transitionTime = hasTransitionTime ? options.transition_time : 0;
|
|
286
|
+
return {...options, transition: transitionTime};
|
|
287
|
+
},
|
|
288
|
+
|
|
289
|
+
getter: payload => {
|
|
290
|
+
// Requires testing!
|
|
291
|
+
|
|
292
|
+
try {
|
|
293
|
+
// JSON
|
|
294
|
+
const colorJSON = JSON.parse(payload.replaceAll("'",'"'));
|
|
295
|
+
if (colorJSON.r != undefined && colorJSON.g != undefined && colorJSON.b != undefined) {
|
|
296
|
+
const hexstring = (colorJSON.r*65536 + colorJSON.g * 256 + colorJSON.b).toString(16).padStart(6);
|
|
297
|
+
return `#${hexstring.substring(2)}`;
|
|
298
|
+
}
|
|
299
|
+
return undefined;
|
|
300
|
+
}
|
|
301
|
+
catch {
|
|
302
|
+
// intentionally empty;
|
|
303
|
+
}
|
|
304
|
+
if (payload.color.startsWith('#')) return payload.color;
|
|
305
|
+
const p = payload.replace('0x', '');
|
|
306
|
+
const m = p.match(/[0123456789abcdefABCDEF]+/);
|
|
307
|
+
if (p.length < 7 && m && m[0].length == p.length) return `${'#000000'.substring(0, 7-p.length)}${p}`;
|
|
308
|
+
return undefined;
|
|
309
|
+
},
|
|
310
|
+
setattr: 'color',
|
|
311
|
+
|
|
312
|
+
},
|
|
313
|
+
battery: {
|
|
314
|
+
id: 'battery',
|
|
315
|
+
prop: 'battery',
|
|
316
|
+
name: 'Battery percent',
|
|
317
|
+
icon: 'img/battery_p.png',
|
|
318
|
+
role: 'value.battery',
|
|
319
|
+
write: false,
|
|
320
|
+
read: true,
|
|
321
|
+
type: 'number',
|
|
322
|
+
unit: '%',
|
|
323
|
+
min: 0,
|
|
324
|
+
max: 120
|
|
325
|
+
},
|
|
326
|
+
temperature: {
|
|
327
|
+
id: 'temperature',
|
|
328
|
+
name: 'Temperature',
|
|
329
|
+
icon: undefined,
|
|
330
|
+
role: 'value.temperature',
|
|
331
|
+
write: false,
|
|
332
|
+
read: true,
|
|
333
|
+
type: 'number',
|
|
334
|
+
unit: '°C'
|
|
335
|
+
},
|
|
336
|
+
humidity: {
|
|
337
|
+
id: 'humidity',
|
|
338
|
+
name: 'Humidity',
|
|
339
|
+
icon: undefined,
|
|
340
|
+
role: 'value.humidity',
|
|
341
|
+
write: false,
|
|
342
|
+
read: true,
|
|
343
|
+
type: 'number',
|
|
344
|
+
unit: '%',
|
|
345
|
+
min: 0,
|
|
346
|
+
max: 100
|
|
347
|
+
},
|
|
348
|
+
pressure: {
|
|
349
|
+
id: 'pressure',
|
|
350
|
+
name: 'Pressure',
|
|
351
|
+
icon: undefined,
|
|
352
|
+
role: 'value.pressure',
|
|
353
|
+
write: false,
|
|
354
|
+
read: true,
|
|
355
|
+
type: 'number',
|
|
356
|
+
unit: 'hPa',
|
|
357
|
+
min: 0,
|
|
358
|
+
max: 10000
|
|
359
|
+
},
|
|
360
|
+
illuminance: {
|
|
361
|
+
id: 'illuminance',
|
|
362
|
+
prop: 'illuminance_lux',
|
|
363
|
+
name: 'Illuminance',
|
|
364
|
+
icon: undefined,
|
|
365
|
+
role: 'value.brightness',
|
|
366
|
+
write: false,
|
|
367
|
+
read: true,
|
|
368
|
+
type: 'number',
|
|
369
|
+
unit: 'lux'
|
|
370
|
+
},
|
|
371
|
+
illuminance_raw: {
|
|
372
|
+
id: 'illuminance_raw',
|
|
373
|
+
prop: 'illuminance',
|
|
374
|
+
name: 'Illuminance raw',
|
|
375
|
+
icon: undefined,
|
|
376
|
+
role: 'value.brightness',
|
|
377
|
+
write: false,
|
|
378
|
+
read: true,
|
|
379
|
+
type: 'number',
|
|
380
|
+
unit: ''
|
|
381
|
+
},
|
|
382
|
+
occupancy: {
|
|
383
|
+
id: 'occupancy',
|
|
384
|
+
name: 'Occupancy',
|
|
385
|
+
icon: undefined,
|
|
386
|
+
role: 'sensor.motion',
|
|
387
|
+
write: false,
|
|
388
|
+
read: true,
|
|
389
|
+
type: 'boolean',
|
|
390
|
+
},
|
|
391
|
+
load_power: {
|
|
392
|
+
id: 'load_power',
|
|
393
|
+
prop: 'power',
|
|
394
|
+
name: 'Load power',
|
|
395
|
+
icon: undefined,
|
|
396
|
+
role: 'value.power',
|
|
397
|
+
write: false,
|
|
398
|
+
read: true,
|
|
399
|
+
type: 'number',
|
|
400
|
+
unit: 'W'
|
|
401
|
+
},
|
|
402
|
+
contact: {
|
|
403
|
+
id: 'contact',
|
|
404
|
+
name: 'Contact event',
|
|
405
|
+
icon: undefined,
|
|
406
|
+
role: 'state',
|
|
407
|
+
write: false,
|
|
408
|
+
read: true,
|
|
409
|
+
type: 'boolean'
|
|
410
|
+
},
|
|
411
|
+
opened: {
|
|
412
|
+
id: 'opened',
|
|
413
|
+
prop: 'contact',
|
|
414
|
+
name: 'Is open',
|
|
415
|
+
icon: undefined,
|
|
416
|
+
role: 'state',
|
|
417
|
+
write: false,
|
|
418
|
+
read: true,
|
|
419
|
+
type: 'boolean',
|
|
420
|
+
getter: payload => !payload.contact,
|
|
421
|
+
},
|
|
422
|
+
heiman_batt_low: {
|
|
423
|
+
id: 'battery_low',
|
|
424
|
+
prop: 'battery_low',
|
|
425
|
+
name: 'Battery Status Low',
|
|
426
|
+
icon: undefined,
|
|
427
|
+
role: 'indicator.lowbat',
|
|
428
|
+
write: false,
|
|
429
|
+
read: true,
|
|
430
|
+
type: 'boolean'
|
|
431
|
+
},
|
|
432
|
+
tamper: {
|
|
433
|
+
id: 'tampered',
|
|
434
|
+
prop: 'tamper',
|
|
435
|
+
name: 'Is tampered',
|
|
436
|
+
icon: undefined,
|
|
437
|
+
role: 'state',
|
|
438
|
+
write: false,
|
|
439
|
+
read: true,
|
|
440
|
+
type: 'boolean',
|
|
441
|
+
},
|
|
442
|
+
water_detected: {
|
|
443
|
+
id: 'detected',
|
|
444
|
+
prop: 'water_leak',
|
|
445
|
+
name: 'Water leak detected',
|
|
446
|
+
icon: undefined,
|
|
447
|
+
role: 'indicator.leakage',
|
|
448
|
+
write: false,
|
|
449
|
+
read: true,
|
|
450
|
+
type: 'boolean'
|
|
451
|
+
},
|
|
452
|
+
climate_away_mode: {
|
|
453
|
+
id: 'away_mode',
|
|
454
|
+
prop: 'away_mode',
|
|
455
|
+
name: 'Away',
|
|
456
|
+
icon: undefined,
|
|
457
|
+
role: 'state',
|
|
458
|
+
write: true,
|
|
459
|
+
read: true,
|
|
460
|
+
type: 'boolean',
|
|
461
|
+
isEvent: true,
|
|
462
|
+
getter: payload => (payload.action === 'ON') ? true : false,
|
|
463
|
+
setter: (value) => (value) ? 'ON' : 'OFF',
|
|
464
|
+
},
|
|
465
|
+
climate_system_mode: {
|
|
466
|
+
id: 'mode',
|
|
467
|
+
name: 'Mode',
|
|
468
|
+
prop: 'system_mode',
|
|
469
|
+
icon: undefined,
|
|
470
|
+
role: 'state',
|
|
471
|
+
write: true,
|
|
472
|
+
read: true,
|
|
473
|
+
type: 'string',
|
|
474
|
+
states: 'auto:auto;off:off;heat:heat',
|
|
475
|
+
},
|
|
476
|
+
climate_running_mode: {
|
|
477
|
+
id: 'running_state',
|
|
478
|
+
name: 'Running Mode',
|
|
479
|
+
prop: 'running_state',
|
|
480
|
+
icon: undefined,
|
|
481
|
+
role: 'state',
|
|
482
|
+
write: true,
|
|
483
|
+
read: true,
|
|
484
|
+
type: 'string', // valid: low, medium, high
|
|
485
|
+
states: 'idle;idle;heat:heat',
|
|
486
|
+
},
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
const lightStates = [states.state, states.brightness, states.brightness_move, states.transition_time, states.brightness_step],
|
|
492
|
+
lightStatesWithColor=[...lightStates, states.color, states.hex_color, states.colortemp, states.colortemp_move],
|
|
493
|
+
onOffStates=[states.state],
|
|
494
|
+
lightStatesWithColortemp = [...lightStates, states.colortemp, states.colortemp_move],
|
|
495
|
+
lightStatesWithColor_hue= [...lightStatesWithColor, states.hue_move, states.transition_time, states.effect_type_hue],
|
|
496
|
+
lightStatesWithColorNoTemp= [...lightStates, states.color],
|
|
497
|
+
commonStates=Object.values(states).filter((candidate) => candidate.isCommonState),
|
|
498
|
+
commonGroupStates=[states.groupstateupdate, states.groupmemberupdate],
|
|
499
|
+
groupStates=[states.groupstateupdate, states.groupmemberupdate, ...lightStatesWithColor, ...onOffStates];
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Returns a model description, if one exists
|
|
503
|
+
* @param {string} model the model name
|
|
504
|
+
* @param {string} UUID a UUID for models with device specific model definitions
|
|
505
|
+
* @param {boolean} legacy true: legacy devices only. false: exposed devices only. undefined: Exposed devices preferred, legacy devices else
|
|
506
|
+
* @returns {object: adapterModelDefinition}
|
|
507
|
+
*/
|
|
508
|
+
function findModel(model, deviceId, legacy) {
|
|
509
|
+
const UUID = deviceId ? UUIDbyDevice.get(deviceId) : undefined;
|
|
510
|
+
if (legacy) return legacyDevices.findModel(model, true);
|
|
511
|
+
const m = UUID ? herdsmanModelInfo.get(UUID) : herdsmanModelInfo.get(getModelRegEx(model));
|
|
512
|
+
if (m) return m;
|
|
513
|
+
if (legacy === undefined) return legacyDevices.findModel(model, true)
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Returns a 16 digit hex hash
|
|
519
|
+
* @param {string} hashMe the item to hash
|
|
520
|
+
* @returns {string} the hash.
|
|
521
|
+
*/
|
|
522
|
+
function toHash(hashMe) {
|
|
523
|
+
const hashStr = JSON.stringify(hashMe);
|
|
524
|
+
let hash = 0;
|
|
525
|
+
if (hashStr.length == 0) return hash;
|
|
526
|
+
for (let i = 0; i < hashStr.length; i++) {
|
|
527
|
+
const char = hashStr.charCodeAt(i);
|
|
528
|
+
hash = ((hash << 5) - hash) + char;
|
|
529
|
+
hash = hash & hash;
|
|
530
|
+
}
|
|
531
|
+
if (hash < 0) hash+= Number.MAX_SAFE_INTEGER;
|
|
532
|
+
return `00000000000000${hash.toString(16)}`.slice(-13);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* Returns a 16 digit hex hash
|
|
537
|
+
* @param {string} hashMe the item to hash
|
|
538
|
+
* @returns {string} the hash.
|
|
539
|
+
*/
|
|
540
|
+
async function addExposeToDevices(device, model, logger) {
|
|
541
|
+
|
|
542
|
+
// grab the model definition from herdsman
|
|
543
|
+
//
|
|
544
|
+
const hM = await findByDevice(device);
|
|
545
|
+
|
|
546
|
+
if (hM) {
|
|
547
|
+
const adapterModelDefinition = { model:model, device:device, key: getModelRegEx(model), icon:hM.icon }
|
|
548
|
+
|
|
549
|
+
adapterModelDefinition.herdsmanModel = hM
|
|
550
|
+
if (typeof hM.exposes == 'function') {
|
|
551
|
+
adapterModelDefinition.exposes = hM.exposes(device);
|
|
552
|
+
const endpoints = typeof hM.endpoints == 'function' ? hM.endpoint(device) || {} : {};
|
|
553
|
+
adapterModelDefinition.UUID = toHash(`${adapterModelDefinition.exposes.map((expose) => JSON.stringify(expose)).join('.')}.${Object.keys(endpoints).join('.')}`);
|
|
554
|
+
UUIDbyDevice.set(device.ieeeAddr,adapterModelDefinition.UUID);
|
|
555
|
+
}
|
|
556
|
+
else adapterModelDefinition.exposes = (typeof hM.exposes == 'object') ? hM.exposes : {};
|
|
557
|
+
adapterModelDefinition.states = commonStates;
|
|
558
|
+
const adapterModel = await findModel(model, device.ieeeAddr, false);
|
|
559
|
+
if (adapterModel) return adapterModel;
|
|
560
|
+
const { newModel, message, error } = await applyHerdsmanModel(adapterModelDefinition);
|
|
561
|
+
if (error) {
|
|
562
|
+
if (logger) logger.warn(`${message} : ${error?.message}`);
|
|
563
|
+
}
|
|
564
|
+
else {
|
|
565
|
+
herdsmanModelInfo.set(adapterModelDefinition.UUID || adapterModelDefinition.key, newModel);
|
|
566
|
+
// default to legacy device if exposes are empty ?
|
|
567
|
+
// currently: not.
|
|
568
|
+
//
|
|
569
|
+
if (adapterModelDefinition.UUID) newModel.UUID = adapterModelDefinition.UUID;
|
|
570
|
+
return newModel;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
return {};
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
async function clearModelDefinitions() {
|
|
577
|
+
UUIDbyDevice.clear();
|
|
578
|
+
herdsmanModelInfo.clear();
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function getStateDefinition(name, type, prop) {
|
|
582
|
+
if (typeof name != 'string') return getStateDefinition('ilstate', type, prop);
|
|
583
|
+
if (legacyStates.hasOwnProperty(name)) return legacyStates[name];
|
|
584
|
+
return states[name] || {
|
|
585
|
+
id: name,
|
|
586
|
+
prop: prop || name,
|
|
587
|
+
name: name.replace('_',' '),
|
|
588
|
+
icon: undefined,
|
|
589
|
+
role: 'state',
|
|
590
|
+
write: true,
|
|
591
|
+
read: true,
|
|
592
|
+
type: type || 'string',
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
module.exports = {
|
|
597
|
+
addExposeToDevices,
|
|
598
|
+
findModel,
|
|
599
|
+
getStateDefinition,
|
|
600
|
+
clearModelDefinitions,
|
|
601
|
+
getIconforLegacyModel:legacyDevices.getIconforLegacyModel,
|
|
602
|
+
hasLegacyDevice:legacyDevices.hasLegacyDevice,
|
|
603
|
+
lightStates,
|
|
604
|
+
lightStatesWithColor,
|
|
605
|
+
states,
|
|
606
|
+
commonStates,
|
|
607
|
+
commonGroupStates,
|
|
608
|
+
groupStates,
|
|
609
|
+
lightStatesWithColortemp,
|
|
610
|
+
lightStatesWithColor_hue,
|
|
611
|
+
lightStatesWithColorNoTemp,
|
|
612
|
+
onOffStates,
|
|
613
|
+
}
|
|
614
|
+
// this stores the Herdsman model info for each device. Note that each model also stores
|
|
615
|
+
// which devices use this model as reference
|