iobroker.zigbee 1.8.9 → 1.8.12

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