iobroker.zigbee 1.7.1 → 1.7.4
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 +379 -373
- package/admin/adapter-settings.js +244 -244
- package/admin/admin.js +2926 -2926
- package/admin/img/MLI-404011-MLI-404049.png +0 -0
- package/admin/img/philips_hue_lom001.png +0 -0
- package/admin/index.html +159 -159
- package/admin/index_m.html +1161 -1161
- 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.css +1 -1
- package/admin/vis-network.min.js +27 -27
- package/admin/words.js +112 -112
- 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 +354 -330
- package/lib/backup.js +171 -171
- 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 +3145 -3145
- package/lib/exclude.js +168 -168
- package/lib/exposes.js +804 -804
- 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 +6416 -6416
- package/lib/statescontroller.js +666 -658
- package/lib/tools.js +54 -54
- package/lib/utils.js +151 -151
- package/lib/zbBaseExtension.js +36 -36
- package/lib/zbDelayedAction.js +152 -152
- package/lib/zbDeviceAvailability.js +315 -315
- package/lib/zbDeviceConfigure.js +152 -152
- package/lib/zbDeviceEvent.js +49 -49
- package/lib/zigbeecontroller.js +946 -946
- package/package.json +2 -2
- package/support/docgen.js +93 -93
- package/admin/img/zbt_remote.png +0 -0
package/lib/exposes.js
CHANGED
|
@@ -1,805 +1,805 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const zigbeeHerdsmanConverters = require('zigbee-herdsman-converters');
|
|
4
|
-
const statesDefs = require(__dirname + '/states.js').states;
|
|
5
|
-
const rgb = require(__dirname + '/rgb.js');
|
|
6
|
-
const utils = require(__dirname + '/utils.js');
|
|
7
|
-
const colors = require(__dirname + '/colors.js');
|
|
8
|
-
const ea = zigbeeHerdsmanConverters.exposes.access;
|
|
9
|
-
|
|
10
|
-
function genState(expose, role, name, desc) {
|
|
11
|
-
let state;
|
|
12
|
-
const readable = (expose.access & ea.STATE) > 0;
|
|
13
|
-
const writable = (expose.access & ea.SET) > 0;
|
|
14
|
-
const stname = (name || expose.property);
|
|
15
|
-
if (typeof stname !== 'string') return;
|
|
16
|
-
const stateId = stname.replace(/\*/g, '');
|
|
17
|
-
const stateName = (desc || expose.description || expose.name);
|
|
18
|
-
const propName = expose.property;
|
|
19
|
-
switch (expose.type) {
|
|
20
|
-
case 'binary':
|
|
21
|
-
state = {
|
|
22
|
-
id: stateId,
|
|
23
|
-
prop: propName,
|
|
24
|
-
name: stateName,
|
|
25
|
-
icon: undefined,
|
|
26
|
-
role: role || 'state',
|
|
27
|
-
write: writable,
|
|
28
|
-
read: true,
|
|
29
|
-
type: 'boolean',
|
|
30
|
-
};
|
|
31
|
-
if (readable) {
|
|
32
|
-
state.getter = payload => (payload[propName] === (expose.value_on || 'ON'));
|
|
33
|
-
}
|
|
34
|
-
else {
|
|
35
|
-
state.getter = payload => ( undefined);
|
|
36
|
-
}
|
|
37
|
-
if (writable) {
|
|
38
|
-
state.setter = (value) => (value) ? (expose.value_on || 'ON') : ((expose.value_off != undefined) ? expose.value_off : 'OFF');
|
|
39
|
-
state.setattr = expose.name;
|
|
40
|
-
}
|
|
41
|
-
if (expose.endpoint) {
|
|
42
|
-
state.epname = expose.endpoint;
|
|
43
|
-
}
|
|
44
|
-
break;
|
|
45
|
-
|
|
46
|
-
case 'numeric':
|
|
47
|
-
state = {
|
|
48
|
-
id: stateId,
|
|
49
|
-
prop: propName,
|
|
50
|
-
name: stateName,
|
|
51
|
-
icon: undefined,
|
|
52
|
-
role: role || 'state',
|
|
53
|
-
write: writable,
|
|
54
|
-
read: true,
|
|
55
|
-
type: 'number',
|
|
56
|
-
min: expose.value_min || 0,
|
|
57
|
-
max: expose.value_max,
|
|
58
|
-
unit: expose.unit,
|
|
59
|
-
};
|
|
60
|
-
if (expose.endpoint) {
|
|
61
|
-
state.epname = expose.endpoint;
|
|
62
|
-
}
|
|
63
|
-
break;
|
|
64
|
-
|
|
65
|
-
case 'enum':
|
|
66
|
-
state = {
|
|
67
|
-
id: stateId,
|
|
68
|
-
prop: propName,
|
|
69
|
-
name: stateName,
|
|
70
|
-
icon: undefined,
|
|
71
|
-
role: role || 'state',
|
|
72
|
-
write: writable,
|
|
73
|
-
read: true,
|
|
74
|
-
type: 'string',
|
|
75
|
-
states: expose.values.map((item) => `${item}:${item}`).join(';'),
|
|
76
|
-
};
|
|
77
|
-
if (expose.endpoint) {
|
|
78
|
-
state.epname = expose.endpoint;
|
|
79
|
-
state.setattr = expose.name;
|
|
80
|
-
}
|
|
81
|
-
break;
|
|
82
|
-
|
|
83
|
-
case 'text':
|
|
84
|
-
state = {
|
|
85
|
-
id: stateId,
|
|
86
|
-
prop: propName,
|
|
87
|
-
name: stateName,
|
|
88
|
-
icon: undefined,
|
|
89
|
-
role: role || 'state',
|
|
90
|
-
write: writable,
|
|
91
|
-
read: true,
|
|
92
|
-
type: 'string',
|
|
93
|
-
};
|
|
94
|
-
if (expose.endpoint) {
|
|
95
|
-
state.epname = expose.endpoint;
|
|
96
|
-
}
|
|
97
|
-
break;
|
|
98
|
-
|
|
99
|
-
default:
|
|
100
|
-
break;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
return state;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
function createFromExposes(model, def) {
|
|
109
|
-
const states = [];
|
|
110
|
-
// make the different (set and get) part of state is updatable if different exposes is used for get and set
|
|
111
|
-
// as example:
|
|
112
|
-
// ...
|
|
113
|
-
// exposes.binary('some_option', ea.STATE, true, false).withDescription('Some Option'),
|
|
114
|
-
// exposes.composite('options', 'options')
|
|
115
|
-
// .withDescription('Some composite Options')
|
|
116
|
-
// .withFeature(exposes.binary('some_option', ea.SET, true, false).withDescription('Some Option'))
|
|
117
|
-
//in this case one state - `some_option` has two different exposes for set an get, we have to combine it ...
|
|
118
|
-
//
|
|
119
|
-
|
|
120
|
-
function pushToStates(state, access) {
|
|
121
|
-
if (state === undefined) {
|
|
122
|
-
return 0;
|
|
123
|
-
}
|
|
124
|
-
if (access === undefined) access = ea.ALL;
|
|
125
|
-
state.readable = (access & ea.STATE) > 0;
|
|
126
|
-
state.writable = (access & ea.SET) > 0;
|
|
127
|
-
const stateExists = states.findIndex( (element, index, array) => (element.id === state.id ));
|
|
128
|
-
if (stateExists < 0 ) {
|
|
129
|
-
state.write = state.writable;
|
|
130
|
-
if (! state.writable) {
|
|
131
|
-
if ( state.hasOwnProperty('setter') ) {
|
|
132
|
-
delete state.setter;
|
|
133
|
-
}
|
|
134
|
-
if ( state.hasOwnProperty('setattr') ) {
|
|
135
|
-
delete state.setattr;
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
if (! state.readable) {
|
|
139
|
-
if (state.hasOwnProperty('getter') ) {
|
|
140
|
-
//to awid some worning on unprocessed data
|
|
141
|
-
state.getter = payload => ( undefined );
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
return states.push(state);
|
|
145
|
-
}
|
|
146
|
-
else {
|
|
147
|
-
if ( (state.readable) && (! states[stateExists].readable ) ) {
|
|
148
|
-
states[stateExists].read = state.read;
|
|
149
|
-
// as state is readable, it can't be button or event
|
|
150
|
-
if (states[stateExists].role === 'button') {
|
|
151
|
-
states[stateExists].role = state.role;
|
|
152
|
-
}
|
|
153
|
-
if (states[stateExists].hasOwnProperty('isEvent')) {
|
|
154
|
-
delete states[stateExists].isEvent;
|
|
155
|
-
}
|
|
156
|
-
// we have to use the getter from "new" state
|
|
157
|
-
if ( state.hasOwnProperty('getter') ) {
|
|
158
|
-
states[stateExists].getter = state.getter;
|
|
159
|
-
}
|
|
160
|
-
// trying to remove the `prop` property, as main key for get and set,
|
|
161
|
-
// as it can be different in new and old states, and leave only:
|
|
162
|
-
// setattr for old and id for new
|
|
163
|
-
if (( state.hasOwnProperty('prop') ) && (state.prop === state.id)) {
|
|
164
|
-
if ( states[stateExists].hasOwnProperty('prop') ) {
|
|
165
|
-
if (states[stateExists].prop !== states[stateExists].id) {
|
|
166
|
-
if (! states[stateExists].hasOwnProperty('setattr')) {
|
|
167
|
-
states[stateExists].setattr = states[stateExists].prop;
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
delete states[stateExists].prop;
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
else if ( state.hasOwnProperty('prop') ) {
|
|
174
|
-
states[stateExists].prop = state.prop;
|
|
175
|
-
}
|
|
176
|
-
states[stateExists].readable = true;
|
|
177
|
-
}
|
|
178
|
-
if ( (state.writable) && (! states[stateExists].writable ) ) {
|
|
179
|
-
states[stateExists].write = state.writable;
|
|
180
|
-
// use new state `setter`
|
|
181
|
-
if ( state.hasOwnProperty('setter') ) {
|
|
182
|
-
states[stateExists].setter = state.setter;
|
|
183
|
-
}
|
|
184
|
-
// use new state `setterOpt`
|
|
185
|
-
if ( state.hasOwnProperty('setterOpt') ) {
|
|
186
|
-
states[stateExists].setterOpt = state.setterOpt;
|
|
187
|
-
}
|
|
188
|
-
// use new state `inOptions`
|
|
189
|
-
if ( state.hasOwnProperty('inOptions') ) {
|
|
190
|
-
states[stateExists].inOptions = state.inOptions;
|
|
191
|
-
}
|
|
192
|
-
// as we have new state, responsible for set, we have to use new `isOption`
|
|
193
|
-
// or remove it
|
|
194
|
-
if (((! state.hasOwnProperty('isOption') ) || (state.isOptions === false))
|
|
195
|
-
&& (states[stateExists].hasOwnProperty('isOption'))) {
|
|
196
|
-
delete states[stateExists].isOption;
|
|
197
|
-
}
|
|
198
|
-
else {
|
|
199
|
-
states[stateExists].isOption = state.isOption;
|
|
200
|
-
}
|
|
201
|
-
// use new `setattr` or `prop` as `setattr`
|
|
202
|
-
if ( state.hasOwnProperty('setattr') ) {
|
|
203
|
-
states[stateExists].setattr = state.setattr;
|
|
204
|
-
}
|
|
205
|
-
else if ( state.hasOwnProperty('prop') ) {
|
|
206
|
-
states[stateExists].setattr = state.prop;
|
|
207
|
-
}
|
|
208
|
-
// remove `prop` equal to if, due to prop is uses as key in set and get
|
|
209
|
-
if ( states[stateExists].prop === states[stateExists].id) {
|
|
210
|
-
delete states[stateExists].prop;
|
|
211
|
-
}
|
|
212
|
-
if ( state.hasOwnProperty('epname') ) {
|
|
213
|
-
states[stateExists].epname = state.epname;
|
|
214
|
-
}
|
|
215
|
-
states[stateExists].writable = true;
|
|
216
|
-
}
|
|
217
|
-
return states.length;
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
const icon = utils.getDeviceIcon(def);
|
|
221
|
-
for (const expose of def.exposes) {
|
|
222
|
-
let state;
|
|
223
|
-
|
|
224
|
-
switch (expose.type) {
|
|
225
|
-
case 'light':
|
|
226
|
-
for (const prop of expose.features) {
|
|
227
|
-
switch (prop.name) {
|
|
228
|
-
case 'state': {
|
|
229
|
-
const stateNameS = expose.endpoint ? `state_${expose.endpoint}` : 'state';
|
|
230
|
-
pushToStates({
|
|
231
|
-
id: stateNameS,
|
|
232
|
-
name: `Switch state ${expose.endpoint ? expose.endpoint : ''}`.trim(),
|
|
233
|
-
icon: undefined,
|
|
234
|
-
role: 'switch',
|
|
235
|
-
write: true,
|
|
236
|
-
read: true,
|
|
237
|
-
type: 'boolean',
|
|
238
|
-
getter: (payload) => (payload[stateNameS] === (prop.value_on || 'ON')),
|
|
239
|
-
setter: (value) => (value) ? prop.value_on || 'ON' : ((prop.value_off != undefined) ? prop.value_off : 'OFF'),
|
|
240
|
-
epname: expose.endpoint,
|
|
241
|
-
setattr: 'state',
|
|
242
|
-
}, prop.access);
|
|
243
|
-
break;
|
|
244
|
-
}
|
|
245
|
-
case 'brightness': {
|
|
246
|
-
const stateNameB = expose.endpoint ? `brightness_${expose.endpoint}` : 'brightness';
|
|
247
|
-
pushToStates({
|
|
248
|
-
id: stateNameB,
|
|
249
|
-
name: `Brightness ${expose.endpoint ? expose.endpoint : ''}`.trim(),
|
|
250
|
-
icon: undefined,
|
|
251
|
-
role: 'level.dimmer',
|
|
252
|
-
write: true,
|
|
253
|
-
read: true,
|
|
254
|
-
type: 'number',
|
|
255
|
-
min: 0, // ignore expose.value_min
|
|
256
|
-
max: 100, // ignore expose.value_max
|
|
257
|
-
inOptions: true,
|
|
258
|
-
getter: payload => {
|
|
259
|
-
return utils.bulbLevelToAdapterLevel(payload[stateNameB]);
|
|
260
|
-
},
|
|
261
|
-
setter: (value) => {
|
|
262
|
-
return utils.adapterLevelToBulbLevel(value);
|
|
263
|
-
},
|
|
264
|
-
setterOpt: (value, options) => {
|
|
265
|
-
const hasTransitionTime = options && options.hasOwnProperty('transition_time');
|
|
266
|
-
const transitionTime = hasTransitionTime ? options.transition_time : 0;
|
|
267
|
-
const preparedOptions = {...options, transition: transitionTime};
|
|
268
|
-
preparedOptions.brightness = utils.adapterLevelToBulbLevel(value);
|
|
269
|
-
return preparedOptions;
|
|
270
|
-
},
|
|
271
|
-
readResponse: (resp) => {
|
|
272
|
-
const respObj = resp[0];
|
|
273
|
-
if (respObj.status === 0 && respObj.attrData != undefined) {
|
|
274
|
-
return utils.bulbLevelToAdapterLevel(respObj.attrData);
|
|
275
|
-
}
|
|
276
|
-
},
|
|
277
|
-
epname: expose.endpoint,
|
|
278
|
-
setattr: 'brightness',
|
|
279
|
-
}, prop.access);
|
|
280
|
-
pushToStates(statesDefs.brightness_move, prop.access);
|
|
281
|
-
break;
|
|
282
|
-
}
|
|
283
|
-
case 'color_temp': {
|
|
284
|
-
const stateNameT = expose.endpoint ? `colortemp_${expose.endpoint}` : 'colortemp';
|
|
285
|
-
pushToStates({
|
|
286
|
-
id: stateNameT,
|
|
287
|
-
prop: expose.endpoint ? `color_temp_${expose.endpoint}` : 'color_temp',
|
|
288
|
-
name: `Color temperature ${expose.endpoint ? expose.endpoint : ''}`.trim(),
|
|
289
|
-
icon: undefined,
|
|
290
|
-
role: 'level.color.temperature',
|
|
291
|
-
write: true,
|
|
292
|
-
read: true,
|
|
293
|
-
type: 'number',
|
|
294
|
-
// Ignore min and max value, so setting mireds and Kelvin with conversion to mireds works.
|
|
295
|
-
// https://github.com/ioBroker/ioBroker.zigbee/pull/1433#issuecomment-1113837035
|
|
296
|
-
min: undefined,
|
|
297
|
-
max: undefined,
|
|
298
|
-
setter: (value) => {
|
|
299
|
-
return utils.toMired(value);
|
|
300
|
-
},
|
|
301
|
-
setterOpt: (value, options) => {
|
|
302
|
-
const hasTransitionTime = options && options.hasOwnProperty('transition_time');
|
|
303
|
-
const transitionTime = hasTransitionTime ? options.transition_time : 0;
|
|
304
|
-
return {...options, transition: transitionTime};
|
|
305
|
-
},
|
|
306
|
-
epname: expose.endpoint,
|
|
307
|
-
setattr: 'color_temp',
|
|
308
|
-
}, prop.access);
|
|
309
|
-
pushToStates(statesDefs.colortemp_move, prop.access);
|
|
310
|
-
break;
|
|
311
|
-
}
|
|
312
|
-
case 'color_xy': {
|
|
313
|
-
const stateNameC = expose.endpoint ? `color_${expose.endpoint}` : 'color';
|
|
314
|
-
pushToStates({
|
|
315
|
-
id: stateNameC,
|
|
316
|
-
prop: expose.endpoint ? `color_${expose.endpoint}` : 'color',
|
|
317
|
-
name: `Color ${expose.endpoint ? expose.endpoint : ''}`.trim(),
|
|
318
|
-
icon: undefined,
|
|
319
|
-
role: 'level.color.rgb',
|
|
320
|
-
write: true,
|
|
321
|
-
read: true,
|
|
322
|
-
type: 'string',
|
|
323
|
-
setter: (value) => {
|
|
324
|
-
// convert RGB to XY for set
|
|
325
|
-
/*
|
|
326
|
-
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(value);
|
|
327
|
-
let xy = [0, 0];
|
|
328
|
-
if (result) {
|
|
329
|
-
const r = parseInt(result[1], 16),
|
|
330
|
-
g = parseInt(result[2], 16),
|
|
331
|
-
b = parseInt(result[3], 16);
|
|
332
|
-
xy = rgb.rgb_to_cie(r, g, b);
|
|
333
|
-
}
|
|
334
|
-
return {
|
|
335
|
-
x: xy[0],
|
|
336
|
-
y: xy[1]
|
|
337
|
-
};
|
|
338
|
-
*/
|
|
339
|
-
let xy = [0, 0];
|
|
340
|
-
const rgbcolor = colors.ParseColor(value);
|
|
341
|
-
|
|
342
|
-
xy = rgb.rgb_to_cie(rgbcolor.r, rgbcolor.g, rgbcolor.b);
|
|
343
|
-
return {
|
|
344
|
-
x: xy[0],
|
|
345
|
-
y: xy[1]
|
|
346
|
-
};
|
|
347
|
-
|
|
348
|
-
},
|
|
349
|
-
setterOpt: (value, options) => {
|
|
350
|
-
const hasTransitionTime = options && options.hasOwnProperty('transition_time');
|
|
351
|
-
const transitionTime = hasTransitionTime ? options.transition_time : 0;
|
|
352
|
-
return {...options, transition: transitionTime};
|
|
353
|
-
},
|
|
354
|
-
getter: payload => {
|
|
355
|
-
if (payload.color && payload.color.hasOwnProperty('x') && payload.color.hasOwnProperty('y')) {
|
|
356
|
-
const colorval = rgb.cie_to_rgb(payload.color.x, payload.color.y);
|
|
357
|
-
return '#' + utils.decimalToHex(colorval[0]) + utils.decimalToHex(colorval[1]) + utils.decimalToHex(colorval[2]);
|
|
358
|
-
} else {
|
|
359
|
-
return undefined;
|
|
360
|
-
}
|
|
361
|
-
},
|
|
362
|
-
epname: expose.endpoint,
|
|
363
|
-
setattr: 'color',
|
|
364
|
-
}, prop.access);
|
|
365
|
-
break;
|
|
366
|
-
}
|
|
367
|
-
case 'color_hs': {
|
|
368
|
-
const stateNameH = expose.endpoint ? `color_${expose.endpoint}` : 'color';
|
|
369
|
-
pushToStates({
|
|
370
|
-
id: stateNameH,
|
|
371
|
-
prop: expose.endpoint ? `color_${expose.endpoint}` : 'color',
|
|
372
|
-
name: `Color ${expose.endpoint ? expose.endpoint : ''}`.trim(),
|
|
373
|
-
icon: undefined,
|
|
374
|
-
role: 'level.color.rgb',
|
|
375
|
-
write: true,
|
|
376
|
-
read: true,
|
|
377
|
-
type: 'string',
|
|
378
|
-
setter: (value) => {
|
|
379
|
-
const _rgb = colors.ParseColor(value);
|
|
380
|
-
const hsv = rgb.rgbToHSV(_rgb.r, _rgb.g, _rgb.b, true);
|
|
381
|
-
return {
|
|
382
|
-
hue: Math.min(Math.max(hsv.h,1),359),
|
|
383
|
-
saturation: hsv.s,
|
|
384
|
-
// brightness: Math.floor(hsv.v * 2.55),
|
|
385
|
-
|
|
386
|
-
};
|
|
387
|
-
},
|
|
388
|
-
setterOpt: (value, options) => {
|
|
389
|
-
const hasTransitionTime = options && options.hasOwnProperty('transition_time');
|
|
390
|
-
const transitionTime = hasTransitionTime ? options.transition_time : 0;
|
|
391
|
-
return {...options, transition: transitionTime};
|
|
392
|
-
},
|
|
393
|
-
epname: expose.endpoint,
|
|
394
|
-
setattr: 'color',
|
|
395
|
-
}, prop.access);
|
|
396
|
-
pushToStates({
|
|
397
|
-
id: expose.endpoint ? `hue_${expose.endpoint}` : 'hue',
|
|
398
|
-
prop: expose.endpoint ? `color_${expose.endpoint}` : 'color',
|
|
399
|
-
name: `Hue ${expose.endpoint ? expose.endpoint : ''}`.trim(),
|
|
400
|
-
icon: undefined,
|
|
401
|
-
role: 'level.color.hue',
|
|
402
|
-
write: true,
|
|
403
|
-
read: false,
|
|
404
|
-
type: 'number',
|
|
405
|
-
min: 0,
|
|
406
|
-
max: 360,
|
|
407
|
-
inOptions: true,
|
|
408
|
-
setter: (value, options) => {
|
|
409
|
-
return {
|
|
410
|
-
hue: value,
|
|
411
|
-
saturation: options.saturation,
|
|
412
|
-
};
|
|
413
|
-
},
|
|
414
|
-
setterOpt: (value, options) => {
|
|
415
|
-
const hasTransitionTime = options && options.hasOwnProperty('transition_time');
|
|
416
|
-
const transitionTime = hasTransitionTime ? options.transition_time : 0;
|
|
417
|
-
const hasHueCalibrationTable = options && options.hasOwnProperty('hue_calibration');
|
|
418
|
-
if (hasHueCalibrationTable)
|
|
419
|
-
try {
|
|
420
|
-
return {...options, transition: transitionTime, hue_correction: JSON.parse(options.hue_calibration)};
|
|
421
|
-
}
|
|
422
|
-
catch {
|
|
423
|
-
const hue_correction_table = [];
|
|
424
|
-
options.hue_calibration.split(',').forEach(element => {
|
|
425
|
-
const match = /([0-9]+):([0-9]+)/.exec(element);
|
|
426
|
-
if (match && match.length ==3)
|
|
427
|
-
hue_correction_table.push({ in: Number(match[1]), out: Number(match[2])});
|
|
428
|
-
});
|
|
429
|
-
if (hue_correction_table.length > 0)
|
|
430
|
-
return {...options, transition: transitionTime, hue_correction: hue_correction_table};
|
|
431
|
-
}
|
|
432
|
-
return {...options, transition: transitionTime};
|
|
433
|
-
},
|
|
434
|
-
|
|
435
|
-
}, prop.access);
|
|
436
|
-
pushToStates({
|
|
437
|
-
id: expose.endpoint ? `saturation_${expose.endpoint}` : 'saturation',
|
|
438
|
-
prop: expose.endpoint ? `color_${expose.endpoint}` : 'color',
|
|
439
|
-
name: `Saturation ${expose.endpoint ? expose.endpoint : ''}`.trim(),
|
|
440
|
-
icon: undefined,
|
|
441
|
-
role: 'level.color.saturation',
|
|
442
|
-
write: true,
|
|
443
|
-
read: false,
|
|
444
|
-
type: 'number',
|
|
445
|
-
min: 0,
|
|
446
|
-
max: 100,
|
|
447
|
-
inOptions: true,
|
|
448
|
-
setter: (value, options) => {
|
|
449
|
-
return {
|
|
450
|
-
hue: options.hue,
|
|
451
|
-
saturation: value,
|
|
452
|
-
};
|
|
453
|
-
},
|
|
454
|
-
setterOpt: (value, options) => {
|
|
455
|
-
const hasTransitionTime = options && options.hasOwnProperty('transition_time');
|
|
456
|
-
const transitionTime = hasTransitionTime ? options.transition_time : 0;
|
|
457
|
-
const hasHueCalibrationTable = options && options.hasOwnProperty('hue_calibration');
|
|
458
|
-
if (hasHueCalibrationTable)
|
|
459
|
-
try {
|
|
460
|
-
return {...options, transition: transitionTime, hue_correction: JSON.parse(options.hue_calibration)};
|
|
461
|
-
}
|
|
462
|
-
catch {
|
|
463
|
-
const hue_correction_table = [];
|
|
464
|
-
options.hue_calibration.split(',').forEach(element => {
|
|
465
|
-
const match = /([0-9]+):([0-9]+)/.exec(element);
|
|
466
|
-
if (match && match.length ==3)
|
|
467
|
-
hue_correction_table.push({ in: Number(match[1]), out: Number(match[2])});
|
|
468
|
-
});
|
|
469
|
-
if (hue_correction_table.length > 0)
|
|
470
|
-
return {...options, transition: transitionTime, hue_correction: hue_correction_table};
|
|
471
|
-
}
|
|
472
|
-
return {...options, transition: transitionTime};
|
|
473
|
-
},
|
|
474
|
-
|
|
475
|
-
}, prop.access);
|
|
476
|
-
pushToStates(statesDefs.hue_move, prop.access);
|
|
477
|
-
pushToStates(statesDefs.saturation_move, prop.access);
|
|
478
|
-
pushToStates({
|
|
479
|
-
id: 'hue_calibration',
|
|
480
|
-
prop: 'color',
|
|
481
|
-
name: 'Hue color calibration table',
|
|
482
|
-
icon: undefined,
|
|
483
|
-
role: 'table',
|
|
484
|
-
write: true,
|
|
485
|
-
read: false,
|
|
486
|
-
type: 'string',
|
|
487
|
-
inOptions: true,
|
|
488
|
-
setter: (value, options) => {
|
|
489
|
-
return {
|
|
490
|
-
hue: options.hue,
|
|
491
|
-
saturation: options.saturation,
|
|
492
|
-
};
|
|
493
|
-
},
|
|
494
|
-
setterOpt: (value, options) => {
|
|
495
|
-
const hasTransitionTime = options && options.hasOwnProperty('transition_time');
|
|
496
|
-
const transitionTime = hasTransitionTime ? options.transition_time : 0;
|
|
497
|
-
const hasHueCalibrationTable = options && options.hasOwnProperty('hue_calibration');
|
|
498
|
-
if (hasHueCalibrationTable)
|
|
499
|
-
try {
|
|
500
|
-
return {...options, transition: transitionTime, hue_correction: JSON.parse(options.hue_calibration)};
|
|
501
|
-
}
|
|
502
|
-
catch {
|
|
503
|
-
const hue_correction_table = [];
|
|
504
|
-
options.hue_calibration.split(',').forEach(element => {
|
|
505
|
-
const match = /([0-9]+):([0-9]+)/.exec(element);
|
|
506
|
-
if (match && match.length ==3)
|
|
507
|
-
hue_correction_table.push({ in: Number(match[1]), out: Number(match[2])});
|
|
508
|
-
});
|
|
509
|
-
if (hue_correction_table.length > 0)
|
|
510
|
-
return {...options, transition: transitionTime, hue_correction: hue_correction_table};
|
|
511
|
-
}
|
|
512
|
-
return {...options, transition: transitionTime};
|
|
513
|
-
},
|
|
514
|
-
|
|
515
|
-
}, prop.access);
|
|
516
|
-
break;
|
|
517
|
-
}
|
|
518
|
-
default:
|
|
519
|
-
pushToStates(genState(prop), prop.access);
|
|
520
|
-
break;
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
pushToStates(statesDefs.transition_time, ea.STATE_SET);
|
|
524
|
-
break;
|
|
525
|
-
|
|
526
|
-
case 'switch':
|
|
527
|
-
for (const prop of expose.features) {
|
|
528
|
-
switch (prop.name) {
|
|
529
|
-
case 'state':
|
|
530
|
-
pushToStates(genState(prop, 'switch'), prop.access);
|
|
531
|
-
break;
|
|
532
|
-
default:
|
|
533
|
-
pushToStates(genState(prop), prop.access);
|
|
534
|
-
break;
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
break;
|
|
538
|
-
|
|
539
|
-
case 'numeric':
|
|
540
|
-
if (expose.endpoint) {
|
|
541
|
-
state = genState(expose);
|
|
542
|
-
} else {
|
|
543
|
-
switch (expose.name) {
|
|
544
|
-
case 'linkquality':
|
|
545
|
-
state = undefined;
|
|
546
|
-
break;
|
|
547
|
-
|
|
548
|
-
case 'battery':
|
|
549
|
-
state = statesDefs.battery;
|
|
550
|
-
break;
|
|
551
|
-
|
|
552
|
-
case 'voltage':
|
|
553
|
-
state = statesDefs.plug_voltage;
|
|
554
|
-
break;
|
|
555
|
-
|
|
556
|
-
case 'temperature':
|
|
557
|
-
state = statesDefs.temperature;
|
|
558
|
-
break;
|
|
559
|
-
|
|
560
|
-
case 'humidity':
|
|
561
|
-
state = statesDefs.humidity;
|
|
562
|
-
break;
|
|
563
|
-
|
|
564
|
-
case 'pressure':
|
|
565
|
-
state = statesDefs.pressure;
|
|
566
|
-
break;
|
|
567
|
-
|
|
568
|
-
case 'illuminance':
|
|
569
|
-
state = statesDefs.illuminance_raw;
|
|
570
|
-
break;
|
|
571
|
-
|
|
572
|
-
case 'illuminance_lux':
|
|
573
|
-
state = statesDefs.illuminance;
|
|
574
|
-
break;
|
|
575
|
-
|
|
576
|
-
case 'power':
|
|
577
|
-
state = statesDefs.load_power;
|
|
578
|
-
break;
|
|
579
|
-
|
|
580
|
-
default:
|
|
581
|
-
state = genState(expose);
|
|
582
|
-
break;
|
|
583
|
-
}
|
|
584
|
-
}
|
|
585
|
-
if (state) pushToStates(state, expose.access);
|
|
586
|
-
break;
|
|
587
|
-
|
|
588
|
-
case 'enum':
|
|
589
|
-
switch (expose.name) {
|
|
590
|
-
case 'action': {
|
|
591
|
-
|
|
592
|
-
// Ansatz:
|
|
593
|
-
|
|
594
|
-
// Action aufspalten in 2 Blöcke:
|
|
595
|
-
// Action (bekommt text ausser hold und release, auto reset nach 250 ms)
|
|
596
|
-
// Hold: wird gesetzt bei hold, gelöscht bei passendem Release
|
|
597
|
-
|
|
598
|
-
if (!Array.isArray(expose.values)) break;
|
|
599
|
-
const hasHold = expose.values.find((actionName) => actionName.includes('hold'));
|
|
600
|
-
const hasRelease = expose.values.find((actionName) => actionName.includes('release'));
|
|
601
|
-
for (const actionName of expose.values) {
|
|
602
|
-
// is release state ? - skip
|
|
603
|
-
if (hasHold && hasRelease && actionName.includes('release')) continue;
|
|
604
|
-
// is hold state ?
|
|
605
|
-
if (hasHold && hasRelease && actionName.includes('hold')) {
|
|
606
|
-
const releaseActionName = actionName.replace('hold', 'release');
|
|
607
|
-
state = {
|
|
608
|
-
id: actionName.replace(/\*/g, ''),
|
|
609
|
-
prop: 'action',
|
|
610
|
-
name: actionName,
|
|
611
|
-
icon: undefined,
|
|
612
|
-
role: 'button',
|
|
613
|
-
write: false,
|
|
614
|
-
read: true,
|
|
615
|
-
type: 'boolean',
|
|
616
|
-
getter: payload => (payload.action === actionName) ? true : (payload.action === releaseActionName) ? false : undefined,
|
|
617
|
-
};
|
|
618
|
-
} else {
|
|
619
|
-
state = {
|
|
620
|
-
id: actionName.replace(/\*/g, ''),
|
|
621
|
-
prop: 'action',
|
|
622
|
-
name: actionName,
|
|
623
|
-
icon: undefined,
|
|
624
|
-
role: 'button',
|
|
625
|
-
write: false,
|
|
626
|
-
read: true,
|
|
627
|
-
type: 'boolean',
|
|
628
|
-
getter: payload => (payload.action === actionName) ? true : undefined,
|
|
629
|
-
isEvent: true,
|
|
630
|
-
};
|
|
631
|
-
}
|
|
632
|
-
pushToStates(state, expose.access);
|
|
633
|
-
}
|
|
634
|
-
state = null;
|
|
635
|
-
break;
|
|
636
|
-
}
|
|
637
|
-
default:
|
|
638
|
-
state = genState(expose);
|
|
639
|
-
break;
|
|
640
|
-
}
|
|
641
|
-
if (state) pushToStates(state, expose.access);
|
|
642
|
-
break;
|
|
643
|
-
|
|
644
|
-
case 'binary':
|
|
645
|
-
if (expose.endpoint) {
|
|
646
|
-
state = genState(expose);
|
|
647
|
-
} else {
|
|
648
|
-
switch (expose.name) {
|
|
649
|
-
case 'contact':
|
|
650
|
-
state = statesDefs.contact;
|
|
651
|
-
pushToStates(statesDefs.opened, ea.STATE);
|
|
652
|
-
break;
|
|
653
|
-
|
|
654
|
-
case 'battery_low':
|
|
655
|
-
state = statesDefs.heiman_batt_low;
|
|
656
|
-
break;
|
|
657
|
-
|
|
658
|
-
case 'tamper':
|
|
659
|
-
state = statesDefs.tamper;
|
|
660
|
-
break;
|
|
661
|
-
|
|
662
|
-
case 'water_leak':
|
|
663
|
-
state = statesDefs.water_detected;
|
|
664
|
-
break;
|
|
665
|
-
|
|
666
|
-
case 'lock':
|
|
667
|
-
state = statesDefs.child_lock;
|
|
668
|
-
break;
|
|
669
|
-
|
|
670
|
-
case 'occupancy':
|
|
671
|
-
state = statesDefs.occupancy;
|
|
672
|
-
break;
|
|
673
|
-
|
|
674
|
-
default:
|
|
675
|
-
state = genState(expose);
|
|
676
|
-
break;
|
|
677
|
-
}
|
|
678
|
-
}
|
|
679
|
-
if (state) pushToStates(state, expose.access);
|
|
680
|
-
break;
|
|
681
|
-
|
|
682
|
-
case 'text':
|
|
683
|
-
state = genState(expose);
|
|
684
|
-
pushToStates(state, expose.access);
|
|
685
|
-
break;
|
|
686
|
-
|
|
687
|
-
case 'lock':
|
|
688
|
-
case 'fan':
|
|
689
|
-
case 'cover':
|
|
690
|
-
for (const prop of expose.features) {
|
|
691
|
-
switch (prop.name) {
|
|
692
|
-
case 'state':
|
|
693
|
-
pushToStates(genState(prop, 'switch'), prop.access);
|
|
694
|
-
break;
|
|
695
|
-
default:
|
|
696
|
-
pushToStates(genState(prop), prop.access);
|
|
697
|
-
break;
|
|
698
|
-
}
|
|
699
|
-
}
|
|
700
|
-
break;
|
|
701
|
-
|
|
702
|
-
case 'climate':
|
|
703
|
-
for (const prop of expose.features) {
|
|
704
|
-
switch (prop.name) {
|
|
705
|
-
case 'away_mode':
|
|
706
|
-
pushToStates(statesDefs.climate_away_mode, prop.access);
|
|
707
|
-
break;
|
|
708
|
-
case 'system_mode':
|
|
709
|
-
pushToStates(statesDefs.climate_system_mode, prop.access);
|
|
710
|
-
break;
|
|
711
|
-
case 'running_mode':
|
|
712
|
-
pushToStates(statesDefs.climate_running_mode, prop.access);
|
|
713
|
-
break;
|
|
714
|
-
default:
|
|
715
|
-
pushToStates(genState(prop), prop.access);
|
|
716
|
-
break;
|
|
717
|
-
}
|
|
718
|
-
}
|
|
719
|
-
break;
|
|
720
|
-
|
|
721
|
-
case 'composite':
|
|
722
|
-
for (const prop of expose.features) {
|
|
723
|
-
const st = genState(prop);
|
|
724
|
-
st.prop = expose.property;
|
|
725
|
-
st.inOptions = true;
|
|
726
|
-
// I'm not fully sure, as it really needed, but
|
|
727
|
-
st.setterOpt = (value, options) => {
|
|
728
|
-
const result = {};
|
|
729
|
-
options[prop.property] = value;
|
|
730
|
-
result[expose.property] = options;
|
|
731
|
-
return result;
|
|
732
|
-
};
|
|
733
|
-
// if we have a composite expose, the value have to be an object {expose.property : {prop.property: value}}
|
|
734
|
-
if (prop.access & ea.SET) {
|
|
735
|
-
st.setter = (value, options) => {
|
|
736
|
-
const result = {};
|
|
737
|
-
options[prop.property] = value;
|
|
738
|
-
result[expose.property] = options;
|
|
739
|
-
return result;
|
|
740
|
-
};
|
|
741
|
-
st.setattr = expose.property;
|
|
742
|
-
};
|
|
743
|
-
// if we have a composite expose, the payload will be an object {expose.property : {prop.property: value}}
|
|
744
|
-
if (prop.access & ea.STATE) {
|
|
745
|
-
st.getter = payload => {
|
|
746
|
-
if ( (payload.hasOwnProperty(expose.property)) && (payload[expose.property] !== null) && payload[expose.property].hasOwnProperty(prop.property)) {
|
|
747
|
-
return !isNaN(payload[expose.property][prop.property]) ? payload[expose.property][prop.property] : undefined
|
|
748
|
-
}
|
|
749
|
-
else {
|
|
750
|
-
return undefined;
|
|
751
|
-
}
|
|
752
|
-
};
|
|
753
|
-
}
|
|
754
|
-
else {
|
|
755
|
-
st.getter = payload => { return undefined };
|
|
756
|
-
}
|
|
757
|
-
;
|
|
758
|
-
pushToStates(st, prop.access);
|
|
759
|
-
}
|
|
760
|
-
break;
|
|
761
|
-
default:
|
|
762
|
-
console.log(`Unhandled expose type ${expose.type} for device ${model}`);
|
|
763
|
-
}
|
|
764
|
-
}
|
|
765
|
-
const newDev = {
|
|
766
|
-
models: [model],
|
|
767
|
-
icon: icon,
|
|
768
|
-
states: states,
|
|
769
|
-
exposed: true,
|
|
770
|
-
};
|
|
771
|
-
// make the function code printable in log
|
|
772
|
-
//console.log(`Created mapping for device ${model}: ${JSON.stringify(newDev, function(key, value) {
|
|
773
|
-
// if (typeof value === 'function') {return value.toString() } else { return value } }, ' ')}`);
|
|
774
|
-
return newDev;
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
function applyExposes(mappedDevices, byModel, allExcludesObj) {
|
|
778
|
-
// for exlude search
|
|
779
|
-
const allExcludesStr = JSON.stringify(allExcludesObj);
|
|
780
|
-
// create or update device from exposes
|
|
781
|
-
for (const deviceDef of zigbeeHerdsmanConverters.definitions) {
|
|
782
|
-
const strippedModel = (deviceDef.model) ? deviceDef.model.replace(/\0.*$/g, '').trim() : '';
|
|
783
|
-
// check if device is mapped
|
|
784
|
-
const existsMap = byModel.get(strippedModel);
|
|
785
|
-
|
|
786
|
-
if ((deviceDef.hasOwnProperty('exposes') && (!existsMap || !existsMap.hasOwnProperty('states'))) || allExcludesStr.indexOf(strippedModel) > 0) {
|
|
787
|
-
try {
|
|
788
|
-
const newDevice = createFromExposes(strippedModel, deviceDef);
|
|
789
|
-
if (!existsMap) {
|
|
790
|
-
mappedDevices.push(newDevice);
|
|
791
|
-
byModel.set(strippedModel, newDevice);
|
|
792
|
-
} else {
|
|
793
|
-
existsMap.states = newDevice.states;
|
|
794
|
-
existsMap.exposed = true;
|
|
795
|
-
}
|
|
796
|
-
} catch (e) {
|
|
797
|
-
console.log(`Wrong expose devicedefinition ${deviceDef.vendor} ${deviceDef.model}` );
|
|
798
|
-
}
|
|
799
|
-
}
|
|
800
|
-
}
|
|
801
|
-
}
|
|
802
|
-
|
|
803
|
-
module.exports = {
|
|
804
|
-
applyExposes: applyExposes,
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const zigbeeHerdsmanConverters = require('zigbee-herdsman-converters');
|
|
4
|
+
const statesDefs = require(__dirname + '/states.js').states;
|
|
5
|
+
const rgb = require(__dirname + '/rgb.js');
|
|
6
|
+
const utils = require(__dirname + '/utils.js');
|
|
7
|
+
const colors = require(__dirname + '/colors.js');
|
|
8
|
+
const ea = zigbeeHerdsmanConverters.exposes.access;
|
|
9
|
+
|
|
10
|
+
function genState(expose, role, name, desc) {
|
|
11
|
+
let state;
|
|
12
|
+
const readable = (expose.access & ea.STATE) > 0;
|
|
13
|
+
const writable = (expose.access & ea.SET) > 0;
|
|
14
|
+
const stname = (name || expose.property);
|
|
15
|
+
if (typeof stname !== 'string') return;
|
|
16
|
+
const stateId = stname.replace(/\*/g, '');
|
|
17
|
+
const stateName = (desc || expose.description || expose.name);
|
|
18
|
+
const propName = expose.property;
|
|
19
|
+
switch (expose.type) {
|
|
20
|
+
case 'binary':
|
|
21
|
+
state = {
|
|
22
|
+
id: stateId,
|
|
23
|
+
prop: propName,
|
|
24
|
+
name: stateName,
|
|
25
|
+
icon: undefined,
|
|
26
|
+
role: role || 'state',
|
|
27
|
+
write: writable,
|
|
28
|
+
read: true,
|
|
29
|
+
type: 'boolean',
|
|
30
|
+
};
|
|
31
|
+
if (readable) {
|
|
32
|
+
state.getter = payload => (payload[propName] === (expose.value_on || 'ON'));
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
state.getter = payload => ( undefined);
|
|
36
|
+
}
|
|
37
|
+
if (writable) {
|
|
38
|
+
state.setter = (value) => (value) ? (expose.value_on || 'ON') : ((expose.value_off != undefined) ? expose.value_off : 'OFF');
|
|
39
|
+
state.setattr = expose.name;
|
|
40
|
+
}
|
|
41
|
+
if (expose.endpoint) {
|
|
42
|
+
state.epname = expose.endpoint;
|
|
43
|
+
}
|
|
44
|
+
break;
|
|
45
|
+
|
|
46
|
+
case 'numeric':
|
|
47
|
+
state = {
|
|
48
|
+
id: stateId,
|
|
49
|
+
prop: propName,
|
|
50
|
+
name: stateName,
|
|
51
|
+
icon: undefined,
|
|
52
|
+
role: role || 'state',
|
|
53
|
+
write: writable,
|
|
54
|
+
read: true,
|
|
55
|
+
type: 'number',
|
|
56
|
+
min: expose.value_min || 0,
|
|
57
|
+
max: expose.value_max,
|
|
58
|
+
unit: expose.unit,
|
|
59
|
+
};
|
|
60
|
+
if (expose.endpoint) {
|
|
61
|
+
state.epname = expose.endpoint;
|
|
62
|
+
}
|
|
63
|
+
break;
|
|
64
|
+
|
|
65
|
+
case 'enum':
|
|
66
|
+
state = {
|
|
67
|
+
id: stateId,
|
|
68
|
+
prop: propName,
|
|
69
|
+
name: stateName,
|
|
70
|
+
icon: undefined,
|
|
71
|
+
role: role || 'state',
|
|
72
|
+
write: writable,
|
|
73
|
+
read: true,
|
|
74
|
+
type: 'string',
|
|
75
|
+
states: expose.values.map((item) => `${item}:${item}`).join(';'),
|
|
76
|
+
};
|
|
77
|
+
if (expose.endpoint) {
|
|
78
|
+
state.epname = expose.endpoint;
|
|
79
|
+
state.setattr = expose.name;
|
|
80
|
+
}
|
|
81
|
+
break;
|
|
82
|
+
|
|
83
|
+
case 'text':
|
|
84
|
+
state = {
|
|
85
|
+
id: stateId,
|
|
86
|
+
prop: propName,
|
|
87
|
+
name: stateName,
|
|
88
|
+
icon: undefined,
|
|
89
|
+
role: role || 'state',
|
|
90
|
+
write: writable,
|
|
91
|
+
read: true,
|
|
92
|
+
type: 'string',
|
|
93
|
+
};
|
|
94
|
+
if (expose.endpoint) {
|
|
95
|
+
state.epname = expose.endpoint;
|
|
96
|
+
}
|
|
97
|
+
break;
|
|
98
|
+
|
|
99
|
+
default:
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return state;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
function createFromExposes(model, def) {
|
|
109
|
+
const states = [];
|
|
110
|
+
// make the different (set and get) part of state is updatable if different exposes is used for get and set
|
|
111
|
+
// as example:
|
|
112
|
+
// ...
|
|
113
|
+
// exposes.binary('some_option', ea.STATE, true, false).withDescription('Some Option'),
|
|
114
|
+
// exposes.composite('options', 'options')
|
|
115
|
+
// .withDescription('Some composite Options')
|
|
116
|
+
// .withFeature(exposes.binary('some_option', ea.SET, true, false).withDescription('Some Option'))
|
|
117
|
+
//in this case one state - `some_option` has two different exposes for set an get, we have to combine it ...
|
|
118
|
+
//
|
|
119
|
+
|
|
120
|
+
function pushToStates(state, access) {
|
|
121
|
+
if (state === undefined) {
|
|
122
|
+
return 0;
|
|
123
|
+
}
|
|
124
|
+
if (access === undefined) access = ea.ALL;
|
|
125
|
+
state.readable = (access & ea.STATE) > 0;
|
|
126
|
+
state.writable = (access & ea.SET) > 0;
|
|
127
|
+
const stateExists = states.findIndex( (element, index, array) => (element.id === state.id ));
|
|
128
|
+
if (stateExists < 0 ) {
|
|
129
|
+
state.write = state.writable;
|
|
130
|
+
if (! state.writable) {
|
|
131
|
+
if ( state.hasOwnProperty('setter') ) {
|
|
132
|
+
delete state.setter;
|
|
133
|
+
}
|
|
134
|
+
if ( state.hasOwnProperty('setattr') ) {
|
|
135
|
+
delete state.setattr;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (! state.readable) {
|
|
139
|
+
if (state.hasOwnProperty('getter') ) {
|
|
140
|
+
//to awid some worning on unprocessed data
|
|
141
|
+
state.getter = payload => ( undefined );
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return states.push(state);
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
if ( (state.readable) && (! states[stateExists].readable ) ) {
|
|
148
|
+
states[stateExists].read = state.read;
|
|
149
|
+
// as state is readable, it can't be button or event
|
|
150
|
+
if (states[stateExists].role === 'button') {
|
|
151
|
+
states[stateExists].role = state.role;
|
|
152
|
+
}
|
|
153
|
+
if (states[stateExists].hasOwnProperty('isEvent')) {
|
|
154
|
+
delete states[stateExists].isEvent;
|
|
155
|
+
}
|
|
156
|
+
// we have to use the getter from "new" state
|
|
157
|
+
if ( state.hasOwnProperty('getter') ) {
|
|
158
|
+
states[stateExists].getter = state.getter;
|
|
159
|
+
}
|
|
160
|
+
// trying to remove the `prop` property, as main key for get and set,
|
|
161
|
+
// as it can be different in new and old states, and leave only:
|
|
162
|
+
// setattr for old and id for new
|
|
163
|
+
if (( state.hasOwnProperty('prop') ) && (state.prop === state.id)) {
|
|
164
|
+
if ( states[stateExists].hasOwnProperty('prop') ) {
|
|
165
|
+
if (states[stateExists].prop !== states[stateExists].id) {
|
|
166
|
+
if (! states[stateExists].hasOwnProperty('setattr')) {
|
|
167
|
+
states[stateExists].setattr = states[stateExists].prop;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
delete states[stateExists].prop;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
else if ( state.hasOwnProperty('prop') ) {
|
|
174
|
+
states[stateExists].prop = state.prop;
|
|
175
|
+
}
|
|
176
|
+
states[stateExists].readable = true;
|
|
177
|
+
}
|
|
178
|
+
if ( (state.writable) && (! states[stateExists].writable ) ) {
|
|
179
|
+
states[stateExists].write = state.writable;
|
|
180
|
+
// use new state `setter`
|
|
181
|
+
if ( state.hasOwnProperty('setter') ) {
|
|
182
|
+
states[stateExists].setter = state.setter;
|
|
183
|
+
}
|
|
184
|
+
// use new state `setterOpt`
|
|
185
|
+
if ( state.hasOwnProperty('setterOpt') ) {
|
|
186
|
+
states[stateExists].setterOpt = state.setterOpt;
|
|
187
|
+
}
|
|
188
|
+
// use new state `inOptions`
|
|
189
|
+
if ( state.hasOwnProperty('inOptions') ) {
|
|
190
|
+
states[stateExists].inOptions = state.inOptions;
|
|
191
|
+
}
|
|
192
|
+
// as we have new state, responsible for set, we have to use new `isOption`
|
|
193
|
+
// or remove it
|
|
194
|
+
if (((! state.hasOwnProperty('isOption') ) || (state.isOptions === false))
|
|
195
|
+
&& (states[stateExists].hasOwnProperty('isOption'))) {
|
|
196
|
+
delete states[stateExists].isOption;
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
states[stateExists].isOption = state.isOption;
|
|
200
|
+
}
|
|
201
|
+
// use new `setattr` or `prop` as `setattr`
|
|
202
|
+
if ( state.hasOwnProperty('setattr') ) {
|
|
203
|
+
states[stateExists].setattr = state.setattr;
|
|
204
|
+
}
|
|
205
|
+
else if ( state.hasOwnProperty('prop') ) {
|
|
206
|
+
states[stateExists].setattr = state.prop;
|
|
207
|
+
}
|
|
208
|
+
// remove `prop` equal to if, due to prop is uses as key in set and get
|
|
209
|
+
if ( states[stateExists].prop === states[stateExists].id) {
|
|
210
|
+
delete states[stateExists].prop;
|
|
211
|
+
}
|
|
212
|
+
if ( state.hasOwnProperty('epname') ) {
|
|
213
|
+
states[stateExists].epname = state.epname;
|
|
214
|
+
}
|
|
215
|
+
states[stateExists].writable = true;
|
|
216
|
+
}
|
|
217
|
+
return states.length;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
const icon = utils.getDeviceIcon(def);
|
|
221
|
+
for (const expose of def.exposes) {
|
|
222
|
+
let state;
|
|
223
|
+
|
|
224
|
+
switch (expose.type) {
|
|
225
|
+
case 'light':
|
|
226
|
+
for (const prop of expose.features) {
|
|
227
|
+
switch (prop.name) {
|
|
228
|
+
case 'state': {
|
|
229
|
+
const stateNameS = expose.endpoint ? `state_${expose.endpoint}` : 'state';
|
|
230
|
+
pushToStates({
|
|
231
|
+
id: stateNameS,
|
|
232
|
+
name: `Switch state ${expose.endpoint ? expose.endpoint : ''}`.trim(),
|
|
233
|
+
icon: undefined,
|
|
234
|
+
role: 'switch',
|
|
235
|
+
write: true,
|
|
236
|
+
read: true,
|
|
237
|
+
type: 'boolean',
|
|
238
|
+
getter: (payload) => (payload[stateNameS] === (prop.value_on || 'ON')),
|
|
239
|
+
setter: (value) => (value) ? prop.value_on || 'ON' : ((prop.value_off != undefined) ? prop.value_off : 'OFF'),
|
|
240
|
+
epname: expose.endpoint,
|
|
241
|
+
setattr: 'state',
|
|
242
|
+
}, prop.access);
|
|
243
|
+
break;
|
|
244
|
+
}
|
|
245
|
+
case 'brightness': {
|
|
246
|
+
const stateNameB = expose.endpoint ? `brightness_${expose.endpoint}` : 'brightness';
|
|
247
|
+
pushToStates({
|
|
248
|
+
id: stateNameB,
|
|
249
|
+
name: `Brightness ${expose.endpoint ? expose.endpoint : ''}`.trim(),
|
|
250
|
+
icon: undefined,
|
|
251
|
+
role: 'level.dimmer',
|
|
252
|
+
write: true,
|
|
253
|
+
read: true,
|
|
254
|
+
type: 'number',
|
|
255
|
+
min: 0, // ignore expose.value_min
|
|
256
|
+
max: 100, // ignore expose.value_max
|
|
257
|
+
inOptions: true,
|
|
258
|
+
getter: payload => {
|
|
259
|
+
return utils.bulbLevelToAdapterLevel(payload[stateNameB]);
|
|
260
|
+
},
|
|
261
|
+
setter: (value) => {
|
|
262
|
+
return utils.adapterLevelToBulbLevel(value);
|
|
263
|
+
},
|
|
264
|
+
setterOpt: (value, options) => {
|
|
265
|
+
const hasTransitionTime = options && options.hasOwnProperty('transition_time');
|
|
266
|
+
const transitionTime = hasTransitionTime ? options.transition_time : 0;
|
|
267
|
+
const preparedOptions = {...options, transition: transitionTime};
|
|
268
|
+
preparedOptions.brightness = utils.adapterLevelToBulbLevel(value);
|
|
269
|
+
return preparedOptions;
|
|
270
|
+
},
|
|
271
|
+
readResponse: (resp) => {
|
|
272
|
+
const respObj = resp[0];
|
|
273
|
+
if (respObj.status === 0 && respObj.attrData != undefined) {
|
|
274
|
+
return utils.bulbLevelToAdapterLevel(respObj.attrData);
|
|
275
|
+
}
|
|
276
|
+
},
|
|
277
|
+
epname: expose.endpoint,
|
|
278
|
+
setattr: 'brightness',
|
|
279
|
+
}, prop.access);
|
|
280
|
+
pushToStates(statesDefs.brightness_move, prop.access);
|
|
281
|
+
break;
|
|
282
|
+
}
|
|
283
|
+
case 'color_temp': {
|
|
284
|
+
const stateNameT = expose.endpoint ? `colortemp_${expose.endpoint}` : 'colortemp';
|
|
285
|
+
pushToStates({
|
|
286
|
+
id: stateNameT,
|
|
287
|
+
prop: expose.endpoint ? `color_temp_${expose.endpoint}` : 'color_temp',
|
|
288
|
+
name: `Color temperature ${expose.endpoint ? expose.endpoint : ''}`.trim(),
|
|
289
|
+
icon: undefined,
|
|
290
|
+
role: 'level.color.temperature',
|
|
291
|
+
write: true,
|
|
292
|
+
read: true,
|
|
293
|
+
type: 'number',
|
|
294
|
+
// Ignore min and max value, so setting mireds and Kelvin with conversion to mireds works.
|
|
295
|
+
// https://github.com/ioBroker/ioBroker.zigbee/pull/1433#issuecomment-1113837035
|
|
296
|
+
min: undefined,
|
|
297
|
+
max: undefined,
|
|
298
|
+
setter: (value) => {
|
|
299
|
+
return utils.toMired(value);
|
|
300
|
+
},
|
|
301
|
+
setterOpt: (value, options) => {
|
|
302
|
+
const hasTransitionTime = options && options.hasOwnProperty('transition_time');
|
|
303
|
+
const transitionTime = hasTransitionTime ? options.transition_time : 0;
|
|
304
|
+
return {...options, transition: transitionTime};
|
|
305
|
+
},
|
|
306
|
+
epname: expose.endpoint,
|
|
307
|
+
setattr: 'color_temp',
|
|
308
|
+
}, prop.access);
|
|
309
|
+
pushToStates(statesDefs.colortemp_move, prop.access);
|
|
310
|
+
break;
|
|
311
|
+
}
|
|
312
|
+
case 'color_xy': {
|
|
313
|
+
const stateNameC = expose.endpoint ? `color_${expose.endpoint}` : 'color';
|
|
314
|
+
pushToStates({
|
|
315
|
+
id: stateNameC,
|
|
316
|
+
prop: expose.endpoint ? `color_${expose.endpoint}` : 'color',
|
|
317
|
+
name: `Color ${expose.endpoint ? expose.endpoint : ''}`.trim(),
|
|
318
|
+
icon: undefined,
|
|
319
|
+
role: 'level.color.rgb',
|
|
320
|
+
write: true,
|
|
321
|
+
read: true,
|
|
322
|
+
type: 'string',
|
|
323
|
+
setter: (value) => {
|
|
324
|
+
// convert RGB to XY for set
|
|
325
|
+
/*
|
|
326
|
+
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(value);
|
|
327
|
+
let xy = [0, 0];
|
|
328
|
+
if (result) {
|
|
329
|
+
const r = parseInt(result[1], 16),
|
|
330
|
+
g = parseInt(result[2], 16),
|
|
331
|
+
b = parseInt(result[3], 16);
|
|
332
|
+
xy = rgb.rgb_to_cie(r, g, b);
|
|
333
|
+
}
|
|
334
|
+
return {
|
|
335
|
+
x: xy[0],
|
|
336
|
+
y: xy[1]
|
|
337
|
+
};
|
|
338
|
+
*/
|
|
339
|
+
let xy = [0, 0];
|
|
340
|
+
const rgbcolor = colors.ParseColor(value);
|
|
341
|
+
|
|
342
|
+
xy = rgb.rgb_to_cie(rgbcolor.r, rgbcolor.g, rgbcolor.b);
|
|
343
|
+
return {
|
|
344
|
+
x: xy[0],
|
|
345
|
+
y: xy[1]
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
},
|
|
349
|
+
setterOpt: (value, options) => {
|
|
350
|
+
const hasTransitionTime = options && options.hasOwnProperty('transition_time');
|
|
351
|
+
const transitionTime = hasTransitionTime ? options.transition_time : 0;
|
|
352
|
+
return {...options, transition: transitionTime};
|
|
353
|
+
},
|
|
354
|
+
getter: payload => {
|
|
355
|
+
if (payload.color && payload.color.hasOwnProperty('x') && payload.color.hasOwnProperty('y')) {
|
|
356
|
+
const colorval = rgb.cie_to_rgb(payload.color.x, payload.color.y);
|
|
357
|
+
return '#' + utils.decimalToHex(colorval[0]) + utils.decimalToHex(colorval[1]) + utils.decimalToHex(colorval[2]);
|
|
358
|
+
} else {
|
|
359
|
+
return undefined;
|
|
360
|
+
}
|
|
361
|
+
},
|
|
362
|
+
epname: expose.endpoint,
|
|
363
|
+
setattr: 'color',
|
|
364
|
+
}, prop.access);
|
|
365
|
+
break;
|
|
366
|
+
}
|
|
367
|
+
case 'color_hs': {
|
|
368
|
+
const stateNameH = expose.endpoint ? `color_${expose.endpoint}` : 'color';
|
|
369
|
+
pushToStates({
|
|
370
|
+
id: stateNameH,
|
|
371
|
+
prop: expose.endpoint ? `color_${expose.endpoint}` : 'color',
|
|
372
|
+
name: `Color ${expose.endpoint ? expose.endpoint : ''}`.trim(),
|
|
373
|
+
icon: undefined,
|
|
374
|
+
role: 'level.color.rgb',
|
|
375
|
+
write: true,
|
|
376
|
+
read: true,
|
|
377
|
+
type: 'string',
|
|
378
|
+
setter: (value) => {
|
|
379
|
+
const _rgb = colors.ParseColor(value);
|
|
380
|
+
const hsv = rgb.rgbToHSV(_rgb.r, _rgb.g, _rgb.b, true);
|
|
381
|
+
return {
|
|
382
|
+
hue: Math.min(Math.max(hsv.h,1),359),
|
|
383
|
+
saturation: hsv.s,
|
|
384
|
+
// brightness: Math.floor(hsv.v * 2.55),
|
|
385
|
+
|
|
386
|
+
};
|
|
387
|
+
},
|
|
388
|
+
setterOpt: (value, options) => {
|
|
389
|
+
const hasTransitionTime = options && options.hasOwnProperty('transition_time');
|
|
390
|
+
const transitionTime = hasTransitionTime ? options.transition_time : 0;
|
|
391
|
+
return {...options, transition: transitionTime};
|
|
392
|
+
},
|
|
393
|
+
epname: expose.endpoint,
|
|
394
|
+
setattr: 'color',
|
|
395
|
+
}, prop.access);
|
|
396
|
+
pushToStates({
|
|
397
|
+
id: expose.endpoint ? `hue_${expose.endpoint}` : 'hue',
|
|
398
|
+
prop: expose.endpoint ? `color_${expose.endpoint}` : 'color',
|
|
399
|
+
name: `Hue ${expose.endpoint ? expose.endpoint : ''}`.trim(),
|
|
400
|
+
icon: undefined,
|
|
401
|
+
role: 'level.color.hue',
|
|
402
|
+
write: true,
|
|
403
|
+
read: false,
|
|
404
|
+
type: 'number',
|
|
405
|
+
min: 0,
|
|
406
|
+
max: 360,
|
|
407
|
+
inOptions: true,
|
|
408
|
+
setter: (value, options) => {
|
|
409
|
+
return {
|
|
410
|
+
hue: value,
|
|
411
|
+
saturation: options.saturation,
|
|
412
|
+
};
|
|
413
|
+
},
|
|
414
|
+
setterOpt: (value, options) => {
|
|
415
|
+
const hasTransitionTime = options && options.hasOwnProperty('transition_time');
|
|
416
|
+
const transitionTime = hasTransitionTime ? options.transition_time : 0;
|
|
417
|
+
const hasHueCalibrationTable = options && options.hasOwnProperty('hue_calibration');
|
|
418
|
+
if (hasHueCalibrationTable)
|
|
419
|
+
try {
|
|
420
|
+
return {...options, transition: transitionTime, hue_correction: JSON.parse(options.hue_calibration)};
|
|
421
|
+
}
|
|
422
|
+
catch {
|
|
423
|
+
const hue_correction_table = [];
|
|
424
|
+
options.hue_calibration.split(',').forEach(element => {
|
|
425
|
+
const match = /([0-9]+):([0-9]+)/.exec(element);
|
|
426
|
+
if (match && match.length ==3)
|
|
427
|
+
hue_correction_table.push({ in: Number(match[1]), out: Number(match[2])});
|
|
428
|
+
});
|
|
429
|
+
if (hue_correction_table.length > 0)
|
|
430
|
+
return {...options, transition: transitionTime, hue_correction: hue_correction_table};
|
|
431
|
+
}
|
|
432
|
+
return {...options, transition: transitionTime};
|
|
433
|
+
},
|
|
434
|
+
|
|
435
|
+
}, prop.access);
|
|
436
|
+
pushToStates({
|
|
437
|
+
id: expose.endpoint ? `saturation_${expose.endpoint}` : 'saturation',
|
|
438
|
+
prop: expose.endpoint ? `color_${expose.endpoint}` : 'color',
|
|
439
|
+
name: `Saturation ${expose.endpoint ? expose.endpoint : ''}`.trim(),
|
|
440
|
+
icon: undefined,
|
|
441
|
+
role: 'level.color.saturation',
|
|
442
|
+
write: true,
|
|
443
|
+
read: false,
|
|
444
|
+
type: 'number',
|
|
445
|
+
min: 0,
|
|
446
|
+
max: 100,
|
|
447
|
+
inOptions: true,
|
|
448
|
+
setter: (value, options) => {
|
|
449
|
+
return {
|
|
450
|
+
hue: options.hue,
|
|
451
|
+
saturation: value,
|
|
452
|
+
};
|
|
453
|
+
},
|
|
454
|
+
setterOpt: (value, options) => {
|
|
455
|
+
const hasTransitionTime = options && options.hasOwnProperty('transition_time');
|
|
456
|
+
const transitionTime = hasTransitionTime ? options.transition_time : 0;
|
|
457
|
+
const hasHueCalibrationTable = options && options.hasOwnProperty('hue_calibration');
|
|
458
|
+
if (hasHueCalibrationTable)
|
|
459
|
+
try {
|
|
460
|
+
return {...options, transition: transitionTime, hue_correction: JSON.parse(options.hue_calibration)};
|
|
461
|
+
}
|
|
462
|
+
catch {
|
|
463
|
+
const hue_correction_table = [];
|
|
464
|
+
options.hue_calibration.split(',').forEach(element => {
|
|
465
|
+
const match = /([0-9]+):([0-9]+)/.exec(element);
|
|
466
|
+
if (match && match.length ==3)
|
|
467
|
+
hue_correction_table.push({ in: Number(match[1]), out: Number(match[2])});
|
|
468
|
+
});
|
|
469
|
+
if (hue_correction_table.length > 0)
|
|
470
|
+
return {...options, transition: transitionTime, hue_correction: hue_correction_table};
|
|
471
|
+
}
|
|
472
|
+
return {...options, transition: transitionTime};
|
|
473
|
+
},
|
|
474
|
+
|
|
475
|
+
}, prop.access);
|
|
476
|
+
pushToStates(statesDefs.hue_move, prop.access);
|
|
477
|
+
pushToStates(statesDefs.saturation_move, prop.access);
|
|
478
|
+
pushToStates({
|
|
479
|
+
id: 'hue_calibration',
|
|
480
|
+
prop: 'color',
|
|
481
|
+
name: 'Hue color calibration table',
|
|
482
|
+
icon: undefined,
|
|
483
|
+
role: 'table',
|
|
484
|
+
write: true,
|
|
485
|
+
read: false,
|
|
486
|
+
type: 'string',
|
|
487
|
+
inOptions: true,
|
|
488
|
+
setter: (value, options) => {
|
|
489
|
+
return {
|
|
490
|
+
hue: options.hue,
|
|
491
|
+
saturation: options.saturation,
|
|
492
|
+
};
|
|
493
|
+
},
|
|
494
|
+
setterOpt: (value, options) => {
|
|
495
|
+
const hasTransitionTime = options && options.hasOwnProperty('transition_time');
|
|
496
|
+
const transitionTime = hasTransitionTime ? options.transition_time : 0;
|
|
497
|
+
const hasHueCalibrationTable = options && options.hasOwnProperty('hue_calibration');
|
|
498
|
+
if (hasHueCalibrationTable)
|
|
499
|
+
try {
|
|
500
|
+
return {...options, transition: transitionTime, hue_correction: JSON.parse(options.hue_calibration)};
|
|
501
|
+
}
|
|
502
|
+
catch {
|
|
503
|
+
const hue_correction_table = [];
|
|
504
|
+
options.hue_calibration.split(',').forEach(element => {
|
|
505
|
+
const match = /([0-9]+):([0-9]+)/.exec(element);
|
|
506
|
+
if (match && match.length ==3)
|
|
507
|
+
hue_correction_table.push({ in: Number(match[1]), out: Number(match[2])});
|
|
508
|
+
});
|
|
509
|
+
if (hue_correction_table.length > 0)
|
|
510
|
+
return {...options, transition: transitionTime, hue_correction: hue_correction_table};
|
|
511
|
+
}
|
|
512
|
+
return {...options, transition: transitionTime};
|
|
513
|
+
},
|
|
514
|
+
|
|
515
|
+
}, prop.access);
|
|
516
|
+
break;
|
|
517
|
+
}
|
|
518
|
+
default:
|
|
519
|
+
pushToStates(genState(prop), prop.access);
|
|
520
|
+
break;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
pushToStates(statesDefs.transition_time, ea.STATE_SET);
|
|
524
|
+
break;
|
|
525
|
+
|
|
526
|
+
case 'switch':
|
|
527
|
+
for (const prop of expose.features) {
|
|
528
|
+
switch (prop.name) {
|
|
529
|
+
case 'state':
|
|
530
|
+
pushToStates(genState(prop, 'switch'), prop.access);
|
|
531
|
+
break;
|
|
532
|
+
default:
|
|
533
|
+
pushToStates(genState(prop), prop.access);
|
|
534
|
+
break;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
break;
|
|
538
|
+
|
|
539
|
+
case 'numeric':
|
|
540
|
+
if (expose.endpoint) {
|
|
541
|
+
state = genState(expose);
|
|
542
|
+
} else {
|
|
543
|
+
switch (expose.name) {
|
|
544
|
+
case 'linkquality':
|
|
545
|
+
state = undefined;
|
|
546
|
+
break;
|
|
547
|
+
|
|
548
|
+
case 'battery':
|
|
549
|
+
state = statesDefs.battery;
|
|
550
|
+
break;
|
|
551
|
+
|
|
552
|
+
case 'voltage':
|
|
553
|
+
state = statesDefs.plug_voltage;
|
|
554
|
+
break;
|
|
555
|
+
|
|
556
|
+
case 'temperature':
|
|
557
|
+
state = statesDefs.temperature;
|
|
558
|
+
break;
|
|
559
|
+
|
|
560
|
+
case 'humidity':
|
|
561
|
+
state = statesDefs.humidity;
|
|
562
|
+
break;
|
|
563
|
+
|
|
564
|
+
case 'pressure':
|
|
565
|
+
state = statesDefs.pressure;
|
|
566
|
+
break;
|
|
567
|
+
|
|
568
|
+
case 'illuminance':
|
|
569
|
+
state = statesDefs.illuminance_raw;
|
|
570
|
+
break;
|
|
571
|
+
|
|
572
|
+
case 'illuminance_lux':
|
|
573
|
+
state = statesDefs.illuminance;
|
|
574
|
+
break;
|
|
575
|
+
|
|
576
|
+
case 'power':
|
|
577
|
+
state = statesDefs.load_power;
|
|
578
|
+
break;
|
|
579
|
+
|
|
580
|
+
default:
|
|
581
|
+
state = genState(expose);
|
|
582
|
+
break;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
if (state) pushToStates(state, expose.access);
|
|
586
|
+
break;
|
|
587
|
+
|
|
588
|
+
case 'enum':
|
|
589
|
+
switch (expose.name) {
|
|
590
|
+
case 'action': {
|
|
591
|
+
|
|
592
|
+
// Ansatz:
|
|
593
|
+
|
|
594
|
+
// Action aufspalten in 2 Blöcke:
|
|
595
|
+
// Action (bekommt text ausser hold und release, auto reset nach 250 ms)
|
|
596
|
+
// Hold: wird gesetzt bei hold, gelöscht bei passendem Release
|
|
597
|
+
|
|
598
|
+
if (!Array.isArray(expose.values)) break;
|
|
599
|
+
const hasHold = expose.values.find((actionName) => actionName.includes('hold'));
|
|
600
|
+
const hasRelease = expose.values.find((actionName) => actionName.includes('release'));
|
|
601
|
+
for (const actionName of expose.values) {
|
|
602
|
+
// is release state ? - skip
|
|
603
|
+
if (hasHold && hasRelease && actionName.includes('release')) continue;
|
|
604
|
+
// is hold state ?
|
|
605
|
+
if (hasHold && hasRelease && actionName.includes('hold')) {
|
|
606
|
+
const releaseActionName = actionName.replace('hold', 'release');
|
|
607
|
+
state = {
|
|
608
|
+
id: actionName.replace(/\*/g, ''),
|
|
609
|
+
prop: 'action',
|
|
610
|
+
name: actionName,
|
|
611
|
+
icon: undefined,
|
|
612
|
+
role: 'button',
|
|
613
|
+
write: false,
|
|
614
|
+
read: true,
|
|
615
|
+
type: 'boolean',
|
|
616
|
+
getter: payload => (payload.action === actionName) ? true : (payload.action === releaseActionName) ? false : undefined,
|
|
617
|
+
};
|
|
618
|
+
} else {
|
|
619
|
+
state = {
|
|
620
|
+
id: actionName.replace(/\*/g, ''),
|
|
621
|
+
prop: 'action',
|
|
622
|
+
name: actionName,
|
|
623
|
+
icon: undefined,
|
|
624
|
+
role: 'button',
|
|
625
|
+
write: false,
|
|
626
|
+
read: true,
|
|
627
|
+
type: 'boolean',
|
|
628
|
+
getter: payload => (payload.action === actionName) ? true : undefined,
|
|
629
|
+
isEvent: true,
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
pushToStates(state, expose.access);
|
|
633
|
+
}
|
|
634
|
+
state = null;
|
|
635
|
+
break;
|
|
636
|
+
}
|
|
637
|
+
default:
|
|
638
|
+
state = genState(expose);
|
|
639
|
+
break;
|
|
640
|
+
}
|
|
641
|
+
if (state) pushToStates(state, expose.access);
|
|
642
|
+
break;
|
|
643
|
+
|
|
644
|
+
case 'binary':
|
|
645
|
+
if (expose.endpoint) {
|
|
646
|
+
state = genState(expose);
|
|
647
|
+
} else {
|
|
648
|
+
switch (expose.name) {
|
|
649
|
+
case 'contact':
|
|
650
|
+
state = statesDefs.contact;
|
|
651
|
+
pushToStates(statesDefs.opened, ea.STATE);
|
|
652
|
+
break;
|
|
653
|
+
|
|
654
|
+
case 'battery_low':
|
|
655
|
+
state = statesDefs.heiman_batt_low;
|
|
656
|
+
break;
|
|
657
|
+
|
|
658
|
+
case 'tamper':
|
|
659
|
+
state = statesDefs.tamper;
|
|
660
|
+
break;
|
|
661
|
+
|
|
662
|
+
case 'water_leak':
|
|
663
|
+
state = statesDefs.water_detected;
|
|
664
|
+
break;
|
|
665
|
+
|
|
666
|
+
case 'lock':
|
|
667
|
+
state = statesDefs.child_lock;
|
|
668
|
+
break;
|
|
669
|
+
|
|
670
|
+
case 'occupancy':
|
|
671
|
+
state = statesDefs.occupancy;
|
|
672
|
+
break;
|
|
673
|
+
|
|
674
|
+
default:
|
|
675
|
+
state = genState(expose);
|
|
676
|
+
break;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
if (state) pushToStates(state, expose.access);
|
|
680
|
+
break;
|
|
681
|
+
|
|
682
|
+
case 'text':
|
|
683
|
+
state = genState(expose);
|
|
684
|
+
pushToStates(state, expose.access);
|
|
685
|
+
break;
|
|
686
|
+
|
|
687
|
+
case 'lock':
|
|
688
|
+
case 'fan':
|
|
689
|
+
case 'cover':
|
|
690
|
+
for (const prop of expose.features) {
|
|
691
|
+
switch (prop.name) {
|
|
692
|
+
case 'state':
|
|
693
|
+
pushToStates(genState(prop, 'switch'), prop.access);
|
|
694
|
+
break;
|
|
695
|
+
default:
|
|
696
|
+
pushToStates(genState(prop), prop.access);
|
|
697
|
+
break;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
break;
|
|
701
|
+
|
|
702
|
+
case 'climate':
|
|
703
|
+
for (const prop of expose.features) {
|
|
704
|
+
switch (prop.name) {
|
|
705
|
+
case 'away_mode':
|
|
706
|
+
pushToStates(statesDefs.climate_away_mode, prop.access);
|
|
707
|
+
break;
|
|
708
|
+
case 'system_mode':
|
|
709
|
+
pushToStates(statesDefs.climate_system_mode, prop.access);
|
|
710
|
+
break;
|
|
711
|
+
case 'running_mode':
|
|
712
|
+
pushToStates(statesDefs.climate_running_mode, prop.access);
|
|
713
|
+
break;
|
|
714
|
+
default:
|
|
715
|
+
pushToStates(genState(prop), prop.access);
|
|
716
|
+
break;
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
break;
|
|
720
|
+
|
|
721
|
+
case 'composite':
|
|
722
|
+
for (const prop of expose.features) {
|
|
723
|
+
const st = genState(prop);
|
|
724
|
+
st.prop = expose.property;
|
|
725
|
+
st.inOptions = true;
|
|
726
|
+
// I'm not fully sure, as it really needed, but
|
|
727
|
+
st.setterOpt = (value, options) => {
|
|
728
|
+
const result = {};
|
|
729
|
+
options[prop.property] = value;
|
|
730
|
+
result[expose.property] = options;
|
|
731
|
+
return result;
|
|
732
|
+
};
|
|
733
|
+
// if we have a composite expose, the value have to be an object {expose.property : {prop.property: value}}
|
|
734
|
+
if (prop.access & ea.SET) {
|
|
735
|
+
st.setter = (value, options) => {
|
|
736
|
+
const result = {};
|
|
737
|
+
options[prop.property] = value;
|
|
738
|
+
result[expose.property] = options;
|
|
739
|
+
return result;
|
|
740
|
+
};
|
|
741
|
+
st.setattr = expose.property;
|
|
742
|
+
};
|
|
743
|
+
// if we have a composite expose, the payload will be an object {expose.property : {prop.property: value}}
|
|
744
|
+
if (prop.access & ea.STATE) {
|
|
745
|
+
st.getter = payload => {
|
|
746
|
+
if ( (payload.hasOwnProperty(expose.property)) && (payload[expose.property] !== null) && payload[expose.property].hasOwnProperty(prop.property)) {
|
|
747
|
+
return !isNaN(payload[expose.property][prop.property]) ? payload[expose.property][prop.property] : undefined
|
|
748
|
+
}
|
|
749
|
+
else {
|
|
750
|
+
return undefined;
|
|
751
|
+
}
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
else {
|
|
755
|
+
st.getter = payload => { return undefined };
|
|
756
|
+
}
|
|
757
|
+
;
|
|
758
|
+
pushToStates(st, prop.access);
|
|
759
|
+
}
|
|
760
|
+
break;
|
|
761
|
+
default:
|
|
762
|
+
console.log(`Unhandled expose type ${expose.type} for device ${model}`);
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
const newDev = {
|
|
766
|
+
models: [model],
|
|
767
|
+
icon: icon,
|
|
768
|
+
states: states,
|
|
769
|
+
exposed: true,
|
|
770
|
+
};
|
|
771
|
+
// make the function code printable in log
|
|
772
|
+
//console.log(`Created mapping for device ${model}: ${JSON.stringify(newDev, function(key, value) {
|
|
773
|
+
// if (typeof value === 'function') {return value.toString() } else { return value } }, ' ')}`);
|
|
774
|
+
return newDev;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
function applyExposes(mappedDevices, byModel, allExcludesObj) {
|
|
778
|
+
// for exlude search
|
|
779
|
+
const allExcludesStr = JSON.stringify(allExcludesObj);
|
|
780
|
+
// create or update device from exposes
|
|
781
|
+
for (const deviceDef of zigbeeHerdsmanConverters.definitions) {
|
|
782
|
+
const strippedModel = (deviceDef.model) ? deviceDef.model.replace(/\0.*$/g, '').trim() : '';
|
|
783
|
+
// check if device is mapped
|
|
784
|
+
const existsMap = byModel.get(strippedModel);
|
|
785
|
+
|
|
786
|
+
if ((deviceDef.hasOwnProperty('exposes') && (!existsMap || !existsMap.hasOwnProperty('states'))) || allExcludesStr.indexOf(strippedModel) > 0) {
|
|
787
|
+
try {
|
|
788
|
+
const newDevice = createFromExposes(strippedModel, deviceDef);
|
|
789
|
+
if (!existsMap) {
|
|
790
|
+
mappedDevices.push(newDevice);
|
|
791
|
+
byModel.set(strippedModel, newDevice);
|
|
792
|
+
} else {
|
|
793
|
+
existsMap.states = newDevice.states;
|
|
794
|
+
existsMap.exposed = true;
|
|
795
|
+
}
|
|
796
|
+
} catch (e) {
|
|
797
|
+
console.log(`Wrong expose devicedefinition ${deviceDef.vendor} ${deviceDef.model}` );
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
module.exports = {
|
|
804
|
+
applyExposes: applyExposes,
|
|
805
805
|
};
|